【读书笔记】《Go语言高级编程》——柴树杉,曹春晖

夏宪
2023-12-01

这本书并不适合初学者阅读,适合对Go语言的应用有一些心得,并希望能够深入理解底层实现原理或者是希望能够在Web开发方面结合Go语言来实现进阶学习的技术人员学习和参考。

第一章:语言基础。1、Go的基因来自CSP理论(贝尔实验室)、面向对象和包的特性、C语言;2、数组、字符串和切片三者是密切相关的数据结构。这3种数据类型,在底层原始数据有着相同的内存结构,在上层,因为语法的限制而有着不同的行为表现。字符串底层数据也是对应的字节数组,但是字符串的只读属性禁止了在程序中对底层字节数组的元素的修改。切片的行为更为灵活,切片的结构和字符串结构类似,但是解除了只读限制。切片的底层数据虽然也是对应数据类型的数组,但是每个切片还有独立的长度和容量信息,切片赋值和函数传参时也是将切片头信息部分按传值方式处理。因为切片头含有底层数据的指针,所以它的赋值也不会导致底层数据的复制;3、函数主要有具名和匿名之分,包级函数一般都是具名函数,具名函数是匿名函数的一种特例。函数还可以直接或间接地调用自己,也就是支持递归调用。每个Goroutine刚启动时只会分配很小的栈(4KB或8KB,具体依赖实现),根据需要动态调整栈的大小,栈最大可以达到GB级,在Go 1.4以前,Go的动态栈采用的是分段式的动态栈,通俗地说就是采用一个链表来实现动态栈,每个链表的节点内存位置不会发生变化。为了解决热点调用的CPU缓存命中率问题,Go 1.4之后改用连续的动态栈实现,也就是采用一个类似动态数组的结构来表示栈。不过连续动态栈也带来了新的问题:当连续栈动态增长时,需要将之前的数据移动到新的内存空间,这会导致之前栈中全部变量的地址发生变化。Go的接口类型是对其他类型行为的抽象和概括,通过这种抽象的方式我们可以让对象更加灵活和更具有适应能力。Go语言中接口类型的独特之处在于它是满足隐式实现的鸭子类型。所谓鸭子类型说的是:只要走起路来像鸭子、叫起来也像鸭子,那么就可以把它当作鸭子。4、Goroutine之间是共享内存的。系统级线程都会有一个固定大小的栈(一般默认可能是2MB)。原子操作:sync.Mutex锁,sync/atomic包,可以实现sync.Once的功能。注意同一个Goroutine并不能保证顺序一致性。5、并发模型:通过锁实现、channel实现、sync.WaitGroup等待组可以实现并发版的“Hello World”。生产者/消费者模型。发布/订阅模型。通过godco/vfs包、带缓存的channel可是实现控制并发数量。注意并发的安全退出:例如select中加入time.After()超时回调或者default语句,也可以通过select来阻止main()函数退出,其中关闭select监听的某个channel也可以控制并发的退出。其次,可以利用context包控制并发的退出;6、捕获异常不是最终的目的。如果异常不可预测,直接输出异常信息是最好的处理方式。recover()函数必须写在defer的匿名函数里面,必须要和有异常的栈帧只隔一个栈帧,recover()函数才能正常捕获异常。换言之,recover()函数捕获的是祖父一级调用函数栈帧的异常(刚好可以跨越一层defer()函数)!

第六章:分布式系统。1、分布式id生成器:Twitter的snowflake算法;2、分布式锁:基于Redis的setnx,基于ZooKeeper,基于etcd。如果发展到了分布式服务阶段,但在业务规模不大、每秒查询数很小的情况下,使用哪种锁方案都差不多。业务发展到一定量级的话,就需要从多方面来考虑了。首先是你的锁是否在任何恶劣的条件下都不允许数据丢失,如果不允许,那么就不要使用Redis的setnx的简单锁。如果对锁数据的可靠性要求极高,那就只能使用etcd或者ZooKeeper这种通过一致性协议保证数据可靠性的锁方案。但可靠的背后往往都是较低的吞吐量和较高的延迟;3、延时任务系统:定时器分为时间堆(小顶堆实现)和时间轮。任务分发方案有消息队列(可能会导致延时增加)和处理消息回调(可能会阻塞后续的任务执行)两种模式;4、负载均衡:Fisher-Yates算法洗牌算法:每次随机挑选一个值,放在数组末尾。然后在n-1个元素的数组中再随机挑选一个值,放在数组末尾,依此类推。

附录部分重点:1、数组是值传递,不是地址;2、recover()必须在defer函数中运行;3、不同Goroutine之间不满足顺序一致性内存模型;4、切片会导致整个底层数组被锁定,底层数组无法释放内存。如果底层数组较大需要及时释放;5、空指针和空接口不等价;6、Goroutine未退出会泄漏内存。

 

 类似资料: