Open wisecsj opened 5 years ago
Golang 反射三大定律:
反射是从接口类型变量到反射对象(reflect.Type,relect.Value)
然后从反射对象反射到接口类型变量
如果需要修改一个反射对象,那么它的值需要是settable的
a%(2^b) 如何用位运算实现?
a&(1<<b-1)
Go语言中的map是用hashtable实现的,解决哈希碰撞是用的拉链法。即数组+链表的组合,go源码中将链表称为bucket,桶。
bucket的结构是这样的:
topbit 保存的是某个key hash之后得到的数的高八位,如下图中的hashtop实现:
每个bucket最多可以保存八个elem,然后查询的时候,go可以利用topbit来快速的进行比对(而不是通过key的比较),加快查询速度。同时为了优化内存布局,每个bucket中的key、value是分开集中存储的(见bucket结构图)
参考:
关于字节对齐的一篇文章
Data alignment: Straighten up and fly right
---- Updated 2020.10.31 ----
今天在查原子操作相关资料又涉及到了这个字节aligned话题。这些话题说实话,跟底层硬件结合的挺紧密的,不像我们写业务代码,只需要关注业务逻辑。
要理解字节对齐产生的原因,需要知道三个东西:
cpu怎么从内存拿数据,可以拿多少byte的数据
就像cpu有字长的概念,cpu一次能从内存读取的最大数据取决于数据总线宽度(总线带宽= 频率*宽度)
为什么cpu不支持从任意的内存地址取四个连续字节呢?
相信一开始很多人会有这个疑问🤔️,很自然。原因其实在于我们的内存是由多个内存芯片而组成,它们并不是一个真正物理上连续的byte数组。Why misaligned address access incur 2 or more accesses?
总的来说,硬件可以那么做,但是结果是为了20%不到的场景牺牲了整体的性能,同时增加了硬件电路复杂度
为什么我们除了IP地址还需要MAC地址?
子网中的两台主机进行通信,往往需要用到ARP协议(ip地址 -> mac地址)。虽然arp packet里既包含ip地址,也包含mac地址,但是是封装在链路层帧中的。
Linux下,可以通过 arp -a 查看本机 arp table entries。
这是一个展示子网通信对arp表影响的结果截图:
UNIX-LIKE 系统下如何设置daemon进程/让进程后台运行?
比如我用的是ubuntu桌面版本,因为有很多常用的软件是没有ubuntu版的(netease musical、wechat、qq),然后从github或其他办法下载到其他开发者实现的版本,往往是一个二进制可执行文件。
一般会先设置.bashrc快捷命令,然后比如在shell里执行wechat,但是这些大部分软件自身是没有实现daemon代码的,所以会在前台执行,然后hang住当前terminal,并且关闭当前terminal软件也会退出(相比之下,vscode就不会出现这种情况,自身实现了daemon化)。显然这并不是我们所期望的,so...
常见的办法,利用nohup和&的组合:alias calc='nohup gnome-calculator </dev/null &>/dev/null &'
比如上面这条命令,之后在shell里输入calc,就可以直接打开gnome自带的calculator,并且退出terminal也不会对应用自身有任何影响。并将应用的stdin/stdout重定向到/dev/null,/dev/null代表空设备,这样控制台不会有应用相关的输出显示。
那,有的应用可能要sudo权限,你又想后台运行,如何做?利用 sudo -b:sudo -b netease-cloud-music </dev/null &>/dev/null
有的应用不同,他可能是作为service一直要运行在后台,并且退出需要restart的。这时候就应该使用systemd来创建service。
参考链接:
https://lo-li.cn/1168 http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-part-two.html
因为无法确定reader的大小,所以采取的策略是开一个buf字节数组,循环对Reader进行read(buf)操作,然后对Writer执行write(buf)
适用于已经得到完整的字节切片数据了,直接构建Reader
当未得到完整数据,或者是需要不断写入的场景时,new bytes.Buffer是更好的选择。它会自动地帮我们进行扩容来存储写进的数据
override了r的Read方法,当调用r.Read(buf)时,内部也会调用w.Write(buf)
Linux下快速找到占用端口的PID:lsof -i:PORT_NUMBER
在配合kill命令即可结束占用端口的对应进程
Mysql 目前仍然不支持text类型字段设置默认值,会报错
google也没看到官方一个明确的说法(一个靠谱的说法是因为text的实现机制与其他类型不同,单独存储),只能在应用里显式设置了
关于gorm,准确来说是关于database/sql中的接口,如何实现自定义数据结构的序列化和反序列化: https://github.com/jinzhu/gorm/issues/47
gorm 以结构体形式传参数,进行create/updates时,会自动忽略zero value字段。从而保证只更新我们想更新的字段,避免将数据库中其它字段设为zero value
golang中的for range底层实现跟python不太一样。可以认为是在进行for range操作时,是对当时的range的对象进行了拷贝,然后对拷贝对象进行for range。所以在for range里对原range对象进行操作,不影响当下的for range
bytes.Buffer -> io.Pipe
避免潜在的内存开销和垃圾回收
一般我们说状态机特指有限状态机。而有限状态机又分为确定有限状态机,不确定有限状态机(一个状态对于一个输入可能又多个状态转移路径)。
设计优化的流程:不确定有限状态机 -> 确定有限状态机 -> 最小确定有限状态机
某些时候,我们只select了一个字段,并且这个字段的类型为基础类型(int,string,etc)
但是直接用First会报错:destination should be slice or struct
这个时候,可以“回退”到标准库的database/sql用法:.Row().Scan()
来解决
json.Marshal时关于slice的注意点:
s := make([]string, 0)
var m []string
b, _ := json.Marshal(s)
a, _ := json.Marshal(m)
a=="[]" b=="null"
这里的数据库特指Mysql,其它的不了解。
我们知道在golang中可以利用context包来实现goroutine的“终止”。那当goroutine里的操作为db操作,在golang里cancle后,如果数据库已经在执行对应的statement,是否也会终止呢?
答案是否定的。我们都知道标准库database/sql内含一个db的连接池,每次db操作都会先从池子里去取。而如果需要terminate对应的sql语句,实际上也就是我们在terminal里使用的kill query {{process_list_id}}
,必然要知道当前连接的process_list_id(connection_id?)。而在连接池的实现下,因为连接会被复用,connection_id也就意味着一样,贸然kill可能会错误地将其它goroutine的sql语句给kill掉。
所以一种解决方案是起一个独立conn,获取其connction_id,在该conn上执行sql语句,并在cancel的时候执行kill
https://mysqlserverteam.com/mysql-connection-handling-and-scaling/
on update CURRENT_TIMESTAMP
(未特殊指定,数据库皆指Mysql InnoDB引擎)
当我们需要知道一条记录的最近一次更新时间,通过我们会add column,名为update_time
,在Extra上设置on update CURRENT_TIMESTAMP
来实现。之后该行上其它列进行更新,数据库都会自动更新update_time。
之前我的认知是只要执行update语句,update_time字段就会更新。后来实现&&Google后发现如果update返回的rows affected
为0的话,是不会更新的。
本以为到此就终结了,后来又发现,不对啊,同样的update语句执行多次后,update_time居然也是会随之更新的(认知再一次被颠覆...)。经过实验(again),发现问题出现在对允许为NULL的字段的更新上:如果字段A可为NULL,当update包含A=NULL时,即时update前后都是NULL,return的rows affected也不为0
后来,人们为了彻底解决定义的时间的流逝不均匀的问题,开始使用原子钟定义时间。人们首先用全世界的原子钟共同为地球确立了一个均匀流动的时间,称为国际原子时(International Atomic Time, TAI)。然后,为了使定义的时间与地球自转相配合,人们通过在TAI的基础上不定期增减闰秒的方式,使定义的时间与世界时(UT1)保持差异在0.9秒以内,这样定义的时间就是协调世界时(Coordinated Universal Time, UTC)。UTC是目前全世界使用的时间标准。UTC与UT1之间的差异被称为DUT1。
目前,“格林尼治标准时间”一词在民用领域常常被认为与UTC相同,不过它在航海领域仍旧指UT1。
1.格林尼治标准时间
2.协调世界时
协调世界时(UTC)正式形成于1963年国际无线电咨询委员会的374号建议中[6],该建议由多国时间实验室共同提出。人们对该时间系统进行过数次调整,直到1972年引入了闰秒机制,调整工作得以简化。也有很多人提议用一个没有闰秒的时间系统来替换掉协调世界时,但目前尚未就此达成一致。
现行的协调世界时根据国际电信联盟的建议《Standard-frequency and time-signal emissions》(ITU-R TF.460-6)所确定[7]。UTC基于国际原子时,并通过不规则的加入闰秒来抵消地球自转变慢的影响[8]。闰秒在必要的时候会被插入到UTC中,以保证协调世界时(UTC)与世界时(UT1)相差不超过0.9秒[9]。
3.闰秒
这篇golang groups里有关的讨论:https://groups.google.com/g/golang-nuts/c/BZ-BTHQ9IYo
总结下来,还是底层硬件实现的差异化:
4/8 字节的存取操作是原子地前提是字节对齐
单条cpu指令也可能是非原子的(ARMv7 strd指令)
最近一个代码的bug,大致逻辑就是在一个for循环里,每个循环开启一个事务,然后defer Close()。在某些场景下,会导致报如上的错误。
最终定位是因为所使用的mysql默认事务隔离级别是RC,在RC级别下,在事务A里对某行的更新(update),会使得事务A持有该行行锁;如果此时开启事务B,由于RC,导致业务逻辑也会走到更新该行,那便需要持有该行行锁。但是我们是用的defer,导致事务A没有及时commit,导致事务B等待行锁超时,报错返回。
解决方式也很简单,显式调用下tx.Commit。当然了,这是表因,根因的话其实还是需要深入了解innodb各个事务隔离级别下的事务实现(Todo
最近遇到一些关于http 1.1 协议实现的问题和golang在实现上的一个小坑,于是整体浏览下do的工作流程。 (阅读中发现整个http实现的代码不是很清爽,因为夹杂了很多test,trace和超时控制类的逻辑在里面,非核心逻辑都会忽略)
Notice:
TenProtect Conficential
顺带元旦第一天把cheat engine的9个tutorial给做了,更完整地熟悉了下ce地使用和wg原理。(最一开始接触到ce,还是百度云盘会员300s试用地时候:) )
不说他们的具体实现细节吧,就譬如协议的位结构是怎样的啊,这种直接看rfc就好了
说说它们的异同和使用场景。http/2其实基本就是google spdy协议的标准化,将文本协议换位二进制协议,多路复用,支持hpack头压缩算法和其它的一些功能(grpc使用的http/2)
websocket的出现则是为了解决服务器和客户端实时通信的痛点,在之前可能都是通过长轮询的方式来做,开销大且并不实时
所以,它们虽然都是二进制协议,都是基于tcp的应用层协议,http2目的主要为了解决ping-pong响应式的http请求的性能优化(不必再每个请求建一次tcp链接,不会有阻塞问题等);而websocket则是解决实时问题,适用于聊天室,实时协同编辑的场景
cpu在一个时钟周期内能够处理的位数,也称作字长
没有必然联系,一般32位cpu地址总线为32位,64位cpu为36或4x位
一般相同,但是因为cpu64位兼容32位运行模式(向后兼容)。所以32位的操作系统也可以run在64位cpu上
编程中获取的指针地址都是va,会经由OS(还有专门的硬件辅助MMU)转换成物理地址
这个理解起来可能有点难,因为实际上涉及到很多因素。我个人理解,一般来说,因为指针存储的是地址编号,本质也是一个数值(即数据),所以需要跟cpu位数(cpu寄存器)匹配。所以64位cpu的指针一般就是64位(除开运行在32位模式和一些非通用的cpu)
http包里的client在302跳转的时候会继承原有的header,除了带有私密,安全属性的header。譬如cookies,authentication。这些带有安全性质的header会进行一个check,比如校验是否域名相同或是否为子域,否则时不回带上原有cookie的。
比如 m.tiktok.com -> www.tiktok.com , Cookie Header是不会继承的
同理,cookieJar也时会进行类似的安全校验
Golang对query的处理逻辑也需要注意。譬如net.URL.Query().Encode(),内部实现是先unescape再escape,并且unescape return error 的pair会直接丢弃——对于rawquery为key=100%含金量
,encode后会丢失;第二点就是encode因为用的是map,输出序是不稳定的,所以为了保证结果的稳定性,会先对key进行sort(会破坏原始query的顺序,变成字母序,从而一些req的校验逻辑不通过)
因为hmap中的Buckets数量保证了是2^B次方,所有当我们需要根据hash之后的值M对len(Buckets)取余时,也就是M%2^B=M&(2^B-1). 思想就是M%2^B其实相当于右移B位,而这个B位的值就是我们需要的余数。所以问题就转化成了如何取M的低B位
这个应该是涉及到内存分配的优化
当你拿着手机仰望天空时,如下图,假设你的地理坐标系坐标为x,y,z:
可以得到: 那会奇怪,三元方程,那三颗卫星数据就够了不是么?
问题在于delta t. 我们无法使用手机本身的石英时钟,与原子钟相比误差太大. 而走网络授时中心同步,也会有网络延迟和同步不及时的问题。综上,此刻的时间也需设置为一个未知数,从而需要4颗卫星的数据
另一个问题是误差。误差的来源有很多,比如光在空气中的传播速度受天气影响等。解决的思路是靠标准参照来差分消除误差:譬如基站的地理坐标系是确定的,它同时也作为“手机”接收gps信号并进行坐标计算,从而得到误差值。基站再把这个差值信息传递给手机,进行修正
Linux I/O 多路复用
select/poll的机制基本是一样的,只是传递到内核的数据格式不同:一个是整型数组,一个是结构体数组
select/poll即使只有一个描述符就绪,也需要遍历整个集合
epoll只返回就绪的描述符集合,epoll同时支持水平触发(默认)和边缘触发
epoll无须每次执行都从用户空间复制句柄到内核
参考:epoll详解和使用