读《浅谈Go语言实现原理》


第三章 数据结构

3.1 数组

[...]T类型的数组在编译期间就会被推断为确定大小的数组
元素小于等于4个的数组会直接在栈上初始化;大于4个的会在静态存储区初始化然后拷贝到栈上
越界检查是在编译期间进行

3.2 切片

切片内元素类型是编译期确定的
切片(data-uintptr len cap)
切片的copy直接使用memmove或数组指针的方式将整块内存中的内容拷贝到目标内存区域
切片初始化方式:

1
2
3
4
5
6
// 通过下标方式获得数组或切片的一部分
arr[0:3]  slice[0:3]
// 使用字面量初始化新的切片
slice := []int{1, 2, 3}
// 使用关键字 make
slice := make([]int, 2)

3.2 哈希表

拉链法实现
溢出桶的数量和装载因子决定是否扩容
扩容是在写入或删除时触发的,不会造成性能瞬时大的波动
扩容时将桶的数量分配,元素再分配
哈希tophash相当于一级缓存(!!!!!!没懂啊这是啥)

3.4 字符串

字符串是一个只读的字节数组切片
运行时结构

1
2
3
4
type struct StringHeader struct {
	Data uintptr
	Len int
}

字符串的解析是解析器在词法分析时完成的
声明时使用双引号(",单行)或反引号(`,可多行)
拼接:
小于5为contactstring{2,3,4,5}
concatstrings(传入数组切片):
过滤空字符串> 获取拼接后总长度> 调用copy将所有字符串复制到新分配的内存空间

类型转换:

1
2
3
string (RO)           []byte (RW)
stringtoslicebyte     slicebytetostring
尽量减少对字符串的类型转换操作注意性能问题

第四章

4.1 函数调用

c语言:

  • 参数少于6个时使用寄存器,多于6个时还使用了栈传递函数的参数;
  • 函数的返回值通过eax寄存器进行传递
    go语言:
  • 传递和接受参数使用的都是栈,压栈顺序是从右到左,返回参数由调用函数预先分配内存空间
  • 传递参数时接受函数会复制一份参数再进行计算

很多人都会说无论是传值还是传引用本质上都是对值的传递,这种论调其实并没有什么意义,我们其实需要知道对函数中参数的修改会不会影响调用方栈上的内容

4.2 接口

  • go语言中接口不能包含变量(与java不同)
  • go只会在传递、返回参数及变量赋值时检查结构体实现了哪些接口
  • 实现接口时只需要实现接口中的全部方法,不需显示声明实现接口
  • 接口中传递结构体使用指针,降低动态派发开销
    动态派发:接口类型运行期间决定调用它的方法的哪个实现
    类型断言:将一个接口转换成具体类型
    协变(具体类型转接口)、逆变(接口转具体类型)

4.3 反射

反射的使用:

  • 在运行时判断类型、判断类型是否实现了某些接口
  • 获取和修改变量
  • 动态调用方法

第五章 常用关键字

5.1 for和range

range遍历数组、哈希表、channel等元素

  • 数组和哈希表:
    因为数组和哈希表内存占用都是连续的,遍历清空数组的代码会在编译的时候优化为清空一片内存地址的内容
    range遍历哈希表顺序是随机的(选择遍历开始的桶时使用了随机数)
    for i, v := range list中v在每一次循环都会被重新赋值为一个拷贝变量,所以想获取每个元素的地址时不能用&v,要用&list[i]
  • 字符串
  • channel

5.2 select

select可以对同时多个channel进行非阻塞的收发,在多个channel同时响应时随机选择一个case执行

  • 空的 select 语句会被直接转换成 block 函数的调用,直接挂起当前 Goroutine;
  • 如果 select 语句中只包含一个 case,就会被转换成 if ch == nil { block }; n; 表达式;
    • 首先判断操作的 Channel 是不是空的;
    • 然后执行 case 结构中的内容;
  • 如果 select 语句中只包含两个 case 并且其中一个是 default,那么 Channel 的接收和发送操作都会使用 selectnbrecv 和 selectnbsend 非阻塞地执行接收和发送操作;
  • 在默认情况下会通过 selectgo 函数选择需要执行的 case 并通过多个 if 语句执行 case 中的表达式;

5.3 defer

最后调用的 defer 会先执行
defer 并不是在退出当前代码块作用域时执行的,defer 只会在当前函数和方法返回之前被调用
defer 也会对被传递的变量进行复制
defer 会在编译期间被转换成 deferproc 的函数调用,调用 defer 的函数会在返回之前插入 deferreturn 指令;在运行期间,每一个 deferproc 的调用都会将一个新的 _defer 结构体追加到当前 goroutine 持有的链表头,而 deferreturn 会从 goroutine 中取出 _defer 并依次执行,所有 _defer 结构执行完毕后才会返回

5.4 painc 和 recover

panic 可以连锁触发
recover 关键字会修改 _painc 结构体中的 recovered 字段

5.5 make 和 new

make 只能初始化 go 语言的基本类型,slice map channel 等
new 分配内存并创建一个指向对应类型的指针

第六章 并发编程

6.1 上下文 Context

go 语言中的每一个请求都是通过一个单独的 goroutine 进行处理的,可能会创建多个 goroutine 来处理一次请求
context 的主要作用是在不同 goroutine 之间同步请求特定的数据、取消信号、处理请求的截止日期
每一个 Context 都会从最顶层的 goroutine 一层一层传递到最下层。如果没有 Context, 当上层执行的操作出现错误时,下层不会收到错误而继续执行下去;使用 Context 就可以在下层及时停掉无用工作减少额外的消耗。Context 还能携带以请求为作用域的键值对信息。
Context 是 Go 语言 context 包对外暴露的接口,该接口定义了四个需要实现的方法:

  1. Deadline方法需要返回当前 Context 被取消的时间,也就是完成工作的截止日期;
  2. Done 方法需要返回一个 channel,该 channel 会在当前工作完成或者被取消之后关闭,多次调用 Done 方法会返回同一个 channel;
  3. Err 方法返回当前 Context 结束的原因,它只会在 Done 返回的 channel 被关闭时才会返回非空的值:
    • 如果当前 Context 被取消,返回 Canceled 错误;
    • 如果当前 Context 超时,返回 DeadlineExceeded 错误。
  4. Value 方法会从 Context 中返回键对应的值。对同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的值,这个功能可以用来传递请求特定的数据。

6.2 同步原语与锁

基本原语

位于 sync 包
互斥锁 Mutex
读写互斥锁 RWMutex

WaitGroup

1
2
3
4
5
type WaitGroup struct {
	noCopy noCopy		// 保证 WaitGroup 不会被开发者通过再赋值的方式进行拷贝,进而导致一些诡异的行为

	state1 [3]uint32	// waiter counter sema
}

WaitGroup 可以等待一系列的 goroutine 的返回
暴露了三个接口
WaitGroup 必须在 Wait 方法返回之后才能被重新使用
Add 方法的主要作用就是更新 WaitGroup 中持有的计数器 counter
Done 方法只是调用了 wg.Add(-1)

拓展原语

SingleFlight
能够在一个服务中抑制对下游的多次重复请求,对同一个 Key 只会进行一次函数调用,调用结果会返回并同步给所有请求同一 Key 的请求
减少对下游的瞬时流量
可以使用请求哈希作为抑制相同请求的键,或者使用关键字
使用 singlegroup 提供的 Group 包时需要注意以下问题:

  • Do 用于同步阻塞调用传入的函数,DoChan用于异步调用并通过 Channel 接受函数的返回值
  • Forget 可以通知 singleflight 在持有的映射表中删除某个键。接下来对该键的调用就会直接执行方法而不是等待前面的函数返回
  • 一旦调用的函数返回了错误,所有在等待的 Goroutine 都会接收到相同的错误

6.3 定时器

Go 语言中定时器计算的是相对时间
Timer 存储在堆中
TimersBucket 会存储一个处理器上的全部定时器,超过 64 核时,多个处理器上的定时器可能会存在一个桶里;每一个运行的 Go 程序都会在内存中存储 64 个桶,桶中存储定时器信息
NewTimer: 传入一个 channel 到期时通知
AfterFunc: 传入一个时间到时执行的 func

time.Sleep: 创建了一个会在到期时唤醒当前 Goroutine 的定时器,并用 goparkunlock 将当前协程陷入休眠
Ticker: NewTicker 创建的计时器需要调用 Stop 显式关闭;Ticker 创建的计时器由于只对外提供了 Channel 是无法被关闭的
定时器在内部使用四叉树方式进行实现和存储
定时器在毫秒级别高并发时会有很大误差,若时间间隔 100ms 以上误差较小

6.4 Channel

不要通过共享内存的方式进行通信,而是应该通过通信的方式共享内存。

Using Hugo & Soda theme
fc4soda ❤ 2018.04.24~