├── .gitignore ├── GC ├── assets │ ├── gc1.png │ ├── gc2.png │ ├── gc3.png │ ├── gc-leak1.png │ ├── gc-pacing.png │ ├── gc-trace.png │ ├── gc-trace2.png │ ├── gc-mutator.png │ ├── gc-process.png │ ├── gc-trigger.png │ ├── gc-trigger2.png │ ├── gc-trigger3.png │ ├── gc-wb-yuasa.png │ ├── gc-blueprint.png │ ├── gc-mark-assist.png │ ├── gc-mark-sweep.png │ ├── gc-tuning-ex3.png │ ├── gc-wb-dijkstra.png │ ├── gc-tuning-ex1-1.png │ ├── gc-tuning-ex1-2.png │ ├── gc-tuning-ex1-3.png │ ├── gc-tuning-ex1-4.png │ ├── gc-tuning-ex2-1.png │ ├── gc-tuning-ex2-2.png │ └── gc-tuning-ex2-3.png └── code │ ├── 5 │ └── main.go │ ├── 6 │ ├── main.go │ └── gc.txt │ ├── 7 │ └── main.go │ ├── 11 │ └── main.go │ ├── 14 │ ├── 1 │ │ ├── before │ │ │ └── main.go │ │ └── after │ │ │ └── main.go │ └── 2 │ │ ├── before │ │ └── main.go │ │ └── after │ │ └── main.go │ └── 20 │ ├── 4_test.go │ ├── 1.go │ └── 4.txt ├── 反射 ├── Go 语言中反射有哪些应用.md ├── 什么情况下需要使用反射.md ├── 什么是反射.md └── 如何比较两个对象完全相同.md ├── channel ├── channel 在什么情况下会引起资源泄漏.md ├── 操作 channel 的情况总结.md ├── 从一个关闭的 channel 仍然能读出数据吗.md ├── 什么是 CSP.md ├── channel 发送和接收元素的本质是什么.md ├── 关闭一个 channel 的过程是怎样的.md ├── 关于 channel 的 happened-before 有哪些.md ├── channel 有哪些应用.md ├── channel 底层的数据结构是什么.md ├── 如何优雅地关闭 channel.md └── 向 channel 发送数据的过程是怎样的.md ├── examples └── hello-world │ └── src │ ├── main.go │ └── util │ └── util.go ├── map ├── map 是线程安全的吗.md ├── 可以边遍历边删除吗.md ├── 可以对 map 的元素取地址吗.md ├── 如何比较两个 map 相等.md ├── map 中的 key 为什么是无序的.md ├── map 的删除过程是怎样的.md ├── 如何实现两种 get 操作.md ├── map 的赋值过程是怎样的.md ├── float 类型可以作为 map 的 key 吗.md └── map 的遍历过程是怎样的.md ├── 标准库 ├── context │ ├── context 是什么.md │ ├── context.Value 的查找过程是怎样的.md │ └── context 有什么作用.md └── unsafe │ ├── 如何实现字符串和byte切片的零拷贝转换.md │ ├── 如何利用unsafe包修改私有成员.md │ ├── Go指针和unsafe.Pointer有什么区别.md │ └── 如何利用unsafe获取slice&map的长度.md ├── goroutine 调度器 ├── 什么是M:N模型.md ├── goroutine 调度时机有哪些.md ├── goroutine和线程的区别.md ├── 什么是workstealing.md ├── schedule 循环如何运转.md ├── 一个调度相关的陷阱.md ├── 什么是 go shceduler.md ├── mian gorutine 如何创建.md ├── goroutine 如何退出.md ├── schedule 循环如何启动.md ├── g0 栈何用户栈如何切换.md └── GPM 是什么.md ├── interface ├── Go 接口与 C++ 接口有何异同.md ├── 编译器自动检测类型是否实现接口.md ├── 如何用 interface 实现多态.md ├── Go 语言与鸭子类型的关系.md ├── 接口的动态类型和动态值.md ├── iface 和 eface 的区别是什么.md ├── 值接收者和指针接收者的区别.md ├── 类型转换和断言的区别.md ├── 接口的构造过程是怎样的.md └── 接口转换的原理.md ├── 编译和链接 ├── GoRoot 和 GoPath 有什么用.md ├── Go 程序启动过程是怎样的.md ├── 逃逸分析是怎么进行的.md ├── Go 编译相关的命令详解.md └── Go 编译链接过程概述.md ├── README.md └── 数组和切片 ├── 切片作为函数参数.md ├── 数组和切片有什么异同.md └── 切片的容量是怎样增长的.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | log/ 3 | 4 | *.xml 5 | *.iml 6 | *.idea 7 | 8 | *.DS_Store 9 | -------------------------------------------------------------------------------- /GC/assets/gc1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc1.png -------------------------------------------------------------------------------- /GC/assets/gc2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc2.png -------------------------------------------------------------------------------- /GC/assets/gc3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc3.png -------------------------------------------------------------------------------- /GC/assets/gc-leak1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-leak1.png -------------------------------------------------------------------------------- /GC/assets/gc-pacing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-pacing.png -------------------------------------------------------------------------------- /GC/assets/gc-trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-trace.png -------------------------------------------------------------------------------- /GC/assets/gc-trace2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-trace2.png -------------------------------------------------------------------------------- /GC/assets/gc-mutator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-mutator.png -------------------------------------------------------------------------------- /GC/assets/gc-process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-process.png -------------------------------------------------------------------------------- /GC/assets/gc-trigger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-trigger.png -------------------------------------------------------------------------------- /GC/assets/gc-trigger2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-trigger2.png -------------------------------------------------------------------------------- /GC/assets/gc-trigger3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-trigger3.png -------------------------------------------------------------------------------- /GC/assets/gc-wb-yuasa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-wb-yuasa.png -------------------------------------------------------------------------------- /GC/assets/gc-blueprint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-blueprint.png -------------------------------------------------------------------------------- /GC/assets/gc-mark-assist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-mark-assist.png -------------------------------------------------------------------------------- /GC/assets/gc-mark-sweep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-mark-sweep.png -------------------------------------------------------------------------------- /GC/assets/gc-tuning-ex3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-tuning-ex3.png -------------------------------------------------------------------------------- /GC/assets/gc-wb-dijkstra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-wb-dijkstra.png -------------------------------------------------------------------------------- /GC/assets/gc-tuning-ex1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-tuning-ex1-1.png -------------------------------------------------------------------------------- /GC/assets/gc-tuning-ex1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-tuning-ex1-2.png -------------------------------------------------------------------------------- /GC/assets/gc-tuning-ex1-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-tuning-ex1-3.png -------------------------------------------------------------------------------- /GC/assets/gc-tuning-ex1-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-tuning-ex1-4.png -------------------------------------------------------------------------------- /GC/assets/gc-tuning-ex2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-tuning-ex2-1.png -------------------------------------------------------------------------------- /GC/assets/gc-tuning-ex2-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-tuning-ex2-2.png -------------------------------------------------------------------------------- /GC/assets/gc-tuning-ex2-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changkun/Go-Questions/HEAD/GC/assets/gc-tuning-ex2-3.png -------------------------------------------------------------------------------- /反射/Go 语言中反射有哪些应用.md: -------------------------------------------------------------------------------- 1 | Go 语言中反射的应用非常广:IDE 中的代码自动补全功能、对象序列化(encoding/json)、fmt 相关函数的实现、ORM(全称是:Object Relational Mapping,对象关系映射)…… -------------------------------------------------------------------------------- /GC/code/5/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "runtime" 5 | "time" 6 | ) 7 | 8 | func main() { 9 | go func() { 10 | for { 11 | } 12 | }() 13 | 14 | time.Sleep(time.Millisecond) 15 | runtime.GC() 16 | println("OK") 17 | } 18 | -------------------------------------------------------------------------------- /channel/channel 在什么情况下会引起资源泄漏.md: -------------------------------------------------------------------------------- 1 | Channel 可能会引发 goroutine 泄漏。 2 | 3 | 泄漏的原因是 goroutine 操作 channel 后,处于发送或接收阻塞状态,而 channel 处于满或空的状态,一直得不到改变。同时,垃圾回收器也不会回收此类资源,进而导致 gouroutine 会一直处于等待队列中,不见天日。 4 | 5 | 另外,程序运行过程中,对于一个 channel,如果没有任何 goroutine 引用了,gc 会对其进行回收操作,不会引起内存泄漏。 6 | -------------------------------------------------------------------------------- /examples/hello-world/src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "util" 6 | ) 7 | 8 | func main() { 9 | fmt.Println("hello world!") 10 | 11 | localIp, err := util.GetLocalIPv4Address() 12 | if err != nil { 13 | panic(err) 14 | } 15 | fmt.Printf("Local IP: %s\n", localIp) 16 | } -------------------------------------------------------------------------------- /map/map 是线程安全的吗.md: -------------------------------------------------------------------------------- 1 | map 不是线程安全的。 2 | 3 | 在查找、赋值、遍历、删除的过程中都会检测写标志,一旦发现写标志置位(等于1),则直接 panic。赋值和删除函数在检测完写标志是复位之后,先将写标志位置位,才会进行之后的操作。 4 | 5 | 检测写标志: 6 | 7 | ```golang 8 | if h.flags&hashWriting == 0 { 9 | throw("concurrent map writes") 10 | } 11 | ``` 12 | 13 | 设置写标志: 14 | 15 | ```golang 16 | h.flags |= hashWriting 17 | ``` 18 | -------------------------------------------------------------------------------- /反射/什么情况下需要使用反射.md: -------------------------------------------------------------------------------- 1 | 使用反射的常见场景有以下两种: 2 | 3 | 1. 不能明确接口调用哪个函数,需要根据传入的参数在运行时决定。 4 | 2. 不能明确传入函数的参数类型,需要在运行时处理任意对象。 5 | 6 | 【引申1】不推荐使用反射的理由有哪些? 7 | 8 | 1. 与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。 9 | 2. Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。 10 | 3. 反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。 -------------------------------------------------------------------------------- /标准库/context/context 是什么.md: -------------------------------------------------------------------------------- 1 | Go 1.7 标准库引入 context,中文译作“上下文”,准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。 2 | 3 | context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。 4 | 5 | 随着 context 包的引入,标准库中很多接口因此加上了 context 参数,例如 database/sql 包。context 几乎成为了并发控制和超时控制的标准做法。 6 | 7 | >context.Context 类型的值可以协调多个 groutine 中的代码执行“取消”操作,并且可以存储键值对。最重要的是它是并发安全的。 8 | 9 | >与它协作的 API 都可以由外部控制执行“取消”操作,例如:取消一个 HTTP 请求的执行。 -------------------------------------------------------------------------------- /map/可以边遍历边删除吗.md: -------------------------------------------------------------------------------- 1 | map 并不是一个线程安全的数据结构。同时读写一个 map 是未定义的行为,如果被检测到,会直接 panic。 2 | 3 | 上面说的是发生在多个协程同时读写同一个 map 的情况下。 如果在同一个协程内边遍历边删除,并不会检测到同时读写,理论上是可以这样做的。但是,遍历的结果就可能不会是相同的了,有可能结果遍历结果集中包含了删除的 key,也有可能不包含,这取决于删除 key 的时间:是在遍历到 key 所在的 bucket 时刻前或者后。 4 | 5 | 一般而言,这可以通过读写锁来解决:`sync.RWMutex`。 6 | 7 | 读之前调用 `RLock()` 函数,读完之后调用 `RUnlock()` 函数解锁;写之前调用 `Lock()` 函数,写完之后,调用 `Unlock()` 解锁。 8 | 9 | 另外,`sync.Map` 是线程安全的 map,也可以使用。 -------------------------------------------------------------------------------- /goroutine 调度器/什么是M:N模型.md: -------------------------------------------------------------------------------- 1 | 我们都知道,Go runtime 会负责 goroutine 的生老病死,从创建到销毁,都一手包办。Runtime 会在程序启动的时候,创建 M 个线程(CPU 执行调度的单位),之后创建的 N 个 goroutine 都会依附在这 M 个线程上执行。这就是 M:N 模型: 2 | 3 | ![M:N scheduling](https://user-images.githubusercontent.com/7698088/61340362-8c001880-a874-11e9-9237-d97e6105cd62.png) 4 | 5 | 在同一时刻,一个线程上只能跑一个 goroutine。当 goroutine 发生阻塞(例如上篇文章提到的向一个 channel 发送数据,被阻塞)时,runtime 会把当前 goroutine 调度走,让其他 goroutine 来执行。目的就是不让一个线程闲着,榨干 CPU 的每一滴油水。 -------------------------------------------------------------------------------- /channel/操作 channel 的情况总结.md: -------------------------------------------------------------------------------- 1 | 总结一下操作 channel 的结果: 2 | 3 | |操作|nil channel|closed channel|not nil, not closed channel| 4 | |---|---|---|---| 5 | |close|panic|panic|正常关闭| 6 | |读 <- ch|阻塞|读到对应类型的零值|阻塞或正常读取数据。缓冲型 channel 为空或非缓冲型 channel 没有等待发送者时会阻塞| 7 | |写 ch <-|阻塞|panic|阻塞或正常写入数据。非缓冲型 channel 没有等待接收者或缓冲型 channel buf 满时会被阻塞| 8 | 9 | 总结一下,发生 panic 的情况有三种:向一个关闭的 channel 进行写操作;关闭一个 nil 的 channel;重复关闭一个 channel。 10 | 11 | 读、写一个 nil channel 都会被阻塞。 12 | -------------------------------------------------------------------------------- /map/可以对 map 的元素取地址吗.md: -------------------------------------------------------------------------------- 1 | 无法对 map 的 key 或 value 进行取址。以下代码不能通过编译: 2 | 3 | ```golang 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | m := make(map[string]int) 10 | 11 | fmt.Println(&m["qcrao"]) 12 | } 13 | ``` 14 | 15 | 编译报错: 16 | 17 | ```shell 18 | ./main.go:8:14: cannot take the address of m["qcrao"] 19 | ``` 20 | 21 | 如果通过其他 hack 的方式,例如 unsafe.Pointer 等获取到了 key 或 value 的地址,也不能长期持有,因为一旦发生扩容,key 和 value 的位置就会改变,之前保存的地址也就失效了。 22 | -------------------------------------------------------------------------------- /goroutine 调度器/goroutine 调度时机有哪些.md: -------------------------------------------------------------------------------- 1 | 在四种情形下,goroutine 可能会发生调度,但也并不一定会发生,只是说 Go scheduler 有机会进行调度。 2 | 3 | |情形|说明| 4 | |---|---| 5 | |使用关键字 `go`|go 创建一个新的 goroutine,Go scheduler 会考虑调度| 6 | |GC| 由于进行 GC 的 goroutine 也需要在 M 上运行,因此肯定会发生调度。当然,Go scheduler 还会做很多其他的调度,例如调度不涉及堆访问的 goroutine 来运行。GC 不管栈上的内存,只会回收堆上的内存| 7 | |系统调用|当 goroutine 进行系统调用时,会阻塞 M,所以它会被调度走,同时一个新的 goroutine 会被调度上来| 8 | |内存同步访问|atomic,mutex,channel 操作等会使 goroutine 阻塞,因此会被调度走。等条件满足后(例如其他 goroutine 解锁了)还会被调度上来继续运行| -------------------------------------------------------------------------------- /反射/什么是反射.md: -------------------------------------------------------------------------------- 1 | 维基百科上反射的定义: 2 | 3 | >在计算机科学中,反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。 4 | 5 | 难道不用反射就不能在运行时访问、检测和修改它本身的状态和行为吗? 6 | 7 | 问题的回答,其实要首先理解什么叫访问、检测和修改它本身状态或行为,它的本质是什么? 8 | 9 | 实际上,它的本质是程序在运行期探知对象的类型信息和内存结构。不用反射能行吗?可以的!使用汇编语言,直接和内层打交道,可以获取任何信息?但是,当编程迁移到高级语言上来之后,就不行了!只能通过`反射`来达到此项技能。 10 | 11 | 不同语言的反射模型不尽相同,有些语言还不支持反射。《Go 语言圣经》中是这样定义反射的: 12 | 13 | > Go 语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法,但是在编译时并不知道这些变量的具体类型,这称为反射机制。 -------------------------------------------------------------------------------- /examples/hello-world/src/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "net" 5 | "errors" 6 | ) 7 | 8 | // 获取本机ip地址 9 | func GetLocalIPv4Address() (string, error) { 10 | addrs, err := net.InterfaceAddrs() 11 | if err != nil { 12 | return "", err 13 | } 14 | for _, a := range addrs { 15 | if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 16 | if ipnet.IP.To4() != nil { 17 | return ipnet.IP.String(), nil 18 | } 19 | } 20 | } 21 | return "", errors.New("no ipv4 address found!") 22 | } 23 | -------------------------------------------------------------------------------- /GC/code/14/2/before/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | _ "net/http/pprof" 7 | ) 8 | 9 | func newBuf() []byte { 10 | return make([]byte, 10<<20) 11 | } 12 | 13 | func main() { 14 | go func() { 15 | http.ListenAndServe("localhost:6060", nil) 16 | }() 17 | http.HandleFunc("/example2", func(w http.ResponseWriter, r *http.Request) { 18 | b := newBuf() 19 | for idx := range b { 20 | b[idx] = 1 21 | } 22 | fmt.Fprintf(w, "done, %v", r.URL.Path[1:]) 23 | }) 24 | http.ListenAndServe(":8080", nil) 25 | } 26 | -------------------------------------------------------------------------------- /map/如何比较两个 map 相等.md: -------------------------------------------------------------------------------- 1 | map 深度相等的条件: 2 | 3 | ```shell 4 | 1、都为 nil 5 | 2、非空、长度相等,指向同一个 map 实体对象 6 | 3、相应的 key 指向的 value “深度”相等 7 | ``` 8 | 9 | 直接将使用 map1 == map2 是错误的。这种写法只能比较 map 是否为 nil。 10 | 11 | ```golang 12 | package main 13 | 14 | import "fmt" 15 | 16 | func main() { 17 | var m map[string]int 18 | var n map[string]int 19 | 20 | fmt.Println(m == nil) 21 | fmt.Println(n == nil) 22 | 23 | // 不能通过编译 24 | //fmt.Println(m == n) 25 | } 26 | ``` 27 | 28 | 输出结果: 29 | 30 | ```golang 31 | true 32 | true 33 | ``` 34 | 35 | 因此只能是遍历map 的每个元素,比较元素是否都是深度相等。 36 | 37 | -------------------------------------------------------------------------------- /map/map 中的 key 为什么是无序的.md: -------------------------------------------------------------------------------- 1 | map 在扩容后,会发生 key 的搬迁,原来落在同一个 bucket 中的 key,搬迁后,有些 key 就要远走高飞了(bucket 序号加上了 2^B)。而遍历的过程,就是按顺序遍历 bucket,同时按顺序遍历 bucket 中的 key。搬迁后,key 的位置发生了重大的变化,有些 key 飞上高枝,有些 key 则原地不动。这样,遍历 map 的结果就不可能按原来的顺序了。 2 | 3 | 当然,如果我就一个 hard code 的 map,我也不会向 map 进行插入删除的操作,按理说每次遍历这样的 map 都会返回一个固定顺序的 key/value 序列吧。的确是这样,但是 Go 杜绝了这种做法,因为这样会给新手程序员带来误解,以为这是一定会发生的事情,在某些情况下,可能会酿成大错。 4 | 5 | 当然,Go 做得更绝,当我们在遍历 map 时,并不是固定地从 0 号 bucket 开始遍历,每次都是从一个随机值序号的 bucket 开始遍历,并且是从这个 bucket 的一个随机序号的 cell 开始遍历。这样,即使你是一个写死的 map,仅仅只是遍历它,也不太可能会返回一个固定序列的 key/value 对了。 6 | 7 | 多说一句,“迭代 map 的结果是无序的”这个特性是从 go 1.0 开始加入的。 -------------------------------------------------------------------------------- /GC/code/14/2/after/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | _ "net/http/pprof" 7 | "sync" 8 | ) 9 | 10 | var bufPool = sync.Pool{ 11 | New: func() interface{} { 12 | return make([]byte, 10<<20) 13 | }, 14 | } 15 | 16 | func main() { 17 | go func() { 18 | http.ListenAndServe("localhost:6060", nil) 19 | }() 20 | http.HandleFunc("/example2", func(w http.ResponseWriter, r *http.Request) { 21 | b := bufPool.Get().([]byte) 22 | for idx := range b { 23 | b[idx] = 0 24 | } 25 | fmt.Fprintf(w, "done, %v", r.URL.Path[1:]) 26 | bufPool.Put(b) 27 | }) 28 | http.ListenAndServe(":8080", nil) 29 | } 30 | -------------------------------------------------------------------------------- /channel/从一个关闭的 channel 仍然能读出数据吗.md: -------------------------------------------------------------------------------- 1 | 从一个有缓冲的 channel 里读数据,当 channel 被关闭,依然能读出有效值。只有当返回的 ok 为 false 时,读出的数据才是无效的。 2 | 3 | ```golang 4 | func main() { 5 | ch := make(chan int, 5) 6 | ch <- 18 7 | close(ch) 8 | x, ok := <-ch 9 | if ok { 10 | fmt.Println("received: ", x) 11 | } 12 | 13 | x, ok = <-ch 14 | if !ok { 15 | fmt.Println("channel closed, data invalid.") 16 | } 17 | } 18 | ``` 19 | 20 | 运行结果: 21 | 22 | ```golang 23 | received: 18 24 | channel closed, data invalid. 25 | ``` 26 | 27 | 先创建了一个有缓冲的 channel,向其发送一个元素,然后关闭此 channel。之后两次尝试从 channel 中读取数据,第一次仍然能正常读出值。第二次返回的 ok 为 false,说明 channel 已关闭,且通道里没有数据。 28 | 29 | 具体过程可以参考“从 channel 接收数据的过程是怎样的”一节。 30 | -------------------------------------------------------------------------------- /标准库/unsafe/如何实现字符串和byte切片的零拷贝转换.md: -------------------------------------------------------------------------------- 1 | 这是一个非常精典的例子。实现字符串和 bytes 切片之间的转换,要求是 `zero-copy`。想一下,一般的做法,都需要遍历字符串或 bytes 切片,再挨个赋值。 2 | 3 | 完成这个任务,我们需要了解 slice 和 string 的底层数据结构: 4 | 5 | ```golang 6 | type StringHeader struct { 7 | Data uintptr 8 | Len int 9 | } 10 | 11 | type SliceHeader struct { 12 | Data uintptr 13 | Len int 14 | Cap int 15 | } 16 | ``` 17 | 18 | 上面是反射包下的结构体,路径:src/reflect/value.go。只需要共享底层 Data 和 Len 就可以实现 `zero-copy`。 19 | 20 | ```golang 21 | func string2bytes(s string) []byte { 22 | return *(*[]byte)(unsafe.Pointer(&s)) 23 | } 24 | func bytes2string(b []byte) string{ 25 | return *(*string)(unsafe.Pointer(&b)) 26 | } 27 | ``` 28 | 29 | 原理上是利用指针的强转,代码比较简单,不作详细解释。 -------------------------------------------------------------------------------- /GC/code/7/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "runtime/trace" 6 | ) 7 | 8 | var cache = map[interface{}]interface{}{} 9 | 10 | func keepalloc() { 11 | for i := 0; i < 10000; i++ { 12 | m := make([]byte, 1<<10) 13 | cache[i] = m 14 | } 15 | } 16 | 17 | func keepalloc2() { 18 | for i := 0; i < 100000; i++ { 19 | go func() { 20 | select {} 21 | }() 22 | } 23 | } 24 | 25 | var ch = make(chan struct{}) 26 | 27 | func keepalloc3() { 28 | for i := 0; i < 100000; i++ { 29 | // 没有接收方,goroutine 会一直阻塞 30 | go func() { ch <- struct{}{} }() 31 | } 32 | } 33 | 34 | func main() { 35 | f, _ := os.Create("trace.out") 36 | defer f.Close() 37 | trace.Start(f) 38 | defer trace.Stop() 39 | keepalloc() 40 | keepalloc2() 41 | keepalloc3() 42 | } 43 | -------------------------------------------------------------------------------- /GC/code/20/4_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "sync" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func BenchmarkGCLargeGs(b *testing.B) { 12 | wg := sync.WaitGroup{} 13 | 14 | for ng := 100; ng <= 1000000; ng *= 10 { 15 | b.Run(fmt.Sprintf("#g-%d", ng), func(b *testing.B) { 16 | // Prepare loads of goroutines and wait 17 | // all goroutines terminate. 18 | wg.Add(ng) 19 | for i := 0; i < ng; i++ { 20 | go func() { 21 | time.Sleep(100 * time.Millisecond) 22 | wg.Done() 23 | }() 24 | } 25 | wg.Wait() 26 | 27 | // Run GC once for cleanup 28 | runtime.GC() 29 | 30 | // Now record GC scalability 31 | b.ResetTimer() 32 | for i := 0; i < b.N; i++ { 33 | runtime.GC() 34 | } 35 | }) 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /GC/code/11/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | "runtime/trace" 7 | "sync/atomic" 8 | ) 9 | 10 | var stop uint64 11 | 12 | // 通过对象 P 的释放状态,来确定 GC 是否已经完成 13 | func gcfinished() *int { 14 | p := 1 15 | runtime.SetFinalizer(&p, func(_ *int) { 16 | println("gc finished") 17 | atomic.StoreUint64(&stop, 1) // 通知停止分配 18 | }) 19 | return &p 20 | } 21 | 22 | func allocate() { 23 | // 每次调用分配 0.25MB 24 | _ = make([]byte, int((1<<20)*0.25)) 25 | } 26 | 27 | func main() { 28 | f, _ := os.Create("trace.out") 29 | defer f.Close() 30 | trace.Start(f) 31 | defer trace.Stop() 32 | 33 | gcfinished() 34 | 35 | // 当完成 GC 时停止分配 36 | for n := 1; atomic.LoadUint64(&stop) != 1; n++ { 37 | println("#allocate: ", n) 38 | allocate() 39 | } 40 | println("terminate") 41 | } 42 | -------------------------------------------------------------------------------- /interface/Go 接口与 C++ 接口有何异同.md: -------------------------------------------------------------------------------- 1 | 接口定义了一种规范,描述了类的行为和功能,而不做具体实现。 2 | 3 | C++ 的接口是使用抽象类来实现的,如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 "= 0" 来指定的。例如: 4 | 5 | ```C++ 6 | class Shape 7 | { 8 | public: 9 | // 纯虚函数 10 | virtual double getArea() = 0; 11 | private: 12 | string name; // 名称 13 | }; 14 | ``` 15 | 16 | 设计抽象类的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。 17 | 18 | 派生类需要明确地声明它继承自基类,并且需要实现基类中所有的纯虚函数。 19 | 20 | C++ 定义接口的方式称为“侵入式”,而 Go 采用的是 “非侵入式”,不需要显式声明,只需要实现接口定义的函数,编译器自动会识别。 21 | 22 | C++ 和 Go 在定义接口方式上的不同,也导致了底层实现上的不同。C++ 通过虚函数表来实现基类调用派生类的函数;而 Go 通过 `itab` 中的 `fun` 字段来实现接口变量调用实体类型的函数。C++ 中的虚函数表是在编译期生成的;而 Go 的 `itab` 中的 `fun` 字段是在运行期间动态生成的。原因在于,Go 中实体类型可能会无意中实现 N 多接口,很多接口并不是本来需要的,所以不能为类型实现的所有接口都生成一个 `itab`, 这也是“非侵入式”带来的影响;这在 C++ 中是不存在的,因为派生需要显示声明它继承自哪个基类。 23 | 24 | # 参考资料 25 | 【和 C++ 的对比】https://www.jianshu.com/p/b38b1719636e -------------------------------------------------------------------------------- /编译和链接/GoRoot 和 GoPath 有什么用.md: -------------------------------------------------------------------------------- 1 | GoRoot 是 Go 的安装路径。mac 或 unix 是在 `/usr/local/go` 路径上,来看下这里都装了些什么: 2 | 3 | ![/usr/local/go](https://user-images.githubusercontent.com/7698088/60344492-41178180-99e9-11e9-98b0-b1f8d64ce97d.png) 4 | 5 | bin 目录下面: 6 | 7 | ![bin](https://user-images.githubusercontent.com/7698088/60344698-b5522500-99e9-11e9-8883-a5bf2460fba0.png) 8 | 9 | pkg 目录下面: 10 | 11 | ![pkg](https://user-images.githubusercontent.com/7698088/60344731-c7cc5e80-99e9-11e9-8002-83f3debc09a6.png) 12 | 13 | Go 工具目录如下,其中比较重要的有编译器 `compile`,链接器 `link`: 14 | 15 | ![pkg/tool](https://user-images.githubusercontent.com/7698088/60379164-888d2480-9a60-11e9-9322-920c0e1b2b3d.png) 16 | 17 | GoPath 的作用在于提供一个可以寻找 `.go` 源码的路径,它是一个工作空间的概念,可以设置多个目录。Go 官方要求,GoPath 下面需要包含三个文件夹: 18 | 19 | ```shell 20 | src 21 | pkg 22 | bin 23 | ``` 24 | 25 | src 存放源文件,pkg 存放源文件编译后的库文件,后缀为 `.a`;bin 则存放可执行文件。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 项目介绍 2 | - 个人博客:https://qcrao.com 3 | 4 | - 电子书地址:https://qcrao91.gitbook.io/go 5 | 6 | - 本项目地址:https://github.com/qcrao/Go-Questions 7 | 8 | `Go 语言`学习入门和进阶知识。以 `Go 语言`为突破口,从问题切入,掌握 Go 语言、后端相关的各种硬核知识。希望本项目能在职场表现、项目实战上助你一臂之力! 9 | 10 | # 项目目录 11 | 详见 [wiki](https://github.com/qcrao/Go-Questions/wiki) 12 | 13 | # 深度博客收录 14 | 15 | 只收录有深度的博客,请享用! 16 | 17 | |博客名|简介|地址| 18 | |---|---|---| 19 | |码农桃花源|本项目作者博客园|https://www.cnblogs.com/qcrao-2018| 20 | |欧神开源书《Go 语言原本》|Golang committers|https://changkun.de/golang| 21 | |No Headback|滴滴技术大神曹春晖|http://xargin.com| 22 | |面向信仰编程|给 kubernetes 提交 pr 的大神|https://draveness.me| 23 | |煎鱼的迷之博客|知其然,知其所以然|https://github.com/EDDYCJY/blog| 24 | 25 | 26 | # 学习交流 27 | 你可以加我的微信一起交流:raoquancheng1991。 28 | 29 | 也可以关注公众号,和更多的人一起学习: 30 | 31 | ![QR](https://user-images.githubusercontent.com/7698088/57526048-ebb2e280-735e-11e9-98dc-4a2cb060d0df.png) 32 | -------------------------------------------------------------------------------- /GC/code/14/1/before/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | "runtime/trace" 8 | "sync/atomic" 9 | "time" 10 | ) 11 | 12 | var ( 13 | stop int32 14 | count int64 15 | sum time.Duration 16 | ) 17 | 18 | func concat() { 19 | for n := 0; n < 100; n++ { 20 | for i := 0; i < 8; i++ { 21 | go func() { 22 | s := "Go GC" 23 | s += " " + "Hello" 24 | s += " " + "World" 25 | _ = s 26 | }() 27 | } 28 | } 29 | } 30 | 31 | func main() { 32 | f, _ := os.Create("trace.out") 33 | defer f.Close() 34 | trace.Start(f) 35 | defer trace.Stop() 36 | 37 | go func() { 38 | var t time.Time 39 | for atomic.LoadInt32(&stop) == 0 { 40 | t = time.Now() 41 | runtime.GC() 42 | sum += time.Since(t) 43 | count++ 44 | } 45 | fmt.Printf("GC spend avg: %v\n", time.Duration(int64(sum)/count)) 46 | }() 47 | 48 | concat() 49 | atomic.StoreInt32(&stop, 1) 50 | } 51 | -------------------------------------------------------------------------------- /map/map 的删除过程是怎样的.md: -------------------------------------------------------------------------------- 1 | 写操作底层的执行函数是 `mapdelete`: 2 | 3 | ```golang 4 | func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) 5 | ``` 6 | 7 | 根据 key 类型的不同,删除操作会被优化成更具体的函数: 8 | 9 | |key 类型|删除| 10 | |---|---| 11 | |uint32|mapdelete_fast32(t *maptype, h *hmap, key uint32)| 12 | |uint64|mapdelete_fast64(t *maptype, h *hmap, key uint64)| 13 | |string|mapdelete_faststr(t *maptype, h *hmap, ky string)| 14 | 15 | 当然,我们只关心 `mapdelete` 函数。它首先会检查 h.flags 标志,如果发现写标位是 1,直接 panic,因为这表明有其他协程同时在进行写操作。 16 | 17 | 计算 key 的哈希,找到落入的 bucket。检查此 map 如果正在扩容的过程中,直接触发一次搬迁操作。 18 | 19 | 删除操作同样是两层循环,核心还是找到 key 的具体位置。寻找过程都是类似的,在 bucket 中挨个 cell 寻找。 20 | 21 | 找到对应位置后,对 key 或者 value 进行“清零”操作: 22 | 23 | ```golang 24 | // 对 key 清零 25 | if t.indirectkey { 26 | *(*unsafe.Pointer)(k) = nil 27 | } else { 28 | typedmemclr(t.key, k) 29 | } 30 | 31 | // 对 value 清零 32 | if t.indirectvalue { 33 | *(*unsafe.Pointer)(v) = nil 34 | } else { 35 | typedmemclr(t.elem, v) 36 | } 37 | ``` 38 | 39 | 最后,将 count 值减 1,将对应位置的 tophash 值置成 `Empty`。 40 | 41 | 这块源码同样比较简单,感兴起直接去看代码。 -------------------------------------------------------------------------------- /goroutine 调度器/goroutine和线程的区别.md: -------------------------------------------------------------------------------- 1 | 谈到 goroutine,绕不开的一个话题是:它和 thread 有什么区别? 2 | 3 | 参考资料【How Goroutines Work】告诉我们可以从三个角度区别:内存消耗、创建与销毀、切换。 4 | 5 | - 内存占用 6 | 7 | 创建一个 goroutine 的栈内存消耗为 2 KB,实际运行过程中,如果栈空间不够用,会自动进行扩容。创建一个 thread 则需要消耗 1 MB 栈内存,而且还需要一个被称为 “a guard page” 的区域用于和其他 thread 的栈空间进行隔离。 8 | 9 | 对于一个用 Go 构建的 HTTP Server 而言,对到来的每个请求,创建一个 goroutine 用来处理是非常轻松的一件事。而如果用一个使用线程作为并发原语的语言构建的服务,例如 Java 来说,每个请求对应一个线程则太浪费资源了,很快就会出 OOM 错误(OutOfMermoryError)。 10 | 11 | - 创建和销毀 12 | 13 | Thread 创建和销毀都会有巨大的消耗,因为要和操作系统打交道,是内核级的,通常解决的办法就是线程池。而 goroutine 因为是由 Go runtime 负责管理的,创建和销毁的消耗非常小,是用户级。 14 | 15 | - 切换 16 | 17 | 当 threads 切换时,需要保存各种寄存器,以便将来恢复: 18 | 19 | > 16 general purpose registers, PC (Program Counter), SP (Stack Pointer), segment registers, 16 XMM registers, FP coprocessor state, 16 AVX registers, all MSRs etc. 20 | 21 | 而 goroutines 切换只需保存三个寄存器:Program Counter, Stack Pointer and BP。 22 | 23 | 一般而言,线程切换会消耗 1000-1500 纳秒,一个纳秒平均可以执行 12-18 条指令。所以由于线程切换,执行指令的条数会减少 12000-18000。 24 | 25 | Goroutine 的切换约为 200 ns,相当于 2400-3600 条指令。 26 | 27 | 因此,goroutines 切换成本比 threads 要小得多。 -------------------------------------------------------------------------------- /GC/code/6/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | "runtime/debug" 8 | "runtime/trace" 9 | "time" 10 | ) 11 | 12 | func printGCStats() { 13 | t := time.NewTicker(time.Second) 14 | s := debug.GCStats{} 15 | for { 16 | select { 17 | case <-t.C: 18 | debug.ReadGCStats(&s) 19 | fmt.Printf("gc %d last@%v, PauseTotal %v\n", s.NumGC, s.LastGC, s.PauseTotal) 20 | } 21 | } 22 | } 23 | 24 | func printMemStats() { 25 | t := time.NewTicker(time.Second) 26 | s := runtime.MemStats{} 27 | 28 | for { 29 | select { 30 | case <-t.C: 31 | runtime.ReadMemStats(&s) 32 | fmt.Printf("gc %d last@%v, next_heap_size@%vMB\n", s.NumGC, time.Unix(int64(time.Duration(s.LastGC).Seconds()), 0), s.NextGC/(1<<20)) 33 | } 34 | } 35 | } 36 | 37 | func allocate() { 38 | _ = make([]byte, 1<<20) 39 | } 40 | 41 | func main() { 42 | // go printGCStats() 43 | // go printMemStats() 44 | 45 | f, _ := os.Create("trace.out") 46 | defer f.Close() 47 | trace.Start(f) 48 | defer trace.Stop() 49 | 50 | for n := 1; n < 100000; n++ { 51 | allocate() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /GC/code/14/1/after/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | "runtime/trace" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | ) 12 | 13 | var ( 14 | stop int32 15 | count int64 16 | sum time.Duration 17 | ) 18 | 19 | func concat() { 20 | wg := sync.WaitGroup{} 21 | for n := 0; n < 100; n++ { 22 | wg.Add(8) 23 | for i := 0; i < 8; i++ { 24 | go func() { 25 | s := make([]byte, 0, 20) 26 | s = append(s, "Go GC"...) 27 | s = append(s, ' ') 28 | s = append(s, "Hello"...) 29 | s = append(s, ' ') 30 | s = append(s, "World"...) 31 | _ = string(s) 32 | wg.Done() 33 | }() 34 | } 35 | wg.Wait() 36 | } 37 | } 38 | 39 | func main() { 40 | f, _ := os.Create("trace.out") 41 | defer f.Close() 42 | trace.Start(f) 43 | defer trace.Stop() 44 | 45 | go func() { 46 | var t time.Time 47 | for atomic.LoadInt32(&stop) == 0 { 48 | t = time.Now() 49 | runtime.GC() 50 | sum += time.Since(t) 51 | count++ 52 | } 53 | fmt.Printf("GC spend avg: %v\n", time.Duration(int64(sum)/count)) 54 | }() 55 | 56 | concat() 57 | atomic.StoreInt32(&stop, 1) 58 | } 59 | -------------------------------------------------------------------------------- /GC/code/6/gc.txt: -------------------------------------------------------------------------------- 1 | $ GODEBUG=gctrace=1 ./main 2 | 3 | gc 1 @0.000s 2%: 0.009+0.23+0.004 ms clock, 0.11+0.083/0.019/0.14+0.049 ms cpu, 4->6->2 MB, 5 MB goal, 12 P 4 | scvg: 8 KB released 5 | scvg: inuse: 3, idle: 60, sys: 63, released: 57, consumed: 6 (MB) 6 | gc 2 @0.001s 2%: 0.018+1.1+0.029 ms clock, 0.22+0.047/0.074/0.048+0.34 ms cpu, 4->7->3 MB, 5 MB goal, 12 P 7 | scvg: inuse: 3, idle: 60, sys: 63, released: 56, consumed: 7 (MB) 8 | gc 3 @0.003s 2%: 0.018+0.59+0.011 ms clock, 0.22+0.073/0.008/0.042+0.13 ms cpu, 5->6->1 MB, 6 MB goal, 12 P 9 | scvg: 8 KB released 10 | scvg: inuse: 2, idle: 61, sys: 63, released: 56, consumed: 7 (MB) 11 | gc 4 @0.003s 4%: 0.019+0.70+0.054 ms clock, 0.23+0.051/0.047/0.085+0.65 ms cpu, 4->6->2 MB, 5 MB goal, 12 P 12 | scvg: 8 KB released 13 | scvg: inuse: 3, idle: 60, sys: 63, released: 56, consumed: 7 (MB) 14 | scvg: 8 KB released 15 | scvg: inuse: 4, idle: 59, sys: 63, released: 56, consumed: 7 (MB) 16 | gc 5 @0.004s 12%: 0.021+0.26+0.49 ms clock, 0.26+0.046/0.037/0.11+5.8 ms cpu, 4->7->3 MB, 5 MB goal, 12 P 17 | scvg: inuse: 5, idle: 58, sys: 63, released: 56, consumed: 7 (MB) 18 | gc 6 @0.005s 12%: 0.020+0.17+0.004 ms clock, 0.25+0.080/0.070/0.053+0.051 ms cpu, 5->6->1 MB, 6 MB goal, 12 P 19 | scvg: 8 KB released 20 | scvg: inuse: 5, idle: 58, sys: 63, released: 56, consumed: 7 -------------------------------------------------------------------------------- /interface/编译器自动检测类型是否实现接口.md: -------------------------------------------------------------------------------- 1 | 经常看到一些开源库里会有一些类似下面这种奇怪的用法: 2 | 3 | ```golang 4 | var _ io.Writer = (*myWriter)(nil) 5 | ``` 6 | 7 | 这时候会有点懵,不知道作者想要干什么,实际上这就是此问题的答案。编译器会由此检查 `*myWriter` 类型是否实现了 `io.Writer` 接口。 8 | 9 | 来看一个例子: 10 | 11 | ```golang 12 | package main 13 | 14 | import "io" 15 | 16 | type myWriter struct { 17 | 18 | } 19 | 20 | /*func (w myWriter) Write(p []byte) (n int, err error) { 21 | return 22 | }*/ 23 | 24 | func main() { 25 | // 检查 *myWriter 类型是否实现了 io.Writer 接口 26 | var _ io.Writer = (*myWriter)(nil) 27 | 28 | // 检查 myWriter 类型是否实现了 io.Writer 接口 29 | var _ io.Writer = myWriter{} 30 | } 31 | ``` 32 | 33 | 注释掉为 myWriter 定义的 Write 函数后,运行程序: 34 | 35 | ```golang 36 | src/main.go:14:6: cannot use (*myWriter)(nil) (type *myWriter) as type io.Writer in assignment: 37 | *myWriter does not implement io.Writer (missing Write method) 38 | src/main.go:15:6: cannot use myWriter literal (type myWriter) as type io.Writer in assignment: 39 | myWriter does not implement io.Writer (missing Write method) 40 | ``` 41 | 42 | 报错信息:*myWriter/myWriter 未实现 io.Writer 接口,也就是未实现 Write 方法。 43 | 44 | 解除注释后,运行程序不报错。 45 | 46 | 实际上,上述赋值语句会发生隐式地类型转换,在转换的过程中,编译器会检测等号右边的类型是否实现了等号左边接口所规定的函数。 47 | 48 | 总结一下,可通过在代码中添加类似如下的代码,用来检测类型是否实现了接口: 49 | 50 | ```golang 51 | var _ io.Writer = (*myWriter)(nil) 52 | var _ io.Writer = myWriter{} 53 | ``` 54 | -------------------------------------------------------------------------------- /goroutine 调度器/什么是workstealing.md: -------------------------------------------------------------------------------- 1 | Go scheduler 的职责就是将所有处于 runnable 的 goroutines 均匀分布到在 P 上运行的 M。 2 | 3 | 当一个 P 发现自己的 LRQ 已经没有 G 时,会从其他 P “偷” 一些 G 来运行。看看这是什么精神!自己的工作做完了,为了全局的利益,主动为别人分担。这被称为 `Work-stealing`,Go 从 1.1 开始实现。 4 | 5 | Go scheduler 使用 M:N 模型,在任一时刻,M 个 goroutines(G) 要分配到 N 个内核线程(M),这些 M 跑在个数最多为 GOMAXPROCS 的逻辑处理器(P)上。每个 M 必须依附于一个 P,每个 P 在同一时刻只能运行一个 M。如果 P 上的 M 阻塞了,那它就需要其他的 M 来运行 P 的 LRQ 里的 goroutines。 6 | 7 | ![GPM relatioship](https://user-images.githubusercontent.com/7698088/62031928-02a8f880-b21b-11e9-96a9-96820452463e.png) 8 | 9 | 个人感觉,上面这张图比常见的那些用三角形表示 M,圆形表示 G,矩形表示 P 的那些图更生动形象。 10 | 11 | 实际上,Go scheduler 每一轮调度要做的工作就是找到处于 runnable 的 goroutines,并执行它。找的顺序如下: 12 | 13 | ```golang 14 | runtime.schedule() { 15 | // only 1/61 of the time, check the global runnable queue for a G. 16 | // if not found, check the local queue. 17 | // if not found, 18 | // try to steal from other Ps. 19 | // if not, check the global runnable queue. 20 | // if not found, poll network. 21 | } 22 | ``` 23 | 24 | 找到一个可执行的 goroutine 后,就会一直执行下去,直到被阻塞。 25 | 26 | 当 P2 上的一个 G 执行结束,它就会去 LRQ 获取下一个 G 来执行。如果 LRQ 已经空了,就是说本地可运行队列已经没有 G 需要执行,并且这时 GRQ 也没有 G 了。这时,P2 会随机选择一个 P(称为 P1),P2 会从 P1 的 LRQ “偷”过来一半的 G。 27 | 28 | ![Work Stealing](https://user-images.githubusercontent.com/7698088/62033338-4ea96c80-b21e-11e9-9167-98767c03d2d9.png) 29 | 30 | 这样做的好处是,有更多的 P 可以一起工作,加速执行完所有的 G。 -------------------------------------------------------------------------------- /channel/什么是 CSP.md: -------------------------------------------------------------------------------- 1 | > Do not communicate by sharing memory; instead, share memory by communicating. 2 | 3 | 不要通过共享内存来通信,而要通过通信来实现内存共享。 4 | 5 | 这就是 Go 的并发哲学,它依赖 CSP 模型,基于 channel 实现。 6 | 7 | CSP 经常被认为是 Go 在并发编程上成功的关键因素。CSP 全称是 “Communicating Sequential Processes”,这也是 Tony Hoare 在 1978 年发表在 ACM 的一篇论文。论文里指出一门编程语言应该重视 input 和 output 的原语,尤其是并发编程的代码。 8 | 9 | 在那篇文章发表的时代,人们正在研究模块化编程的思想,该不该用 goto 语句在当时是最激烈的议题。彼时,面向对象编程的思想正在崛起,几乎没什么人关心并发编程。 10 | 11 | 在文章中,CSP 也是一门自定义的编程语言,作者定义了输入输出语句,用于 processes 间的通信(communicatiton)。processes 被认为是需要输入驱动,并且产生输出,供其他 processes 消费,processes 可以是进程、线程、甚至是代码块。输入命令是:!,用来向 processes 写入;输出是:?,用来从 processes 读出。这篇文章要讲的 channel 正是借鉴了这一设计。 12 | 13 | Hoare 还提出了一个 -> 命令,如果 -> 左边的语句返回 false,那它右边的语句就不会执行。 14 | 15 | 通过这些输入输出命令,Hoare 证明了如果一门编程语言中把 processes 间的通信看得第一等重要,那么并发编程的问题就会变得简单。 16 | 17 | Go 是第一个将 CSP 的这些思想引入,并且发扬光大的语言。仅管内存同步访问控制(原文是 memory access synchronization)在某些情况下大有用处,Go 里也有相应的 sync 包支持,但是这在大型程序很容易出错。 18 | 19 | Go 一开始就把 CSP 的思想融入到语言的核心里,所以并发编程成为 Go 的一个独特的优势,而且很容易理解。 20 | 21 | 大多数的编程语言的并发编程模型是基于线程和内存同步访问控制,Go 的并发编程的模型则用 goroutine 和 channel 来替代。Goroutine 和线程类似,channel 和 mutex (用于内存同步访问控制)类似。 22 | 23 | Goroutine 解放了程序员,让我们更能贴近业务去思考问题。而不用考虑各种像线程库、线程开销、线程调度等等这些繁琐的底层问题,goroutine 天生替你解决好了。 24 | 25 | Channel 则天生就可以和其他 channel 组合。我们可以把收集各种子系统结果的 channel 输入到同一个 channel。Channel 还可以和 select, cancel, timeout 结合起来。而 mutex 就没有这些功能。 26 | 27 | Go 的并发原则非常优秀,目标就是简单:尽量使用 channel;把 goroutine 当作免费的资源,随便用。 28 | -------------------------------------------------------------------------------- /GC/code/20/1.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | "runtime/trace" 8 | "time" 9 | ) 10 | 11 | const ( 12 | windowSize = 200000 13 | msgCount = 1000000 14 | ) 15 | 16 | var ( 17 | best time.Duration = time.Second 18 | bestAt time.Time 19 | worst time.Duration 20 | worstAt time.Time 21 | 22 | start = time.Now() 23 | ) 24 | 25 | func main() { 26 | f, _ := os.Create("trace.out") 27 | defer f.Close() 28 | trace.Start(f) 29 | defer trace.Stop() 30 | 31 | for i := 0; i < 5; i++ { 32 | measure() 33 | worst = 0 34 | best = time.Second 35 | runtime.GC() 36 | } 37 | } 38 | 39 | func measure() { 40 | var c channel 41 | for i := 0; i < msgCount; i++ { 42 | c.sendMsg(i) 43 | } 44 | fmt.Printf("Best send delay %v at %v, worst send delay: %v at %v. Wall clock: %v \n", best, bestAt.Sub(start), worst, worstAt.Sub(start), time.Since(start)) 45 | } 46 | 47 | type channel [windowSize][]byte 48 | 49 | func (c *channel) sendMsg(id int) { 50 | start := time.Now() 51 | 52 | // 模拟发送 53 | (*c)[id%windowSize] = newMsg(id) 54 | 55 | end := time.Now() 56 | elapsed := end.Sub(start) 57 | if elapsed > worst { 58 | worst = elapsed 59 | worstAt = end 60 | } 61 | if elapsed < best { 62 | best = elapsed 63 | bestAt = end 64 | } 65 | } 66 | 67 | func newMsg(n int) []byte { 68 | m := make([]byte, 1024) 69 | for i := range m { 70 | m[i] = byte(n) 71 | } 72 | return m 73 | } 74 | -------------------------------------------------------------------------------- /标准库/unsafe/如何利用unsafe包修改私有成员.md: -------------------------------------------------------------------------------- 1 | 对于一个结构体,通过 offset 函数可以获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的。 2 | 3 | 这里有一个内存分配相关的事实:结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址。 4 | 5 | 我们来看一个例子: 6 | 7 | ```golang 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "unsafe" 13 | ) 14 | 15 | type Programmer struct { 16 | name string 17 | language string 18 | } 19 | 20 | func main() { 21 | p := Programmer{"stefno", "go"} 22 | fmt.Println(p) 23 | 24 | name := (*string)(unsafe.Pointer(&p)) 25 | *name = "qcrao" 26 | 27 | lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.language))) 28 | *lang = "Golang" 29 | 30 | fmt.Println(p) 31 | } 32 | ``` 33 | 34 | 运行代码,输出: 35 | 36 | ```shell 37 | {stefno go} 38 | {qcrao Golang} 39 | ``` 40 | 41 | name 是结构体的第一个成员,因此可以直接将 &p 解析成 *string。这一点,在前面获取 map 的 count 成员时,用的是同样的原理。 42 | 43 | 对于结构体的私有成员,现在有办法可以通过 unsafe.Pointer 改变它的值了。 44 | 45 | 我把 Programmer 结构体升级,多加一个字段: 46 | 47 | ```golang 48 | type Programmer struct { 49 | name string 50 | age int 51 | language string 52 | } 53 | ``` 54 | 55 | 并且放在其他包,这样在 main 函数中,它的三个字段都是私有成员变量,不能直接修改。但我通过 unsafe.Sizeof() 函数可以获取成员大小,进而计算出成员的地址,直接修改内存。 56 | 57 | ```golang 58 | func main() { 59 | p := Programmer{"stefno", 18, "go"} 60 | fmt.Println(p) 61 | 62 | lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof(int(0)) + unsafe.Sizeof(string("")))) 63 | *lang = "Golang" 64 | 65 | fmt.Println(p) 66 | } 67 | ``` 68 | 69 | 输出: 70 | 71 | ```shell 72 | {stefno 18 go} 73 | {stefno 18 Golang} 74 | ``` -------------------------------------------------------------------------------- /map/如何实现两种 get 操作.md: -------------------------------------------------------------------------------- 1 | Go 语言中读取 map 有两种语法:带 comma 和 不带 comma。当要查询的 key 不在 map 里,带 comma 的用法会返回一个 bool 型变量提示 key 是否在 map 中;而不带 comma 的语句则会返回一个 key 类型的零值。如果 key 是 int 型就会返回 0,如果 key 是 string 类型,就会返回空字符串。 2 | 3 | ```golang 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | ageMap := make(map[string]int) 10 | ageMap["qcrao"] = 18 11 | 12 | // 不带 comma 用法 13 | age1 := ageMap["stefno"] 14 | fmt.Println(age1) 15 | 16 | // 带 comma 用法 17 | age2, ok := ageMap["stefno"] 18 | fmt.Println(age2, ok) 19 | } 20 | ``` 21 | 22 | 运行结果: 23 | 24 | ```shell 25 | 0 26 | 0 false 27 | ``` 28 | 29 | 以前一直觉得好神奇,怎么实现的?这其实是编译器在背后做的工作:分析代码后,将两种语法对应到底层两个不同的函数。 30 | 31 | ```golang 32 | // src/runtime/hashmap.go 33 | func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer 34 | func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool) 35 | ``` 36 | 37 | 源码里,函数命名不拘小节,直接带上后缀 1,2,完全不理会《代码大全》里的那一套命名的做法。从上面两个函数的声明也可以看出差别了,`mapaccess2` 函数返回值多了一个 bool 型变量,两者的代码也是完全一样的,只是在返回值后面多加了一个 false 或者 true。 38 | 39 | 另外,根据 key 的不同类型,编译器还会将查找、插入、删除的函数用更具体的函数替换,以优化效率: 40 | 41 | |key 类型|查找| 42 | |---|---| 43 | |uint32|mapaccess1_fast32(t *maptype, h *hmap, key uint32) unsafe.Pointer| 44 | |uint32|mapaccess2_fast32(t *maptype, h *hmap, key uint32) (unsafe.Pointer, bool)| 45 | |uint64|mapaccess1_fast64(t *maptype, h *hmap, key uint64) unsafe.Pointer| 46 | |uint64|mapaccess2_fast64(t *maptype, h *hmap, key uint64) (unsafe.Pointer, bool)| 47 | |string|mapaccess1_faststr(t *maptype, h *hmap, ky string) unsafe.Pointer| 48 | |string|mapaccess2_faststr(t *maptype, h *hmap, ky string) (unsafe.Pointer, bool)| 49 | 50 | 这些函数的参数类型直接是具体的 uint32、unt64、string,在函数内部由于提前知晓了 key 的类型,所以内存布局是很清楚的,因此能节省很多操作,提高效率。 51 | 52 | 上面这些函数都是在文件 `src/runtime/hashmap_fast.go` 里。 -------------------------------------------------------------------------------- /goroutine 调度器/schedule 循环如何运转.md: -------------------------------------------------------------------------------- 1 | 上一节,我们讲完 main goroutine 以及普通 goroutine 的退出过程。main goroutine 退出后直接调用 exit(0) 使得整个进程退出,而普通 goroutine 退出后,则进行了一系列的调用,最终又切到 g0 栈,执行 schedule 函数。 2 | 3 | 从前面的文章我们知道,普通 goroutine(gp)就是在 schedule 函数中被选中,然后才有机会执行。而现在,gp 执行完之后,再次进入 schedule 函数,形成一个循环。这个循环太长了,我们有必要再重新梳理一下。 4 | 5 | ![调度循环](https://user-images.githubusercontent.com/7698088/64071400-660bc780-ccac-11e9-8816-1dc43d60bd80.png) 6 | 7 | 如图所示,rt0_go 负责 Go 程序启动的所有初始化,中间进行了很多初始化工作,调用 mstart 之前,已经切换到了 g0 栈,图中不同色块表示使用不同的栈空间。 8 | 9 | 接着调用 gogo 函数,完成从 g0 栈到用户 goroutine 栈的切换,包括 main goroutine 和普通 goroutine。 10 | 11 | 之后,执行 main 函数或者用户自定义的 goroutine 任务。 12 | 13 | 执行完成后,main goroutine 直接调用 eixt(0) 退出,普通 goroutine 则调用 goexit -> goexit1 -> mcall,完成普通 goroutine 退出后的清理工作,然后切换到 g0 栈,调用 goexit0 函数,将普通 goroutine 添加到缓存池中,再调用 schedule 函数进行新一轮的调度。 14 | 15 | ```shell 16 | schedule() -> execute() -> gogo() -> goroutine 任务 -> goexit() -> goexit1() -> mcall() -> goexit0() -> schedule() 17 | ``` 18 | 19 | > 可以看出,一轮调度从调用 schedule 函数开始,经过一系列过程再次调用 schedule 函数来进行新一轮的调度,从一轮调度到新一轮调度的过程称之为一个调度循环。 20 | 21 | > 这里说的调度循环是指某一个工作线程的调度循环,而同一个Go 程序中存在多个工作线程,每个工作线程都在进行着自己的调度循环。 22 | 23 | > 从前面的代码分析可以得知,上面调度循环中的每一个函数调用都没有返回,虽然 `goroutine 任务-> goexit() -> goexit1() -> mcall()` 是在 g2 的栈空间执行的,但剩下的函数都是在 g0 的栈空间执行的。 24 | 25 | > 那么问题就来了,在一个复杂的程序中,调度可能会进行无数次循环,也就是说会进行无数次没有返回的函数调用,大家都知道,每调用一次函数都会消耗一定的栈空间,而如果一直这样无返回的调用下去无论 g0 有多少栈空间终究是会耗尽的,那么这里是不是有问题?其实没有问题!关键点就在于,每次执行 mcall 切换到 g0 栈时都是切换到 g0.sched.sp 所指的固定位置,这之所以行得通,正是因为从 schedule 函数开始之后的一系列函数永远都不会返回,所以重用这些函数上一轮调度时所使用过的栈内存是没有问题的。 26 | 27 | 我再解释一下:栈空间在调用函数时会自动“增大”,而函数返回时,会自动“减小”,这里的增大和减小是指栈顶指针 SP 的变化。上述这些函数都没有返回,说明调用者不需要用到被调用者的返回值,有点像“尾递归”。 28 | 29 | 因为 g0 一直没有动过,所有它之前保存的 sp 还能继续使用。每一次调度循环都会覆盖上一次调度循环的栈数据,完美! 30 | 31 | # 参考资料 32 | 【阿波张 非 main goroutine 的退出及调度循环】https://mp.weixin.qq.com/s/XttP9q7-PO7VXhskaBzGqA -------------------------------------------------------------------------------- /channel/channel 发送和接收元素的本质是什么.md: -------------------------------------------------------------------------------- 1 | Channel 发送和接收元素的本质是什么? 2 | 3 | > All transfer of value on the go channels happens with the copy of value. 4 | 5 | 就是说 channel 的发送和接收操作本质上都是 “值的拷贝”,无论是从 sender goroutine 的栈到 chan buf,还是从 chan buf 到 receiver goroutine,或者是直接从 sender goroutine 到 receiver goroutine。 6 | 7 | 举一个例子: 8 | 9 | ``` 10 | type user struct { 11 | name string 12 | age int8 13 | } 14 | 15 | var u = user{name: "Ankur", age: 25} 16 | var g = &u 17 | 18 | func modifyUser(pu *user) { 19 | fmt.Println("modifyUser Received Vaule", pu) 20 | pu.name = "Anand" 21 | } 22 | 23 | func printUser(u <-chan *user) { 24 | time.Sleep(2 * time.Second) 25 | fmt.Println("printUser goRoutine called", <-u) 26 | } 27 | 28 | func main() { 29 | c := make(chan *user, 5) 30 | c <- g 31 | fmt.Println(g) 32 | // modify g 33 | g = &user{name: "Ankur Anand", age: 100} 34 | go printUser(c) 35 | go modifyUser(g) 36 | time.Sleep(5 * time.Second) 37 | fmt.Println(g) 38 | } 39 | ``` 40 | 41 | 运行结果: 42 | 43 | ```shell 44 | &{Ankur 25} 45 | modifyUser Received Vaule &{Ankur Anand 100} 46 | printUser goRoutine called &{Ankur 25} 47 | &{Anand 100} 48 | ``` 49 | 50 | 这里就是一个很好的 `share memory by communicating` 的例子。 51 | 52 | ![output](https://user-images.githubusercontent.com/7698088/61191276-16ad1000-a6db-11e9-9729-cdea7744f002.png) 53 | 54 | 一开始构造一个结构体 u,地址是 0x56420,图中地址上方就是它的内容。接着把 `&u` 赋值给指针 `g`,g 的地址是 0x565bb0,它的内容就是一个地址,指向 u。 55 | 56 | main 程序里,先把 g 发送到 c,根据 `copy value` 的本质,进入到 chan buf 里的就是 `0x56420`,它是指针 g 的值(不是它指向的内容),所以打印从 channel 接收到的元素时,它就是 `&{Ankur 25}`。因此,这里并不是将指针 g “发送” 到了 channel 里,只是拷贝它的值而已。 57 | 58 | 再强调一次: 59 | 60 | > Remember all transfer of value on the go channels happens with the copy of value. 61 | 62 | # 参考资料 63 | 【深入 channel 底层】https://codeburst.io/diving-deep-into-the-golang-channels-549fd4ed21a8 64 | -------------------------------------------------------------------------------- /goroutine 调度器/一个调度相关的陷阱.md: -------------------------------------------------------------------------------- 1 | 由于 Go 语言是协作式的调度,不会像线程那样,在时间片用完后,由 CPU 中断任务强行将其调度走。对于 Go 语言中运行时间过长的 goroutine,Go scheduler 有一个后台线程在持续监控,一旦发现 goroutine 运行超过 10 ms,会设置 goroutine 的“抢占标志位”,之后调度器会处理。但是设置标志位的时机只有在函数“序言”部分,对于没有函数调用的就没有办法了。 2 | 3 | > Golang implements a co-operative partially preemptive scheduler. 4 | 5 | 所以在某些极端情况下,会掉进一些陷阱。下面这个例子来自参考资料【scheduler 的陷阱】。 6 | 7 | ```golang 8 | func main() { 9 | var x int 10 | threads := runtime.GOMAXPROCS(0) 11 | for i := 0; i < threads; i++ { 12 | go func() { 13 | for { x++ } 14 | }() 15 | } 16 | time.Sleep(time.Second) 17 | fmt.Println("x =", x) 18 | } 19 | ``` 20 | 21 | 运行结果是:在死循环里出不来,不会输出最后的那条打印语句。 22 | 23 | 为什么?上面的例子会启动和机器的 CPU 核心数相等的 goroutine,每个 goroutine 都会执行一个无限循环。 24 | 25 | 创建完这些 goroutines 后,main 函数里执行一条 `time.Sleep(time.Second)` 语句。Go scheduler 看到这条语句后,简直高兴坏了,要来活了。这是调度的好时机啊,于是主 goroutine 被调度走。先前创建的 `threads` 个 goroutines,刚好“一个萝卜一个坑”,把 M 和 P 都占满了。 26 | 27 | 在这些 goroutine 内部,又没有调用一些诸如 `channel`,`time.sleep` 这些会引发调度器工作的事情。麻烦了,只能任由这些无限循环执行下去了。 28 | 29 | 解决的办法也有,把 threads 减小 1: 30 | 31 | ```golang 32 | func main() { 33 | var x int 34 | threads := runtime.GOMAXPROCS(0) - 1 35 | for i := 0; i < threads; i++ { 36 | go func() { 37 | for { x++ } 38 | }() 39 | } 40 | time.Sleep(time.Second) 41 | fmt.Println("x =", x) 42 | } 43 | ``` 44 | 45 | 运行结果: 46 | 47 | ```shell 48 | x = 0 49 | ``` 50 | 51 | 不难理解了吧,主 goroutine 休眠一秒后,被 go schduler 重新唤醒,调度到 M 上继续执行,打印一行语句后,退出。主 goroutine 退出后,其他所有的 goroutine 都必须跟着退出。所谓“覆巢之下 焉有完卵”,一损俱损。 52 | 53 | 至于为什么最后打印出的 x 为 0,之前的文章[《曹大谈内存重排》](https://qcrao.com/2019/06/17/cch-says-memory-reorder/)里有讲到过,这里不再深究了。 54 | 55 | 还有一种解决办法是在 for 循环里加一句: 56 | 57 | ```golang 58 | go func() { 59 | time.Sleep(time.Second) 60 | for { x++ } 61 | }() 62 | ``` 63 | 64 | 同样可以让 main goroutine 有机会调度执行。 -------------------------------------------------------------------------------- /标准库/unsafe/Go指针和unsafe.Pointer有什么区别.md: -------------------------------------------------------------------------------- 1 | Go 语言的作者之一 Ken Thompson 也是 C 语言的作者。所以,Go 可以看作 C 系语言,它的很多特性都和 C 类似,指针就是其中之一。 2 | 3 | 然而,Go 语言的指针相比 C 的指针有很多限制。这当然是为了安全考虑,要知道像 Java/Python 这些现代语言,生怕程序员出错,哪有什么指针(这里指的是显式的指针)?更别说像 C/C++ 还需要程序员自己清理“垃圾”。所以对于 Go 来说,有指针已经很不错了,仅管它有很多限制。 4 | 5 | 相比于 C 语言中指针的灵活,Go 的指针多了一些限制。但这也算是 Go 的成功之处:既可以享受指针带来的便利,又避免了指针的危险性。 6 | 7 | 限制一:`Go 的指针不能进行数学运算`。 8 | 9 | 来看一个简单的例子: 10 | 11 | ```golang 12 | a := 5 13 | p := &a 14 | 15 | p++ 16 | p = &a + 3 17 | ``` 18 | 19 | 上面的代码将不能通过编译,会报编译错误:`invalid operation`,也就是说不能对指针做数学运算。 20 | 21 | 限制二:`不同类型的指针不能相互转换`。 22 | 23 | 例如下面这个简短的例子: 24 | 25 | ```golang 26 | func main() { 27 | a := int(100) 28 | var f *float64 29 | 30 | f = &a 31 | } 32 | ``` 33 | 34 | 也会报编译错误: 35 | 36 | ```shell 37 | cannot use &a (type *int) as type *float64 in assignment 38 | ``` 39 | 40 | 限制三:`不同类型的指针不能使用 == 或 != 比较`。 41 | 42 | 只有在两个指针类型相同或者可以相互转换的情况下,才可以对两者进行比较。另外,指针可以通过 `==` 和 `!=` 直接和 `nil` 作比较。 43 | 44 | 限制四:`不同类型的指针变量不能相互赋值`。 45 | 46 | 这一点同限制三。 47 | 48 | unsafe.Pointer 在 unsafe 包: 49 | 50 | ```golang 51 | type ArbitraryType int 52 | 53 | type Pointer *ArbitraryType 54 | ``` 55 | 56 | 从命名来看,`Arbitrary` 是任意的意思,也就是说 Pointer 可以指向任意类型,实际上它类似于 C 语言里的 `void*`。 57 | 58 | unsafe 包提供了 2 点重要的能力: 59 | 60 | > 1. 任何类型的指针和 unsafe.Pointer 可以相互转换。 61 | > 2. uintptr 类型和 unsafe.Pointer 可以相互转换。 62 | 63 | ![type pointer uintptr](https://user-images.githubusercontent.com/7698088/58747453-1dbaee80-849e-11e9-8c75-2459f76792d2.png) 64 | 65 | pointer 不能直接进行数学运算,但可以把它转换成 uintptr,对 uintptr 类型进行数学运算,再转换成 pointer 类型。 66 | 67 | ```golang 68 | // uintptr 是一个整数类型,它足够大,可以存储 69 | type uintptr uintptr 70 | ``` 71 | 72 | 还有一点要注意的是,uintptr 并没有指针的语义,意思就是 uintptr 所指向的对象会被 gc 无情地回收。而 unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。 73 | 74 | unsafe 包中的几个函数都是在编译期间执行完毕,毕竟,编译器对内存分配这些操作“了然于胸”。在 `/usr/local/go/src/cmd/compile/internal/gc/unsafe.go` 路径下,可以看到编译期间 Go 对 unsafe 包中函数的处理。 -------------------------------------------------------------------------------- /标准库/unsafe/如何利用unsafe获取slice&map的长度.md: -------------------------------------------------------------------------------- 1 | # 获取 slice 长度 2 | 通过前面关于 slice 的[文章](https://mp.weixin.qq.com/s/MTZ0C9zYsNrb8wyIm2D8BA),我们知道了 slice header 的结构体定义: 3 | 4 | ```golang 5 | // runtime/slice.go 6 | type slice struct { 7 | array unsafe.Pointer // 元素指针 8 | len int // 长度 9 | cap int // 容量 10 | } 11 | ``` 12 | 13 | 调用 make 函数新建一个 slice,底层调用的是 makeslice 函数,返回的是 slice 结构体: 14 | 15 | ```golang 16 | func makeslice(et *_type, len, cap int) slice 17 | ``` 18 | 19 | 因此我们可以通过 unsafe.Pointer 和 uintptr 进行转换,得到 slice 的字段值。 20 | 21 | ```golang 22 | func main() { 23 | s := make([]int, 9, 20) 24 | var Len = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(8))) 25 | fmt.Println(Len, len(s)) // 9 9 26 | 27 | var Cap = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(16))) 28 | fmt.Println(Cap, cap(s)) // 20 20 29 | } 30 | ``` 31 | 32 | Len,cap 的转换流程如下: 33 | 34 | ```golang 35 | Len: &s => pointer => uintptr => pointer => *int => int 36 | Cap: &s => pointer => uintptr => pointer => *int => int 37 | ``` 38 | 39 | # 获取 map 长度 40 | 再来看一下上篇文章我们讲到的 map: 41 | 42 | ```golang 43 | type hmap struct { 44 | count int 45 | flags uint8 46 | B uint8 47 | noverflow uint16 48 | hash0 uint32 49 | 50 | buckets unsafe.Pointer 51 | oldbuckets unsafe.Pointer 52 | nevacuate uintptr 53 | 54 | extra *mapextra 55 | } 56 | ``` 57 | 58 | 和 slice 不同的是,makemap 函数返回的是 hmap 的指针,注意是指针: 59 | 60 | ```golang 61 | func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap 62 | ``` 63 | 64 | 我们依然能通过 unsafe.Pointer 和 uintptr 进行转换,得到 hamp 字段的值,只不过,现在 count 变成二级指针了: 65 | 66 | ```golang 67 | func main() { 68 | mp := make(map[string]int) 69 | mp["qcrao"] = 100 70 | mp["stefno"] = 18 71 | 72 | count := **(**int)(unsafe.Pointer(&mp)) 73 | fmt.Println(count, len(mp)) // 2 2 74 | } 75 | ``` 76 | 77 | count 的转换过程: 78 | 79 | ```golang 80 | &mp => pointer => **int => int 81 | ``` 82 | -------------------------------------------------------------------------------- /GC/code/20/4.txt: -------------------------------------------------------------------------------- 1 | goos: darwin 2 | goarch: amd64 3 | BenchmarkGCLargeGs 4 | BenchmarkGCLargeGs/#g-100 5 | BenchmarkGCLargeGs/#g-100-12 6670 181526 ns/op 6 | BenchmarkGCLargeGs/#g-100-12 6050 188918 ns/op 7 | BenchmarkGCLargeGs/#g-100-12 6156 195642 ns/op 8 | BenchmarkGCLargeGs/#g-100-12 6067 197367 ns/op 9 | BenchmarkGCLargeGs/#g-100-12 6164 194289 ns/op 10 | BenchmarkGCLargeGs/#g-1000 11 | BenchmarkGCLargeGs/#g-1000-12 3544 328971 ns/op 12 | BenchmarkGCLargeGs/#g-1000-12 3628 331038 ns/op 13 | BenchmarkGCLargeGs/#g-1000-12 3621 332077 ns/op 14 | BenchmarkGCLargeGs/#g-1000-12 3603 332605 ns/op 15 | BenchmarkGCLargeGs/#g-1000-12 3477 332496 ns/op 16 | BenchmarkGCLargeGs/#g-10000 17 | BenchmarkGCLargeGs/#g-10000-12 939 1211196 ns/op 18 | BenchmarkGCLargeGs/#g-10000-12 927 1228460 ns/op 19 | BenchmarkGCLargeGs/#g-10000-12 925 1211521 ns/op 20 | BenchmarkGCLargeGs/#g-10000-12 920 1227664 ns/op 21 | BenchmarkGCLargeGs/#g-10000-12 906 1232928 ns/op 22 | BenchmarkGCLargeGs/#g-100000 23 | BenchmarkGCLargeGs/#g-100000-12 100 11134261 ns/op 24 | BenchmarkGCLargeGs/#g-100000-12 108 10581869 ns/op 25 | BenchmarkGCLargeGs/#g-100000-12 110 11042436 ns/op 26 | BenchmarkGCLargeGs/#g-100000-12 100 10828657 ns/op 27 | BenchmarkGCLargeGs/#g-100000-12 100 10927736 ns/op 28 | BenchmarkGCLargeGs/#g-1000000 29 | BenchmarkGCLargeGs/#g-1000000-12 42 31126262 ns/op 30 | BenchmarkGCLargeGs/#g-1000000-12 33 32082860 ns/op 31 | BenchmarkGCLargeGs/#g-1000000-12 33 33453187 ns/op 32 | BenchmarkGCLargeGs/#g-1000000-12 34 32805587 ns/op 33 | BenchmarkGCLargeGs/#g-1000000-12 34 32812612 ns/op 34 | PASS 35 | ok _/Users/changkun/dev/Go-Questions/GC/code/20 55.959s 36 | -------------------------------------------------------------------------------- /数组和切片/切片作为函数参数.md: -------------------------------------------------------------------------------- 1 | 前面我们说到,slice 其实是一个结构体,包含了三个成员:len, cap, array。分别表示切片长度,容量,底层数据的地址。 2 | 3 | 当 slice 作为函数参数时,就是一个普通的结构体。其实很好理解:若直接传 slice,在调用者看来,实参 slice 并不会被函数中的操作改变;若传的是 slice 的指针,在调用者看来,是会被改变原 slice 的。 4 | 5 | 值的注意的是,不管传的是 slice 还是 slice 指针,如果改变了 slice 底层数组的数据,会反应到实参 slice 的底层数据。为什么能改变底层数组的数据?很好理解:底层数据在 slice 结构体里是一个指针,仅管 slice 结构体自身不会被改变,也就是说底层数据地址不会被改变。 但是通过指向底层数据的指针,可以改变切片的底层数据,没有问题。 6 | 7 | 通过 slice 的 array 字段就可以拿到数组的地址。在代码里,是直接通过类似 `s[i]=10` 这种操作改变 slice 底层数组元素值。 8 | 9 | 另外,值得注意的是,Go 语言的函数参数传递,只有值传递,没有引用传递。 10 | 11 | 来看一个代码片段: 12 | 13 | ```golang 14 | package main 15 | 16 | func main() { 17 | s := []int{1, 1, 1} 18 | f(s) 19 | fmt.Println(s) 20 | } 21 | 22 | func f(s []int) { 23 | // i只是一个副本,不能改变s中元素的值 24 | /*for _, i := range s { 25 | i++ 26 | } 27 | */ 28 | 29 | for i := range s { 30 | s[i] += 1 31 | } 32 | } 33 | ``` 34 | 35 | 运行一下,程序输出: 36 | 37 | ```shell 38 | [2 2 2] 39 | ``` 40 | 41 | 果真改变了原始 slice 的底层数据。这里传递的是一个 slice 的副本,在 `f` 函数中,`s` 只是 `main` 函数中 `s` 的一个拷贝。在`f` 函数内部,对 `s` 的作用并不会改变外层 `main` 函数的 `s`。 42 | 43 | 要想真的改变外层 `slice`,只有将返回的新的 slice 赋值到原始 slice,或者向函数传递一个指向 slice 的指针。我们再来看一个例子: 44 | 45 | ```golang 46 | package main 47 | 48 | import "fmt" 49 | 50 | func myAppend(s []int) []int { 51 | // 这里 s 虽然改变了,但并不会影响外层函数的 s 52 | s = append(s, 100) 53 | return s 54 | } 55 | 56 | func myAppendPtr(s *[]int) { 57 | // 会改变外层 s 本身 58 | *s = append(*s, 100) 59 | return 60 | } 61 | 62 | func main() { 63 | s := []int{1, 1, 1} 64 | newS := myAppend(s) 65 | 66 | fmt.Println(s) 67 | fmt.Println(newS) 68 | 69 | s = newS 70 | 71 | myAppendPtr(&s) 72 | fmt.Println(s) 73 | } 74 | ``` 75 | 76 | 运行结果: 77 | 78 | ```shell 79 | [1 1 1] 80 | [1 1 1 100] 81 | [1 1 1 100 100] 82 | ``` 83 | 84 | `myAppend` 函数里,虽然改变了 `s`,但它只是一个值传递,并不会影响外层的 `s`,因此第一行打印出来的结果仍然是 `[1 1 1]`。 85 | 86 | 而 `newS` 是一个新的 `slice`,它是基于 `s` 得到的。因此它打印的是追加了一个 `100` 之后的结果: `[1 1 1 100]`。 87 | 88 | 最后,将 `newS` 赋值给了 `s`,`s` 这时才真正变成了一个新的slice。之后,再给 `myAppendPtr` 函数传入一个 `s 指针`,这回它真的被改变了:`[1 1 1 100 100]`。 -------------------------------------------------------------------------------- /标准库/context/context.Value 的查找过程是怎样的.md: -------------------------------------------------------------------------------- 1 | ```golang 2 | type valueCtx struct { 3 | Context 4 | key, val interface{} 5 | } 6 | ``` 7 | 8 | 它实现了两个方法: 9 | 10 | ```golang 11 | func (c *valueCtx) String() string { 12 | return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val) 13 | } 14 | 15 | func (c *valueCtx) Value(key interface{}) interface{} { 16 | if c.key == key { 17 | return c.val 18 | } 19 | return c.Context.Value(key) 20 | } 21 | ``` 22 | 23 | 由于它直接将 Context 作为匿名字段,因此仅管它只实现了 2 个方法,其他方法继承自父 context。但它仍然是一个 Context,这是 Go 语言的一个特点。 24 | 25 | 创建 valueCtx 的函数: 26 | 27 | ```golang 28 | func WithValue(parent Context, key, val interface{}) Context { 29 | if key == nil { 30 | panic("nil key") 31 | } 32 | if !reflect.TypeOf(key).Comparable() { 33 | panic("key is not comparable") 34 | } 35 | return &valueCtx{parent, key, val} 36 | } 37 | ``` 38 | 39 | 对 key 的要求是可比较,因为之后需要通过 key 取出 context 中的值,可比较是必须的。 40 | 41 | 通过层层传递 context,最终形成这样一棵树: 42 | 43 | ![valueCtx](https://user-images.githubusercontent.com/7698088/59154893-5e72c300-8aaf-11e9-9b78-3c34b5e73a45.png) 44 | 45 | 和链表有点像,只是它的方向相反:Context 指向它的父节点,链表则指向下一个节点。通过 WithValue 函数,可以创建层层的 valueCtx,存储 goroutine 间可以共享的变量。 46 | 47 | 取值的过程,实际上是一个递归查找的过程: 48 | 49 | ```golang 50 | func (c *valueCtx) Value(key interface{}) interface{} { 51 | if c.key == key { 52 | return c.val 53 | } 54 | return c.Context.Value(key) 55 | } 56 | ``` 57 | 58 | 它会顺着链路一直往上找,比较当前节点的 key 59 | 是否是要找的 key,如果是,则直接返回 value。否则,一直顺着 context 往前,最终找到根节点(一般是 emptyCtx),直接返回一个 nil。所以用 Value 方法的时候要判断结果是否为 nil。 60 | 61 | 因为查找方向是往上走的,所以,父节点没法获取子节点存储的值,子节点却可以获取父节点的值。 62 | 63 | `WithValue` 创建 context 节点的过程实际上就是创建链表节点的过程。两个节点的 key 值是可以相等的,但它们是两个不同的 context 节点。查找的时候,会向上查找到最后一个挂载的 context 节点,也就是离得比较近的一个父节点 context。所以,整体上而言,用 `WithValue` 构造的其实是一个低效率的链表。 64 | 65 | 如果你接手过项目,肯定经历过这样的窘境:在一个处理过程中,有若干子函数、子协程。各种不同的地方会向 context 里塞入各种不同的 k-v 对,最后在某个地方使用。 66 | 67 | 你根本就不知道什么时候什么地方传了什么值?这些值会不会被“覆盖”(底层是两个不同的 context 节点,查找的时候,只会返回一个结果)?你肯定会崩溃的。 68 | 69 | 而这也是 `context.Value` 最受争议的地方。很多人建议尽量不要通过 context 传值。 -------------------------------------------------------------------------------- /map/map 的赋值过程是怎样的.md: -------------------------------------------------------------------------------- 1 | 通过汇编语言可以看到,向 map 中插入或者修改 key,最终调用的是 `mapassign` 函数。 2 | 3 | 实际上插入或修改 key 的语法是一样的,只不过前者操作的 key 在 map 中不存在,而后者操作的 key 存在 map 中。 4 | 5 | mapassign 有一个系列的函数,根据 key 类型的不同,编译器会将其优化为相应的“快速函数”。 6 | 7 | |key 类型|插入| 8 | |---|---| 9 | |uint32|mapassign_fast32(t *maptype, h *hmap, key uint32) unsafe.Pointer| 10 | |uint64|mapassign_fast64(t *maptype, h *hmap, key uint64) unsafe.Pointer| 11 | |string|mapassign_faststr(t *maptype, h *hmap, ky string) unsafe.Pointer| 12 | 13 | 我们只用研究最一般的赋值函数 `mapassign`。 14 | 15 | 整体来看,流程非常得简单:对 key 计算 hash 值,根据 hash 值按照之前的流程,找到要赋值的位置(可能是插入新 key,也可能是更新老 key),对相应位置进行赋值。 16 | 17 | 源码大体和之前讲的类似,核心还是一个双层循环,外层遍历 bucket 和它的 overflow bucket,内层遍历整个 bucket 的各个 cell。限于篇幅,这部分代码的注释我也不展示了,有兴趣的可以去看,保证理解了这篇文章内容后,能够看懂。 18 | 19 | 我这里会针对这个过程提几点重要的。 20 | 21 | 函数首先会检查 map 的标志位 flags。如果 flags 的写标志位此时被置 1 了,说明有其他协程在执行“写”操作,进而导致程序 panic。这也说明了 map 对协程是不安全的。 22 | 23 | 通过前文我们知道扩容是渐进式的,如果 map 处在扩容的过程中,那么当 key 定位到了某个 bucket 后,需要确保这个 bucket 对应的老 bucket 完成了迁移过程。即老 bucket 里的 key 都要迁移到新的 bucket 中来(分裂到 2 个新 bucket),才能在新的 bucket 中进行插入或者更新的操作。 24 | 25 | 上面说的操作是在函数靠前的位置进行的,只有进行完了这个搬迁操作后,我们才能放心地在新 bucket 里定位 key 要安置的地址,再进行之后的操作。 26 | 27 | 现在到了定位 key 应该放置的位置了,所谓找准自己的位置很重要。准备两个指针,一个(`inserti`)指向 key 的 hash 值在 tophash 数组所处的位置,另一个(`insertk`)指向 cell 的位置(也就是 key 最终放置的地址),当然,对应 value 的位置就很容易定位出来了。这三者实际上都是关联的,在 tophash 数组中的索引位置决定了 key 在整个 bucket 中的位置(共 8 个 key),而 value 的位置需要“跨过” 8 个 key 的长度。 28 | 29 | 在循环的过程中,inserti 和 insertk 分别指向第一个找到的空闲的 cell。如果之后在 map 没有找到 key 的存在,也就是说原来 map 中没有此 key,这意味着插入新 key。那最终 key 的安置地址就是第一次发现的“空位”(tophash 是 empty)。 30 | 31 | 如果这个 bucket 的 8 个 key 都已经放置满了,那在跳出循环后,发现 inserti 和 insertk 都是空,这时候需要在 bucket 后面挂上 overflow bucket。当然,也有可能是在 overflow bucket 后面再挂上一个 overflow bucket。这就说明,太多 key hash 到了此 bucket。 32 | 33 | 在正式安置 key 之前,还要检查 map 的状态,看它是否需要进行扩容。如果满足扩容的条件,就主动触发一次扩容操作。 34 | 35 | 这之后,整个之前的查找定位 key 的过程,还得再重新走一次。因为扩容之后,key 的分布都发生了变化。 36 | 37 | 最后,会更新 map 相关的值,如果是插入新 key,map 的元素数量字段 count 值会加 1;在函数之初设置的 `hashWriting` 写标志出会清零。 38 | 39 | 另外,有一个重要的点要说一下。前面说的找到 key 的位置,进行赋值操作,实际上并不准确。我们看 `mapassign` 函数的原型就知道,函数并没有传入 value 值,所以赋值操作是什么时候执行的呢? 40 | 41 | ```golang 42 | func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer 43 | ``` 44 | 45 | 答案还得从汇编语言中寻找。我直接揭晓答案,有兴趣可以私下去研究一下。`mapassign` 函数返回的指针就是指向的 key 所对应的 value 值位置,有了地址,就很好操作赋值了。 -------------------------------------------------------------------------------- /interface/如何用 interface 实现多态.md: -------------------------------------------------------------------------------- 1 | `Go` 语言并没有设计诸如虚函数、纯虚函数、继承、多重继承等概念,但它通过接口却非常优雅地支持了面向对象的特性。 2 | 3 | 多态是一种运行期的行为,它有以下几个特点: 4 | 5 | >1. 一种类型具有多种类型的能力 6 | >2. 允许不同的对象对同一消息做出灵活的反应 7 | >3. 以一种通用的方式对待个使用的对象 8 | >4. 非动态语言必须通过继承和接口的方式来实现 9 | 10 | 看一个实现了多态的代码例子: 11 | 12 | ```golang 13 | package main 14 | 15 | import "fmt" 16 | 17 | func main() { 18 | qcrao := Student{age: 18} 19 | whatJob(&qcrao) 20 | 21 | growUp(&qcrao) 22 | fmt.Println(qcrao) 23 | 24 | stefno := Programmer{age: 100} 25 | whatJob(stefno) 26 | 27 | growUp(stefno) 28 | fmt.Println(stefno) 29 | } 30 | 31 | func whatJob(p Person) { 32 | p.job() 33 | } 34 | 35 | func growUp(p Person) { 36 | p.growUp() 37 | } 38 | 39 | type Person interface { 40 | job() 41 | growUp() 42 | } 43 | 44 | type Student struct { 45 | age int 46 | } 47 | 48 | func (p Student) job() { 49 | fmt.Println("I am a student.") 50 | return 51 | } 52 | 53 | func (p *Student) growUp() { 54 | p.age += 1 55 | return 56 | } 57 | 58 | type Programmer struct { 59 | age int 60 | } 61 | 62 | func (p Programmer) job() { 63 | fmt.Println("I am a programmer.") 64 | return 65 | } 66 | 67 | func (p Programmer) growUp() { 68 | // 程序员老得太快 ^_^ 69 | p.age += 10 70 | return 71 | } 72 | ``` 73 | 74 | 代码里先定义了 1 个 `Person` 接口,包含两个函数: 75 | 76 | ```golang 77 | job() 78 | growUp() 79 | ``` 80 | 81 | 然后,又定义了 2 个结构体,`Student` 和 `Programmer`,同时,类型 `*Student`、`Programmer` 实现了 `Person` 接口定义的两个函数。注意,`*Student` 类型实现了接口, `Student` 类型却没有。 82 | 83 | 之后,我又定义了函数参数是 `Person` 接口的两个函数: 84 | 85 | ```golang 86 | func whatJob(p Person) 87 | func growUp(p Person) 88 | ``` 89 | 90 | `main` 函数里先生成 `Student` 和 `Programmer` 的对象,再将它们分别传入到函数 `whatJob` 和 `growUp`。函数中,直接调用接口函数,实际执行的时候是看最终传入的实体类型是什么,调用的是实体类型实现的函数。于是,不同对象针对同一消息就有多种表现,`多态`就实现了。 91 | 92 | 更深入一点来说的话,在函数 `whatJob()` 或者 `growUp()` 内部,接口 `person` 绑定了实体类型 `*Student` 或者 `Programmer`。根据前面分析的 `iface` 源码,这里会直接调用 `fun` 里保存的函数,类似于: `s.tab->fun[0]`,而因为 `fun` 数组里保存的是实体类型实现的函数,所以当函数传入不同的实体类型时,调用的实际上是不同的函数实现,从而实现多态。 93 | 94 | 运行一下代码: 95 | 96 | ```shell 97 | I am a student. 98 | {19} 99 | I am a programmer. 100 | {100} 101 | ``` 102 | 103 | # 参考资料 104 | 【各种面向对象的名词】https://cyent.github.io/golang/other/oo/ 105 | 106 | 【多态与鸭子类型】https://www.jb51.net/article/116025.htm -------------------------------------------------------------------------------- /interface/Go 语言与鸭子类型的关系.md: -------------------------------------------------------------------------------- 1 | 先直接来看维基百科里的定义: 2 | > If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck. 3 | 4 | 翻译过来就是:如果某个东西长得像鸭子,像鸭子一样游泳,像鸭子一样嘎嘎叫,那它就可以被看成是一只鸭子。 5 | 6 | `Duck Typing`,鸭子类型,是动态编程语言的一种对象推断策略,它更关注对象能如何被使用,而不是对象的类型本身。Go 语言作为一门静态语言,它通过通过接口的方式完美支持鸭子类型。 7 | 8 | 例如,在动态语言 python 中,定义一个这样的函数: 9 | 10 | ```python 11 | def hello_world(coder): 12 | coder.say_hello() 13 | ``` 14 | 15 | 当调用此函数的时候,可以传入任意类型,只要它实现了 `say_hello()` 函数就可以。如果没有实现,运行过程中会出现错误。 16 | 17 | 而在静态语言如 Java, C++ 中,必须要显示地声明实现了某个接口,之后,才能用在任何需要这个接口的地方。如果你在程序中调用 `hello_world` 函数,却传入了一个根本就没有实现 `say_hello()` 的类型,那在编译阶段就不会通过。这也是静态语言比动态语言更安全的原因。 18 | 19 | 动态语言和静态语言的差别在此就有所体现。静态语言在编译期间就能发现类型不匹配的错误,不像动态语言,必须要运行到那一行代码才会报错。插一句,这也是我不喜欢用 `python` 的一个原因。当然,静态语言要求程序员在编码阶段就要按照规定来编写程序,为每个变量规定数据类型,这在某种程度上,加大了工作量,也加长了代码量。动态语言则没有这些要求,可以让人更专注在业务上,代码也更短,写起来更快,这一点,写 python 的同学比较清楚。 20 | 21 | Go 语言作为一门现代静态语言,是有后发优势的。它引入了动态语言的便利,同时又会进行静态语言的类型检查,写起来是非常 Happy 的。Go 采用了折中的做法:不要求类型显示地声明实现了某个接口,只要实现了相关的方法即可,编译器就能检测到。 22 | 23 | 来看个例子: 24 | 25 | 先定义一个接口,和使用此接口作为参数的函数: 26 | 27 | ```golang 28 | type IGreeting interface { 29 | sayHello() 30 | } 31 | 32 | func sayHello(i IGreeting) { 33 | i.sayHello() 34 | } 35 | ``` 36 | 37 | 再来定义两个结构体: 38 | 39 | ```golang 40 | type Go struct {} 41 | func (g Go) sayHello() { 42 | fmt.Println("Hi, I am GO!") 43 | } 44 | 45 | type PHP struct {} 46 | func (p PHP) sayHello() { 47 | fmt.Println("Hi, I am PHP!") 48 | } 49 | ``` 50 | 51 | 最后,在 main 函数里调用 sayHello() 函数: 52 | 53 | ```golang 54 | func main() { 55 | golang := Go{} 56 | php := PHP{} 57 | 58 | sayHello(golang) 59 | sayHello(php) 60 | } 61 | ``` 62 | 63 | 程序输出: 64 | 65 | ```shell 66 | Hi, I am GO! 67 | Hi, I am PHP! 68 | ``` 69 | 70 | 在 main 函数中,调用调用 sayHello() 函数时,传入了 `golang, php` 对象,它们并没有显式地声明实现了 IGreeting 类型,只是实现了接口所规定的 sayHello() 函数。实际上,编译器在调用 sayHello() 函数时,会隐式地将 `golang, php` 对象转换成 IGreeting 类型,这也是静态语言的类型检查功能。 71 | 72 | 顺带再提一下动态语言的特点: 73 | > 变量绑定的类型是不确定的,在运行期间才能确定 74 | > 函数和方法可以接收任何类型的参数,且调用时不检查参数类型 75 | > 不需要实现接口 76 | 77 | 总结一下,鸭子类型是一种动态语言的风格,在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由它"当前方法和属性的集合"决定。Go 作为一种静态语言,通过接口实现了 `鸭子类型`,实际上是 Go 的编译器在其中作了隐匿的转换工作。 78 | 79 | # 参考资料 80 | 【wikipedia】https://en.wikipedia.org/wiki/Duck_test 81 | 82 | 【Golang 与鸭子类型,讲得比较好】https://blog.csdn.net/cszhouwei/article/details/33741731 83 | 84 | 【各种面向对象的名词】https://cyent.github.io/golang/other/oo/ 85 | 86 | 【多态、鸭子类型特性】https://www.jb51.net/article/116025.htm 87 | 88 | 【鸭子类型、动态静态语言】https://www.jianshu.com/p/650485b78d11 -------------------------------------------------------------------------------- /数组和切片/数组和切片有什么异同.md: -------------------------------------------------------------------------------- 1 | slice 的底层数据是数组,slice 是对数组的封装,它描述一个数组的片段。两者都可以通过下标来访问单个元素。 2 | 3 | 数组是定长的,长度定义好之后,不能再更改。在 Go 中,数组是不常见的,因为其长度是类型的一部分,限制了它的表达能力,比如 [3]int 和 [4]int 就是不同的类型。 4 | 5 | 而切片则非常灵活,它可以动态地扩容。切片的类型和长度无关。 6 | 7 | 数组就是一片连续的内存, slice 实际上是一个结构体,包含三个字段:长度、容量、底层数组。 8 | 9 | ```golang 10 | // runtime/slice.go 11 | type slice struct { 12 | array unsafe.Pointer // 元素指针 13 | len int // 长度 14 | cap int // 容量 15 | } 16 | ``` 17 | 18 | slice 的数据结构如下: 19 | 20 | ![切片数据结构](https://user-images.githubusercontent.com/7698088/55270142-876c2000-52d6-11e9-99e5-2e921fc2d430.png) 21 | 22 | 注意,底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。 23 | 24 | 【引申1】 25 | [3]int 和 [4]int 是同一个类型吗? 26 | 27 | 不是。因为数组的长度是类型的一部分,这是与 slice 不同的一点。 28 | 29 | 【引申2】 30 | 下面的代码输出是什么? 31 | 32 | 说明:例子来自雨痕大佬《Go学习笔记》第四版,P43页。这里我会进行扩展,并会作图详细分析。 33 | 34 | ```golang 35 | package main 36 | 37 | import "fmt" 38 | 39 | func main() { 40 | slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 41 | s1 := slice[2:5] 42 | s2 := s1[2:6:7] 43 | 44 | s2 = append(s2, 100) 45 | s2 = append(s2, 200) 46 | 47 | s1[2] = 20 48 | 49 | fmt.Println(s1) 50 | fmt.Println(s2) 51 | fmt.Println(slice) 52 | } 53 | ``` 54 | 55 | 结果: 56 | 57 | ```shell 58 | [2 3 20] 59 | [4 5 6 7 100 200] 60 | [0 1 2 3 20 5 6 7 100 9] 61 | ``` 62 | 63 | `s1` 从 `slice` 索引2(闭区间)到索引5(开区间,元素真正取到索引4),长度为3,容量默认到数组结尾,为8。 64 | `s2` 从 `s1` 的索引2(闭区间)到索引6(开区间,元素真正取到索引5),容量到索引7(开区间,真正到索引6),为5。 65 | 66 | ![slice origin](https://user-images.githubusercontent.com/7698088/54960948-c5490b80-4f99-11e9-8772-66d102caae8e.png) 67 | 68 | 接着,向 `s2` 尾部追加一个元素 100: 69 | 70 | ```golang 71 | s2 = append(s2, 100) 72 | ``` 73 | `s2` 容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 `s1` 都可以看得到。 74 | 75 | ![append 100](https://user-images.githubusercontent.com/7698088/54960896-8ca93200-4f99-11e9-86de-df4d85cca135.png) 76 | 77 | 再次向 `s2` 追加元素200: 78 | 79 | ```golang 80 | s2 = append(s2, 100) 81 | ``` 82 | 83 | 这时,`s2` 的容量不够用,该扩容了。于是,`s2` 另起炉灶,将原来的元素复制新的位置,扩大自己的容量。并且为了应对未来可能的 `append` 带来的再一次扩容,`s2` 会在此次扩容的时候多留一些 `buffer`,将新的容量将扩大为原始容量的2倍,也就是10了。 84 | 85 | ![append 200](https://user-images.githubusercontent.com/7698088/54961368-4654d280-4f9b-11e9-9b00-de96c6eedea9.png) 86 | 87 | 最后,修改 `s1` 索引为2位置的元素: 88 | 89 | ```golang 90 | s1[2] = 20 91 | ``` 92 | 93 | 这次只会影响原始数组相应位置的元素。它影响不到 `s2` 了,人家已经远走高飞了。 94 | 95 | ![s1[2]=20](https://user-images.githubusercontent.com/7698088/54961330-29200400-4f9b-11e9-88d0-a29308a818ae.png) 96 | 97 | 再提一点,打印 `s1` 的时候,只会打印出 `s1` 长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。 -------------------------------------------------------------------------------- /channel/关闭一个 channel 的过程是怎样的.md: -------------------------------------------------------------------------------- 1 | 关闭某个 channel,会执行函数 `closechan`: 2 | 3 | ```golang 4 | func closechan(c *hchan) { 5 | // 关闭一个 nil channel,panic 6 | if c == nil { 7 | panic(plainError("close of nil channel")) 8 | } 9 | 10 | // 上锁 11 | lock(&c.lock) 12 | // 如果 channel 已经关闭 13 | if c.closed != 0 { 14 | unlock(&c.lock) 15 | // panic 16 | panic(plainError("close of closed channel")) 17 | } 18 | 19 | // ………… 20 | 21 | // 修改关闭状态 22 | c.closed = 1 23 | 24 | var glist *g 25 | 26 | // 将 channel 所有等待接收队列的里 sudog 释放 27 | for { 28 | // 从接收队列里出队一个 sudog 29 | sg := c.recvq.dequeue() 30 | // 出队完毕,跳出循环 31 | if sg == nil { 32 | break 33 | } 34 | 35 | // 如果 elem 不为空,说明此 receiver 未忽略接收数据 36 | // 给它赋一个相应类型的零值 37 | if sg.elem != nil { 38 | typedmemclr(c.elemtype, sg.elem) 39 | sg.elem = nil 40 | } 41 | if sg.releasetime != 0 { 42 | sg.releasetime = cputicks() 43 | } 44 | // 取出 goroutine 45 | gp := sg.g 46 | gp.param = nil 47 | if raceenabled { 48 | raceacquireg(gp, unsafe.Pointer(c)) 49 | } 50 | // 相连,形成链表 51 | gp.schedlink.set(glist) 52 | glist = gp 53 | } 54 | 55 | // 将 channel 等待发送队列里的 sudog 释放 56 | // 如果存在,这些 goroutine 将会 panic 57 | for { 58 | // 从发送队列里出队一个 sudog 59 | sg := c.sendq.dequeue() 60 | if sg == nil { 61 | break 62 | } 63 | 64 | // 发送者会 panic 65 | sg.elem = nil 66 | if sg.releasetime != 0 { 67 | sg.releasetime = cputicks() 68 | } 69 | gp := sg.g 70 | gp.param = nil 71 | if raceenabled { 72 | raceacquireg(gp, unsafe.Pointer(c)) 73 | } 74 | // 形成链表 75 | gp.schedlink.set(glist) 76 | glist = gp 77 | } 78 | // 解锁 79 | unlock(&c.lock) 80 | 81 | // Ready all Gs now that we've dropped the channel lock. 82 | // 遍历链表 83 | for glist != nil { 84 | // 取最后一个 85 | gp := glist 86 | // 向前走一步,下一个唤醒的 g 87 | glist = glist.schedlink.ptr() 88 | gp.schedlink = 0 89 | // 唤醒相应 goroutine 90 | goready(gp, 3) 91 | } 92 | } 93 | ``` 94 | 95 | close 逻辑比较简单,对于一个 channel,recvq 和 sendq 中分别保存了阻塞的发送者和接收者。关闭 channel 后,对于等待接收者而言,会收到一个相应类型的零值。对于等待发送者,会直接 panic。所以,在不了解 channel 还有没有接收者的情况下,不能贸然关闭 channel。 96 | 97 | close 函数先上一把大锁,接着把所有挂在这个 channel 上的 sender 和 receiver 全都连成一个 sudog 链表,再解锁。最后,再将所有的 sudog 全都唤醒。 98 | 99 | 唤醒之后,该干嘛干嘛。sender 会继续执行 chansend 函数里 goparkunlock 函数之后的代码,很不幸,检测到 channel 已经关闭了,panic。receiver 则比较幸运,进行一些扫尾工作后,返回。这里,selected 返回 true,而返回值 received 则要根据 channel 是否关闭,返回不同的值。如果 channel 关闭,received 为 false,否则为 true。这我们分析的这种情况下,received 返回 false。 100 | -------------------------------------------------------------------------------- /interface/接口的动态类型和动态值.md: -------------------------------------------------------------------------------- 1 | 从源码里可以看到:`iface`包含两个字段:`tab` 是接口表指针,指向类型信息;`data` 是数据指针,则指向具体的数据。它们分别被称为`动态类型`和`动态值`。而接口值包括`动态类型`和`动态值`。 2 | 3 | 【引申1】接口类型和 `nil` 作比较 4 | 5 | 接口值的零值是指`动态类型`和`动态值`都为 `nil`。当仅且当这两部分的值都为 `nil` 的情况下,这个接口值就才会被认为 `接口值 == nil`。 6 | 7 | 来看个例子: 8 | 9 | ```golang 10 | package main 11 | 12 | import "fmt" 13 | 14 | type Coder interface { 15 | code() 16 | } 17 | 18 | type Gopher struct { 19 | name string 20 | } 21 | 22 | func (g Gopher) code() { 23 | fmt.Printf("%s is coding\n", g.name) 24 | } 25 | 26 | func main() { 27 | var c Coder 28 | fmt.Println(c == nil) 29 | fmt.Printf("c: %T, %v\n", c, c) 30 | 31 | var g *Gopher 32 | fmt.Println(g == nil) 33 | 34 | c = g 35 | fmt.Println(c == nil) 36 | fmt.Printf("c: %T, %v\n", c, c) 37 | } 38 | ``` 39 | 40 | 输出: 41 | 42 | ```shell 43 | true 44 | c: , 45 | true 46 | false 47 | c: *main.Gopher, 48 | ``` 49 | 50 | 一开始,`c` 的 动态类型和动态值都为 `nil`,`g` 也为 `nil`,当把 `g` 赋值给 `c` 后,`c` 的动态类型变成了 `*main.Gopher`,仅管 `c` 的动态值仍为 `nil`,但是当 `c` 和 `nil` 作比较的时候,结果就是 `false` 了。 51 | 52 | 【引申2】 53 | 来看一个例子,看一下它的输出: 54 | 55 | ```golang 56 | package main 57 | 58 | import "fmt" 59 | 60 | type MyError struct {} 61 | 62 | func (i MyError) Error() string { 63 | return "MyError" 64 | } 65 | 66 | func main() { 67 | err := Process() 68 | fmt.Println(err) 69 | 70 | fmt.Println(err == nil) 71 | } 72 | 73 | func Process() error { 74 | var err *MyError = nil 75 | return err 76 | } 77 | ``` 78 | 79 | 函数运行结果: 80 | 81 | ```shell 82 | 83 | false 84 | ``` 85 | 86 | 这里先定义了一个 `MyError` 结构体,实现了 `Error` 函数,也就实现了 `error` 接口。`Process` 函数返回了一个 `error` 接口,这块隐含了类型转换。所以,虽然它的值是 `nil`,其实它的类型是 `*MyError`,最后和 `nil` 比较的时候,结果为 `false`。 87 | 88 | 【引申3】如何打印出接口的动态类型和值? 89 | 90 | 直接看代码: 91 | 92 | ```golang 93 | package main 94 | 95 | import ( 96 | "unsafe" 97 | "fmt" 98 | ) 99 | 100 | type iface struct { 101 | itab, data uintptr 102 | } 103 | 104 | func main() { 105 | var a interface{} = nil 106 | 107 | var b interface{} = (*int)(nil) 108 | 109 | x := 5 110 | var c interface{} = (*int)(&x) 111 | 112 | ia := *(*iface)(unsafe.Pointer(&a)) 113 | ib := *(*iface)(unsafe.Pointer(&b)) 114 | ic := *(*iface)(unsafe.Pointer(&c)) 115 | 116 | fmt.Println(ia, ib, ic) 117 | 118 | fmt.Println(*(*int)(unsafe.Pointer(ic.data))) 119 | } 120 | ``` 121 | 122 | 代码里直接定义了一个 `iface` 结构体,用两个指针来描述 `itab` 和 `data`,之后将 a, b, c 在内存中的内容强制解释成我们自定义的 `iface`。最后就可以打印出动态类型和动态值的地址。 123 | 124 | 运行结果如下: 125 | 126 | ```shell 127 | {0 0} {17426912 0} {17426912 842350714568} 128 | 5 129 | ``` 130 | 131 | a 的动态类型和动态值的地址均为 0,也就是 nil;b 的动态类型和 c 的动态类型一致,都是 `*int`;最后,c 的动态值为 5。 132 | 133 | # 参考资料 134 | 【一个包含NIL指针的接口不是NIL接口】https://i6448038.github.io/2018/07/18/golang-mistakes/ -------------------------------------------------------------------------------- /channel/关于 channel 的 happened-before 有哪些.md: -------------------------------------------------------------------------------- 1 | 维基百科上给的定义: 2 | 3 | > In computer science, the happened-before relation (denoted: ->) is a relation between the result of two events, such that if one event should happen before another event, the result must reflect that, even if those events are in reality executed out of order (usually to optimize program flow). 4 | 5 | 简单来说就是如果事件 a 和事件 b 存在 happened-before 关系,即 a -> b,那么 a,b 完成后的结果一定要体现这种关系。由于现代编译器、CPU 会做各种优化,包括编译器重排、内存重排等等,在并发代码里,happened-before 限制就非常重要了。 6 | 7 | 根据晃岳攀老师在 Gopher China 2019 上的并发编程分享,关于 channel 的发送(send)、发送完成(send finished)、接收(receive)、接收完成(receive finished)的 happened-before 关系如下: 8 | 9 | 1. 第 n 个 `send` 一定 `happened before` 第 n 个 `receive finished`,无论是缓冲型还是非缓冲型的 channel。 10 | 2. 对于容量为 m 的缓冲型 channel,第 n 个 `receive` 一定 `happened before` 第 n+m 个 `send finished`。 11 | 3. 对于非缓冲型的 channel,第 n 个 `receive` 一定 `happened before` 第 n 个 `send finished`。 12 | 4. channel close 一定 `happened before` receiver 得到通知。 13 | 14 | 我们来逐条解释一下。 15 | 16 | 第一条,我们从源码的角度看也是对的,send 不一定是 `happened before` receive,因为有时候是先 receive,然后 goroutine 被挂起,之后被 sender 唤醒,send happened after receive。但不管怎样,要想完成接收,一定是要先有发送。 17 | 18 | 第二条,缓冲型的 channel,当第 n+m 个 send 发生后,有下面两种情况: 19 | 20 | 若第 n 个 receive 没发生。这时,channel 被填满了,send 就会被阻塞。那当第 n 个 receive 发生时,sender goroutine 会被唤醒,之后再继续发送过程。这样,第 n 个 `receive` 一定 `happened before` 第 n+m 个 `send finished`。 21 | 22 | 若第 n 个 receive 已经发生过了,这直接就符合了要求。 23 | 24 | 第三条,也是比较好理解的。第 n 个 send 如果被阻塞,sender goroutine 挂起,第 n 个 receive 这时到来,先于第 n 个 send finished。如果第 n 个 send 未被阻塞,说明第 n 个 receive 早就在那等着了,它不仅 happened before send finished,它还 happened before send。 25 | 26 | 第四条,回忆一下源码,先设置完 closed = 1,再唤醒等待的 receiver,并将零值拷贝给 receiver。 27 | 28 | 参考资料【鸟窝 并发编程分享】这篇博文的评论区有 PPT 的下载链接,这是晁老师在 Gopher 2019 大会上的演讲。 29 | 30 | 关于 happened before,这里再介绍一个柴大和曹大的新书《Go 语言高级编程》里面提到的一个例子。 31 | 32 | 书中 1.5 节先讲了顺序一致性的内存模型,这是并发编程的基础。 33 | 34 | 我们直接来看例子: 35 | 36 | ```golang 37 | var done = make(chan bool) 38 | var msg string 39 | 40 | func aGoroutine() { 41 | msg = "hello, world" 42 | done <- true 43 | } 44 | 45 | func main() { 46 | go aGoroutine() 47 | <-done 48 | println(msg) 49 | } 50 | ``` 51 | 52 | 先定义了一个 done channel 和一个待打印的字符串。在 main 函数里,启动一个 goroutine,等待从 done 里接收到一个值后,执行打印 msg 的操作。如果 main 函数中没有 `<-done` 这行代码,打印出来的 msg 为空,因为 aGoroutine 来不及被调度,还来不及给 msg 赋值,主程序就会退出。而在 Go 语言里,主协程退出时不会等待其他协程。 53 | 54 | 加了 `<-done` 这行代码后,就会阻塞在此。等 aGoroutine 里向 done 发送了一个值之后,才会被唤醒,继续执行打印 msg 的操作。而这在之前,msg 已经被赋值过了,所以会打印出 `hello, world`。 55 | 56 | 这里依赖的 happened before 就是前面讲的第一条。第一个 send 一定 happened before 第一个 receive finished,即 `done <- true` 先于 `<-done` 发生,这意味着 main 函数里执行完 `<-done` 后接着执行 `println(msg)` 这一行代码时,msg 已经被赋过值了,所以会打印出想要的结果。 57 | 58 | 进一步利用前面提到的第 3 条 happened before 规则,修改一下代码: 59 | 60 | ```golang 61 | var done = make(chan bool) 62 | var msg string 63 | 64 | func aGoroutine() { 65 | msg = "hello, world" 66 | <-done 67 | } 68 | 69 | func main() { 70 | go aGoroutine() 71 | done <- true 72 | println(msg) 73 | } 74 | ``` 75 | 76 | 同样可以得到相同的结果,为什么?根据第三条规则,对于非缓冲型的 channel,第一个 receive 一定 happened before 第一个 send finished。也就是说, 77 | 在 `done <- true` 完成之前,`<-done` 就已经发生了,也就意味着 msg 已经被赋上值了,最终也会打印出 `hello, world`。 78 | -------------------------------------------------------------------------------- /反射/如何比较两个对象完全相同.md: -------------------------------------------------------------------------------- 1 | Go 语言中提供了一个函数可以完成此项功能: 2 | 3 | ```golang 4 | func DeepEqual(x, y interface{}) bool 5 | ``` 6 | 7 | `DeepEqual` 函数的参数是两个 `interface`,实际上也就是可以输入任意类型,输出 true 或者 flase 表示输入的两个变量是否是“深度”相等。 8 | 9 | 先明白一点,如果是不同的类型,即使是底层类型相同,相应的值也相同,那么两者也不是“深度”相等。 10 | 11 | ```golang 12 | type MyInt int 13 | type YourInt int 14 | 15 | func main() { 16 | m := MyInt(1) 17 | y := YourInt(1) 18 | 19 | fmt.Println(reflect.DeepEqual(m, y)) // false 20 | } 21 | ``` 22 | 23 | 上面的代码中,m, y 底层都是 int,而且值都是 1,但是两者静态类型不同,前者是 `MyInt`,后者是 `YourInt`,因此两者不是“深度”相等。 24 | 25 | 在源码里,有对 DeepEqual 函数的非常清楚地注释,列举了不同类型,DeepEqual 的比较情形,这里做一个总结: 26 | 27 | |类型|深度相等情形| 28 | |---|---| 29 | |Array| 相同索引处的元素“深度”相等 | 30 | |Struct| 相应字段,包含导出和不导出,“深度”相等 | 31 | |Func| 只有两者都是 nil 时 | 32 | |Interface| 两者存储的具体值“深度”相等 | 33 | |Map|1、都为 nil;2、非空、长度相等,指向同一个 map 实体对象,或者相应的 key 指向的 value “深度”相等 | 34 | |Pointer|1、使用 == 比较的结果相等;2、指向的实体“深度”相等 | 35 | |Slice|1、都为 nil;2、非空、长度相等,首元素指向同一个底层数组的相同元素,即 &x[0] == &y[0] 或者 相同索引处的元素“深度”相等 | 36 | |numbers, bools, strings, and channels| 使用 == 比较的结果为真 | 37 | 38 | 一般情况下,DeepEqual 的实现只需要递归地调用 == 就可以比较两个变量是否是真的“深度”相等。 39 | 40 | 但是,有一些异常情况:比如 func 类型是不可比较的类型,只有在两个 func 类型都是 nil 的情况下,才是“深度”相等;float 类型,由于精度的原因,也是不能使用 == 比较的;包含 func 类型或者 float 类型的 struct, interface, array 等。 41 | 42 | 对于指针而言,当两个值相等的指针就是“深度”相等,因为两者指向的内容是相等的,即使两者指向的是 func 类型或者 float 类型,这种情况下不关心指针所指向的内容。 43 | 44 | 同样,对于指向相同 slice, map 的两个变量也是“深度”相等的,不关心 slice, map 具体的内容。 45 | 46 | 对于“有环”的类型,比如循环链表,比较两者是否“深度”相等的过程中,需要对已比较的内容作一个标记,一旦发现两个指针之前比较过,立即停止比较,并判定二者是深度相等的。这样做的原因是,及时停止比较,避免陷入无限循环。 47 | 48 | 来看源码: 49 | 50 | ```golang 51 | func DeepEqual(x, y interface{}) bool { 52 | if x == nil || y == nil { 53 | return x == y 54 | } 55 | v1 := ValueOf(x) 56 | v2 := ValueOf(y) 57 | if v1.Type() != v2.Type() { 58 | return false 59 | } 60 | return deepValueEqual(v1, v2, make(map[visit]bool), 0) 61 | } 62 | ``` 63 | 64 | 首先查看两者是否有一个是 nil 的情况,这种情况下,只有两者都是 nil,函数才会返回 true 65 | 66 | 接着,使用反射,获取x,y 的反射对象,并且立即比较两者的类型,根据前面的内容,这里实际上是动态类型,如果类型不同,直接返回 false。 67 | 68 | 最后,最核心的内容在子函数 `deepValueEqual` 中。 69 | 70 | 代码比较长,思路却比较简单清晰:核心是一个 switch 语句,识别输入参数的不同类型,分别递归调用 deepValueEqual 函数,一直递归到最基本的数据类型,比较 int,string 等可以直接得出 true 或者 false,再一层层地返回,最终得到“深度”相等的比较结果。 71 | 72 | 实际上,各种类型的比较套路比较相似,这里就直接节选一个稍微复杂一点的 `map` 类型的比较: 73 | 74 | ```golang 75 | // deepValueEqual 函数 76 | // …… 77 | 78 | case Map: 79 | if v1.IsNil() != v2.IsNil() { 80 | return false 81 | } 82 | if v1.Len() != v2.Len() { 83 | return false 84 | } 85 | if v1.Pointer() == v2.Pointer() { 86 | return true 87 | } 88 | for _, k := range v1.MapKeys() { 89 | val1 := v1.MapIndex(k) 90 | val2 := v2.MapIndex(k) 91 | if !val1.IsValid() || !val2.IsValid() || !deepValueEqual(v1.MapIndex(k), v2.MapIndex(k), visited, depth+1) { 92 | return false 93 | } 94 | } 95 | return true 96 | 97 | // …… 98 | ``` 99 | 100 | 和前文总结的表格里,比较 map 是否相等的思路比较一致,也不需要多说什么。说明一点,`visited` 是一个 map,记录递归过程中,比较过的“对”: 101 | 102 | ```golang 103 | type visit struct { 104 | a1 unsafe.Pointer 105 | a2 unsafe.Pointer 106 | typ Type 107 | } 108 | 109 | map[visit]bool 110 | ``` 111 | 112 | 比较过程中,一旦发现比较的“对”,已经在 map 里出现过的话,直接判定“深度”比较结果的是 `true`。 -------------------------------------------------------------------------------- /channel/channel 有哪些应用.md: -------------------------------------------------------------------------------- 1 | Channel 和 goroutine 的结合是 Go 并发编程的大杀器。而 Channel 的实际应用也经常让人眼前一亮,通过与 select,cancel,timer 等结合,它能实现各种各样的功能。接下来,我们就要梳理一下 channel 的应用。 2 | 3 | # 停止信号 4 | “如何优雅地关闭 channel”那一节已经讲得很多了,这块就略过了。 5 | 6 | channel 用于停止信号的场景还是挺多的,经常是关闭某个 channel 或者向 channel 发送一个元素,使得接收 channel 的那一方获知道此信息,进而做一些其他的操作。 7 | 8 | # 任务定时 9 | 10 | 与 timer 结合,一般有两种玩法:实现超时控制,实现定期执行某个任务。 11 | 12 | 有时候,需要执行某项操作,但又不想它耗费太长时间,上一个定时器就可以搞定: 13 | 14 | ```golang 15 | select { 16 | case <-time.After(100 * time.Millisecond): 17 | case <-s.stopc: 18 | return false 19 | } 20 | ``` 21 | 22 | 等待 100 ms 后,如果 s.stopc 还没有读出数据或者被关闭,就直接结束。这是来自 etcd 源码里的一个例子,这样的写法随处可见。 23 | 24 | 定时执行某个任务,也比较简单: 25 | 26 | ```golang 27 | func worker() { 28 | ticker := time.Tick(1 * time.Second) 29 | for { 30 | select { 31 | case <- ticker: 32 | // 执行定时任务 33 | fmt.Println("执行 1s 定时任务") 34 | } 35 | } 36 | } 37 | ``` 38 | 39 | 每隔 1 秒种,执行一次定时任务。 40 | 41 | # 解耦生产方和消费方 42 | 43 | 服务启动时,启动 n 个 worker,作为工作协程池,这些协程工作在一个 `for {}` 无限循环里,从某个 channel 消费工作任务并执行: 44 | 45 | ```golang 46 | func main() { 47 | taskCh := make(chan int, 100) 48 | go worker(taskCh) 49 | 50 | // 塞任务 51 | for i := 0; i < 10; i++ { 52 | taskCh <- i 53 | } 54 | 55 | // 等待 1 小时 56 | select { 57 | case <-time.After(time.Hour): 58 | } 59 | } 60 | 61 | func worker(taskCh <-chan int) { 62 | const N = 5 63 | // 启动 5 个工作协程 64 | for i := 0; i < N; i++ { 65 | go func(id int) { 66 | for { 67 | task := <- taskCh 68 | fmt.Printf("finish task: %d by worker %d\n", task, id) 69 | time.Sleep(time.Second) 70 | } 71 | }(i) 72 | } 73 | } 74 | ``` 75 | 76 | 5 个工作协程在不断地从工作队列里取任务,生产方只管往 channel 发送任务即可,解耦生产方和消费方。 77 | 78 | 程序输出: 79 | 80 | ```shell 81 | finish task: 1 by worker 4 82 | finish task: 2 by worker 2 83 | finish task: 4 by worker 3 84 | finish task: 3 by worker 1 85 | finish task: 0 by worker 0 86 | finish task: 6 by worker 0 87 | finish task: 8 by worker 3 88 | finish task: 9 by worker 1 89 | finish task: 7 by worker 4 90 | finish task: 5 by worker 2 91 | ``` 92 | 93 | # 控制并发数 94 | 95 | 有时需要定时执行几百个任务,例如每天定时按城市来执行一些离线计算的任务。但是并发数又不能太高,因为任务执行过程依赖第三方的一些资源,对请求的速率有限制。这时就可以通过 channel 来控制并发数。 96 | 97 | 下面的例子来自《Go 语言高级编程》: 98 | 99 | ```golang 100 | var limit = make(chan int, 3) 101 | 102 | func main() { 103 | // ………… 104 | for _, w := range work { 105 | go func() { 106 | limit <- 1 107 | w() 108 | <-limit 109 | }() 110 | } 111 | // ………… 112 | } 113 | ``` 114 | 115 | 构建一个缓冲型的 channel,容量为 3。接着遍历任务列表,每个任务启动一个 goroutine 去完成。真正执行任务,访问第三方的动作在 w() 中完成,在执行 w() 之前,先要从 limit 中拿“许可证”,拿到许可证之后,才能执行 w(),并且在执行完任务,要将“许可证”归还。这样就可以控制同时运行的 goroutine 数。 116 | 117 | 这里,`limit <- 1` 放在 func 内部而不是外部,原因是: 118 | 119 | > 如果在外层,就是控制系统 goroutine 的数量,可能会阻塞 for 循环,影响业务逻辑。 120 | 121 | >limit 其实和逻辑无关,只是性能调优,放在内层和外层的语义不太一样。 122 | 123 | 还有一点要注意的是,如果 w() 发生 panic,那“许可证”可能就还不回去了,因此需要使用 defer 来保证。 124 | 125 | # 参考资料 126 | 127 | 【channel 应用】https://www.s0nnet.com/archives/go-channels-practice 128 | 129 | 【应用举例】https://zhuyasen.com/post/go_queue.html 130 | 131 | 【应用】https://tonybai.com/2014/09/29/a-channel-compendium-for-golang/ 132 | 133 | 【Go 语言高级并发编程】https://chai2010.cn/advanced-go-programming-book/ 134 | -------------------------------------------------------------------------------- /编译和链接/Go 程序启动过程是怎样的.md: -------------------------------------------------------------------------------- 1 | 我们从一个 `Hello World` 的例子开始: 2 | 3 | ```golang 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | fmt.Println("hello world") 10 | } 11 | ``` 12 | 13 | 在项目根目录下执行: 14 | 15 | ```shell 16 | go build -gcflags "-N -l" -o hello src/main.go 17 | ``` 18 | 19 | `-gcflags "-N -l"` 是为了关闭编译器优化和函数内联,防止后面在设置断点的时候找不到相对应的代码位置。 20 | 21 | 得到了可执行文件 hello,执行: 22 | 23 | ```shell 24 | [qcrao@qcrao hello-world]$ gdb hello 25 | ``` 26 | 27 | 进入 gdb 调试模式,执行 `info files`,得到可执行文件的文件头,列出了各种段: 28 | 29 | ![gdb info](https://user-images.githubusercontent.com/7698088/60392813-db88d980-9b3d-11e9-8b0f-7c1d845a8191.png) 30 | 31 | 同时,我们也得到了入口地址:0x450e20。 32 | 33 | ```shell 34 | (gdb) b *0x450e20 35 | Breakpoint 1 at 0x450e20: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8. 36 | ``` 37 | 38 | 这就是 Go 程序的入口地址,我是在 linux 上运行的,所以入口文件为 `src/runtime/rt0_linux_amd64.s`,runtime 目录下有各种不同名称的程序入口文件,支持各种操作系统和架构,代码为: 39 | 40 | ```asm 41 | TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8 42 | LEAQ 8(SP), SI // argv 43 | MOVQ 0(SP), DI // argc 44 | MOVQ $main(SB), AX 45 | JMP AX 46 | ``` 47 | 48 | 主要是把 argc,argv 从内存拉到了寄存器。这里 LEAQ 是计算内存地址,然后把内存地址本身放进寄存器里,也就是把 argv 的地址放到了 SI 寄存器中。最后跳转到: 49 | 50 | ```golang 51 | TEXT main(SB),NOSPLIT,$-8 52 | MOVQ $runtime·rt0_go(SB), AX 53 | JMP AX 54 | ``` 55 | 56 | 继续跳转到 `runtime·rt0_go(SB)`,位置:`/usr/local/go/src/runtime/asm_amd64.s`,代码: 57 | 58 | ```ams 59 | TEXT runtime·rt0_go(SB),NOSPLIT,$0 60 | // 省略很多 CPU 相关的特性标志位检查的代码 61 | // 主要是看不懂,^_^ 62 | 63 | // ……………………………… 64 | 65 | // 下面是最后调用的一些函数,比较重要 66 | // 初始化执行文件的绝对路径 67 | CALL runtime·args(SB) 68 | // 初始化 CPU 个数和内存页大小 69 | CALL runtime·osinit(SB) 70 | // 初始化命令行参数、环境变量、gc、栈空间、内存管理、所有 P 实例、HASH算法等 71 | CALL runtime·schedinit(SB) 72 | 73 | // 要在 main goroutine 上运行的函数 74 | MOVQ $runtime·mainPC(SB), AX // entry 75 | PUSHQ AX 76 | PUSHQ $0 // arg size 77 | 78 | // 新建一个 goroutine,该 goroutine 绑定 runtime.main,放在 P 的本地队列,等待调度 79 | CALL runtime·newproc(SB) 80 | POPQ AX 81 | POPQ AX 82 | 83 | // 启动M,开始调度goroutine 84 | CALL runtime·mstart(SB) 85 | 86 | MOVL $0xf1, 0xf1 // crash 87 | RET 88 | 89 | 90 | DATA runtime·mainPC+0(SB)/8,$runtime·main(SB) 91 | GLOBL runtime·mainPC(SB),RODATA,$8 92 | ``` 93 | 94 | 参考文献里的一篇文章【探索 golang 程序启动过程】研究得比较深入,总结下: 95 | 96 | >1. 检查运行平台的CPU,设置好程序运行需要相关标志。 97 | 2. TLS的初始化。 98 | 3. runtime.args、runtime.osinit、runtime.schedinit 三个方法做好程序运行需要的各种变量与调度器。 99 | 4. runtime.newproc创建新的goroutine用于绑定用户写的main方法。 100 | 5. runtime.mstart开始goroutine的调度。 101 | 102 | 最后用一张图来总结 go bootstrap 过程吧: 103 | 104 | ![golang bootstrap](https://user-images.githubusercontent.com/7698088/60493589-b2a04a00-9cdf-11e9-9c9e-a4b275973f60.png) 105 | 106 | main 函数里执行的一些重要的操作包括:新建一个线程执行 sysmon 函数,定期垃圾回收和调度抢占;启动 gc;执行所有的 init 函数等等。 107 | 108 | 上面是启动过程,看一下退出过程: 109 | 110 | >当 main 函数执行结束之后,会执行 exit(0) 来退出进程。若执行 exit(0) 后,进程没有退出,main 函数最后的代码会一直访问非法地址: 111 | 112 | ```golang 113 | exit(0) 114 | for { 115 | var x *int32 116 | *x = 0 117 | } 118 | ``` 119 | 120 | >正常情况下,一旦出现非法地址访问,系统会把进程杀死,用这样的方法确保进程退出。 121 | 122 | 关于程序退出这一段的阐述来自群聊《golang runtime 阅读》,又是一个高阶的读源码的组织,github 主页见参考资料。 123 | 124 | 当然 Go 程序启动这一部分其实还会涉及到 fork 一个新进程、装载可执行文件,控制权转移等问题。还是推荐看前面的两本书,我觉得我不会写得更好,就不叙述了。 -------------------------------------------------------------------------------- /channel/channel 底层的数据结构是什么.md: -------------------------------------------------------------------------------- 1 | # 数据结构 2 | 底层数据结构需要看源码,版本为 go 1.9.2: 3 | 4 | ```golang 5 | type hchan struct { 6 | // chan 里元素数量 7 | qcount uint 8 | // chan 底层循环数组的长度 9 | dataqsiz uint 10 | // 指向底层循环数组的指针 11 | // 只针对有缓冲的 channel 12 | buf unsafe.Pointer 13 | // chan 中元素大小 14 | elemsize uint16 15 | // chan 是否被关闭的标志 16 | closed uint32 17 | // chan 中元素类型 18 | elemtype *_type // element type 19 | // 已发送元素在循环数组中的索引 20 | sendx uint // send index 21 | // 已接收元素在循环数组中的索引 22 | recvx uint // receive index 23 | // 等待接收的 goroutine 队列 24 | recvq waitq // list of recv waiters 25 | // 等待发送的 goroutine 队列 26 | sendq waitq // list of send waiters 27 | 28 | // 保护 hchan 中所有字段 29 | lock mutex 30 | } 31 | ``` 32 | 33 | 关于字段的含义都写在注释里了,再来重点说几个字段: 34 | 35 | `buf` 指向底层循环数组,只有缓冲型的 channel 才有。 36 | 37 | `sendx`,`recvx` 均指向底层循环数组,表示当前可以发送和接收的元素位置索引值(相对于底层数组)。 38 | 39 | `sendq`,`recvq` 分别表示被阻塞的 goroutine,这些 goroutine 由于尝试读取 channel 或向 channel 发送数据而被阻塞。 40 | 41 | `waitq` 是 `sudog` 的一个双向链表,而 `sudog` 实际上是对 goroutine 的一个封装: 42 | 43 | ```golang 44 | type waitq struct { 45 | first *sudog 46 | last *sudog 47 | } 48 | ``` 49 | 50 | `lock` 用来保证每个读 channel 或写 channel 的操作都是原子的。 51 | 52 | 例如,创建一个容量为 6 的,元素为 int 型的 channel 数据结构如下 : 53 | 54 | ![chan data structure](https://user-images.githubusercontent.com/7698088/61179068-806ee080-a62d-11e9-818c-16af42025b1b.png) 55 | 56 | # 创建 57 | 我们知道,通道有两个方向,发送和接收。理论上来说,我们可以创建一个只发送或只接收的通道,但是这种通道创建出来后,怎么使用呢?一个只能发的通道,怎么接收呢?同样,一个只能收的通道,如何向其发送数据呢? 58 | 59 | 一般而言,使用 `make` 创建一个能收能发的通道: 60 | 61 | ```golang 62 | // 无缓冲通道 63 | ch1 := make(chan int) 64 | // 有缓冲通道 65 | ch2 := make(chan int, 10) 66 | ``` 67 | 68 | 通过[汇编](https://mp.weixin.qq.com/s/obnnVkO2EiFnuXk_AIDHWw)分析,我们知道,最终创建 chan 的函数是 `makechan`: 69 | 70 | ```golang 71 | func makechan(t *chantype, size int64) *hchan 72 | ``` 73 | 74 | 从函数原型来看,创建的 chan 是一个指针。所以我们能在函数间直接传递 channel,而不用传递 channel 的指针。 75 | 76 | 具体来看下代码: 77 | 78 | ```golang 79 | const hchanSize = unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))&(maxAlign-1)) 80 | 81 | func makechan(t *chantype, size int64) *hchan { 82 | elem := t.elem 83 | 84 | // 省略了检查 channel size,align 的代码 85 | // …… 86 | 87 | var c *hchan 88 | // 如果元素类型不含指针 或者 size 大小为 0(无缓冲类型) 89 | // 只进行一次内存分配 90 | if elem.kind&kindNoPointers != 0 || size == 0 { 91 | // 如果 hchan 结构体中不含指针,GC 就不会扫描 chan 中的元素 92 | // 只分配 "hchan 结构体大小 + 元素大小*个数" 的内存 93 | c = (*hchan)(mallocgc(hchanSize+uintptr(size)*elem.size, nil, true)) 94 | // 如果是缓冲型 channel 且元素大小不等于 0(大小等于 0的元素类型:struct{}) 95 | if size > 0 && elem.size != 0 { 96 | c.buf = add(unsafe.Pointer(c), hchanSize) 97 | } else { 98 | // race detector uses this location for synchronization 99 | // Also prevents us from pointing beyond the allocation (see issue 9401). 100 | // 1. 非缓冲型的,buf 没用,直接指向 chan 起始地址处 101 | // 2. 缓冲型的,能进入到这里,说明元素无指针且元素类型为 struct{},也无影响 102 | // 因为只会用到接收和发送游标,不会真正拷贝东西到 c.buf 处(这会覆盖 chan的内容) 103 | c.buf = unsafe.Pointer(c) 104 | } 105 | } else { 106 | // 进行两次内存分配操作 107 | c = new(hchan) 108 | c.buf = newarray(elem, int(size)) 109 | } 110 | c.elemsize = uint16(elem.size) 111 | c.elemtype = elem 112 | // 循环数组长度 113 | c.dataqsiz = uint(size) 114 | 115 | // 返回 hchan 指针 116 | return c 117 | } 118 | ``` 119 | 120 | 新建一个 chan 后,内存在堆上分配,大概长这样: 121 | 122 | ![make chan](https://user-images.githubusercontent.com/7698088/61337268-4d179600-a867-11e9-98ac-f979e3da00a6.png) 123 | 124 | # 参考资料 125 | 126 | 【Kavya在Gopher Con 上关于 channel 的设计,非常好】https://speakerd.s3.amazonaws.com/presentations/10ac0b1d76a6463aa98ad6a9dec917a7/GopherCon_v10.0.pdf 127 | -------------------------------------------------------------------------------- /interface/iface 和 eface 的区别是什么.md: -------------------------------------------------------------------------------- 1 | `iface` 和 `eface` 都是 Go 中描述接口的底层结构体,区别在于 `iface` 描述的接口包含方法,而 `eface` 则是不包含任何方法的空接口:`interface{}`。 2 | 3 | 从源码层面看一下: 4 | 5 | ```golang 6 | type iface struct { 7 | tab *itab 8 | data unsafe.Pointer 9 | } 10 | 11 | type itab struct { 12 | inter *interfacetype 13 | _type *_type 14 | link *itab 15 | hash uint32 // copy of _type.hash. Used for type switches. 16 | bad bool // type does not implement interface 17 | inhash bool // has this itab been added to hash? 18 | unused [2]byte 19 | fun [1]uintptr // variable sized 20 | } 21 | ``` 22 | 23 | `iface` 内部维护两个指针,`tab` 指向一个 `itab` 实体, 它表示接口的类型以及赋给这个接口的实体类型。`data` 则指向接口具体的值,一般而言是一个指向堆内存的指针。 24 | 25 | 再来仔细看一下 `itab` 结构体:`_type` 字段描述了实体的类型,包括内存对齐方式,大小等;`inter` 字段则描述了接口的类型。`fun` 字段放置和接口方法对应的具体数据类型的方法地址,实现接口调用方法的动态分派,一般在每次给接口赋值发生转换时会更新此表,或者直接拿缓存的 itab。 26 | 27 | 这里只会列出实体类型和接口相关的方法,实体类型的其他方法并不会出现在这里。如果你学过 C++ 的话,这里可以类比虚函数的概念。 28 | 29 | 另外,你可能会觉得奇怪,为什么 `fun` 数组的大小为 1,要是接口定义了多个方法可怎么办?实际上,这里存储的是第一个方法的函数指针,如果有更多的方法,在它之后的内存空间里继续存储。从汇编角度来看,通过增加地址就能获取到这些函数指针,没什么影响。顺便提一句,这些方法是按照函数名称的字典序进行排列的。 30 | 31 | 再看一下 `interfacetype` 类型,它描述的是接口的类型: 32 | 33 | ```golang 34 | type interfacetype struct { 35 | typ _type 36 | pkgpath name 37 | mhdr []imethod 38 | } 39 | ``` 40 | 41 | 可以看到,它包装了 `_type` 类型,`_type` 实际上是描述 Go 语言中各种数据类型的结构体。我们注意到,这里还包含一个 `mhdr` 字段,表示接口所定义的函数列表, `pkgpath` 记录定义了接口的包名。 42 | 43 | 这里通过一张图来看下 `iface` 结构体的全貌: 44 | 45 | ![iface 结构体全景](https://user-images.githubusercontent.com/7698088/56564826-82527600-65e1-11e9-956d-d98a212bc863.png) 46 | 47 | 接着来看一下 `eface` 的源码: 48 | 49 | ```golang 50 | type eface struct { 51 | _type *_type 52 | data unsafe.Pointer 53 | } 54 | ``` 55 | 56 | 相比 `iface`,`eface` 就比较简单了。只维护了一个 `_type` 字段,表示空接口所承载的具体的实体类型。`data` 描述了具体的值。 57 | 58 | ![eface 结构体全景](https://user-images.githubusercontent.com/7698088/56565105-318f4d00-65e2-11e9-96bd-4b2e192791dc.png) 59 | 60 | 我们来看个例子: 61 | 62 | ```golang 63 | package main 64 | 65 | import "fmt" 66 | 67 | func main() { 68 | x := 200 69 | var any interface{} = x 70 | fmt.Println(any) 71 | 72 | g := Gopher{"Go"} 73 | var c coder = g 74 | fmt.Println(c) 75 | } 76 | 77 | type coder interface { 78 | code() 79 | debug() 80 | } 81 | 82 | type Gopher struct { 83 | language string 84 | } 85 | 86 | func (p Gopher) code() { 87 | fmt.Printf("I am coding %s language\n", p.language) 88 | } 89 | 90 | func (p Gopher) debug() { 91 | fmt.Printf("I am debuging %s language\n", p.language) 92 | } 93 | ``` 94 | 95 | 执行命令,打印出汇编语言: 96 | 97 | ```shell 98 | go tool compile -S ./src/main.go 99 | ``` 100 | 101 | 可以看到,main 函数里调用了两个函数: 102 | 103 | ```shell 104 | func convT2E64(t *_type, elem unsafe.Pointer) (e eface) 105 | func convT2I(tab *itab, elem unsafe.Pointer) (i iface) 106 | ``` 107 | 108 | 上面两个函数的参数和 `iface` 及 `eface` 结构体的字段是可以联系起来的:两个函数都是将参数`组装`一下,形成最终的接口。 109 | 110 | 作为补充,我们最后再来看下 `_type` 结构体: 111 | 112 | ```golang 113 | type _type struct { 114 | // 类型大小 115 | size uintptr 116 | ptrdata uintptr 117 | // 类型的 hash 值 118 | hash uint32 119 | // 类型的 flag,和反射相关 120 | tflag tflag 121 | // 内存对齐相关 122 | align uint8 123 | fieldalign uint8 124 | // 类型的编号,有bool, slice, struct 等等等等 125 | kind uint8 126 | alg *typeAlg 127 | // gc 相关 128 | gcdata *byte 129 | str nameOff 130 | ptrToThis typeOff 131 | } 132 | ``` 133 | 134 | Go 语言各种数据类型都是在 `_type` 字段的基础上,增加一些额外的字段来进行管理的: 135 | 136 | ```golang 137 | type arraytype struct { 138 | typ _type 139 | elem *_type 140 | slice *_type 141 | len uintptr 142 | } 143 | 144 | type chantype struct { 145 | typ _type 146 | elem *_type 147 | dir uintptr 148 | } 149 | 150 | type slicetype struct { 151 | typ _type 152 | elem *_type 153 | } 154 | 155 | type structtype struct { 156 | typ _type 157 | pkgPath name 158 | fields []structfield 159 | } 160 | ``` 161 | 162 | 这些数据类型的结构体定义,是反射实现的基础。 163 | 164 | # 参考资料 165 | 【有汇编分析,不错】http://legendtkl.com/2017/07/01/golang-interface-implement/ 166 | 167 | 【interface 源码解读 很不错 包含反射】http://wudaijun.com/2018/01/go-interface-implement/ -------------------------------------------------------------------------------- /interface/值接收者和指针接收者的区别.md: -------------------------------------------------------------------------------- 1 | # 方法 2 | 方法能给用户自定义的类型添加新的行为。它和函数的区别在于方法有一个接收者,给一个函数添加一个接收者,那么它就变成了方法。接收者可以是`值接收者`,也可以是`指针接收者`。 3 | 4 | 在调用方法的时候,值类型既可以调用`值接收者`的方法,也可以调用`指针接收者`的方法;指针类型既可以调用`指针接收者`的方法,也可以调用`值接收者`的方法。 5 | 6 | 也就是说,不管方法的接收者是什么类型,该类型的值和指针都可以调用,不必严格符合接收者的类型。 7 | 8 | 来看个例子: 9 | 10 | ```golang 11 | package main 12 | 13 | import "fmt" 14 | 15 | type Person struct { 16 | age int 17 | } 18 | 19 | func (p Person) howOld() int { 20 | return p.age 21 | } 22 | 23 | func (p *Person) growUp() { 24 | p.age += 1 25 | } 26 | 27 | func main() { 28 | // qcrao 是值类型 29 | qcrao := Person{age: 18} 30 | 31 | // 值类型 调用接收者也是值类型的方法 32 | fmt.Println(qcrao.howOld()) 33 | 34 | // 值类型 调用接收者是指针类型的方法 35 | qcrao.growUp() 36 | fmt.Println(qcrao.howOld()) 37 | 38 | // ---------------------- 39 | 40 | // stefno 是指针类型 41 | stefno := &Person{age: 100} 42 | 43 | // 指针类型 调用接收者是值类型的方法 44 | fmt.Println(stefno.howOld()) 45 | 46 | // 指针类型 调用接收者也是指针类型的方法 47 | stefno.growUp() 48 | fmt.Println(stefno.howOld()) 49 | } 50 | ``` 51 | 52 | 上例子的输出结果是: 53 | ```shell 54 | 18 55 | 19 56 | 100 57 | 101 58 | ``` 59 | 60 | 调用了 `growUp` 函数后,不管调用者是值类型还是指针类型,它的 `Age` 值都改变了。 61 | 62 | 实际上,当类型和方法的接收者类型不同时,其实是编译器在背后做了一些工作,用一个表格来呈现: 63 | 64 | |-|值接收者|指针接收者| 65 | |---|---|---| 66 | |值类型调用者|方法会使用调用者的一个副本,类似于“传值”|使用值的引用来调用方法,上例中,`qcrao.growUp()` 实际上是 `(&qcrao).growUp()`| 67 | |指针类型调用者|指针被解引用为值,上例中,`stefno.howOld()` 实际上是 `(*stefno).howOld()`|实际上也是“传值”,方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针| 68 | 69 | # 值接收者和指针接收者 70 | 前面说过,不管接收者类型是值类型还是指针类型,都可以通过值类型或指针类型调用,这里面实际上通过语法糖起作用的。 71 | 72 | 先说结论:实现了接收者是值类型的方法,相当于自动实现了接收者是指针类型的方法;而实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法。 73 | 74 | 来看一个例子,就会完全明白: 75 | 76 | ```golang 77 | package main 78 | 79 | import "fmt" 80 | 81 | type coder interface { 82 | code() 83 | debug() 84 | } 85 | 86 | type Gopher struct { 87 | language string 88 | } 89 | 90 | func (p Gopher) code() { 91 | fmt.Printf("I am coding %s language\n", p.language) 92 | } 93 | 94 | func (p *Gopher) debug() { 95 | fmt.Printf("I am debuging %s language\n", p.language) 96 | } 97 | 98 | func main() { 99 | var c coder = &Gopher{"Go"} 100 | c.code() 101 | c.debug() 102 | } 103 | ``` 104 | 105 | 上述代码里定义了一个接口 `coder`,接口定义了两个函数: 106 | 107 | ```golang 108 | code() 109 | debug() 110 | ``` 111 | 112 | 接着定义了一个结构体 `Gopher`,它实现了两个方法,一个值接收者,一个指针接收者。 113 | 114 | 最后,我们在 `main` 函数里通过接口类型的变量调用了定义的两个函数。 115 | 116 | 运行一下,结果: 117 | 118 | ```shell 119 | I am coding Go language 120 | I am debuging Go language 121 | ``` 122 | 123 | 但是如果我们把 `main` 函数的第一条语句换一下: 124 | 125 | ```golang 126 | func main() { 127 | var c coder = Gopher{"Go"} 128 | c.code() 129 | c.debug() 130 | } 131 | ``` 132 | 133 | 运行一下,报错: 134 | 135 | ```shell 136 | src/main.go:23:6: cannot use Gopher literal (type Gopher) as type coder in assignment: 137 | Gopher does not implement coder (debug method has pointer receiver) 138 | ``` 139 | 140 | 看出这两处代码的差别了吗?第一次是将 `&Gopher` 赋给了 `coder`;第二次则是将 `Gopher` 赋给了 `coder`。 141 | 142 | 第二次报错是说,`Gopher` 没有实现 `coder`。很明显了吧,因为 `Gopher` 类型并没有实现 `debug` 方法;表面上看, `*Gopher` 类型也没有实现 `code` 方法,但是因为 `Gopher` 类型实现了 `code` 方法,所以让 `*Gopher` 类型自动拥有了 `code` 方法。 143 | 144 | 当然,上面的说法有一个简单的解释:接收者是指针类型的方法,很可能在方法中会对接收者的属性进行更改操作,从而影响接收者;而对于接收者是值类型的方法,在方法中不会对接收者本身产生影响。 145 | 146 | 所以,当实现了一个接收者是值类型的方法,就可以自动生成一个接收者是对应指针类型的方法,因为两者都不会影响接收者。但是,当实现了一个接收者是指针类型的方法,如果此时自动生成一个接收者是值类型的方法,原本期望对接收者的改变(通过指针实现),现在无法实现,因为值类型会产生一个拷贝,不会真正影响调用者。 147 | 148 | 最后,只要记住下面这点就可以了: 149 | 150 | >如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。 151 | 152 | # 两者分别在何时使用 153 | 如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。 154 | 155 | 使用指针作为方法的接收者的理由: 156 | 157 | - 方法能够修改接收者指向的值。 158 | - 避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。 159 | 160 | 是使用值接收者还是指针接收者,不是由该方法是否修改了调用者(也就是接收者)来决定,而是应该基于该类型的`本质`。 161 | 162 | 如果类型具备“原始的本质”,也就是说它的成员都是由 Go 语言里内置的原始类型,如字符串,整型值等,那就定义值接收者类型的方法。像内置的引用类型,如 slice,map,interface,channel,这些类型比较特殊,声明他们的时候,实际上是创建了一个 `header`, 对于他们也是直接定义值接收者类型的方法。这样,调用函数时,是直接 copy 了这些类型的 `header`,而 `header` 本身就是为复制设计的。 163 | 164 | 如果类型具备非原始的本质,不能被安全地复制,这种类型总是应该被共享,那就定义指针接收者的方法。比如 go 源码里的文件结构体(struct File)就不应该被复制,应该只有一份`实体`。 165 | 166 | 这一段说的比较绕,大家可以去看《Go 语言实战》5.3 那一节。 167 | 168 | # 参考资料 169 | 【飞雪无情 Go实战笔记】https://www.flysnow.org/2017/04/03/go-in-action-go-interface.html 170 | 171 | 【何时使用指针接收者】http://ironxu.com/711 172 | 173 | 【理解Go Interface】http://lanlingzi.cn/post/technical/2016/0803_go_interface/ 174 | 175 | 【Go语言实战 类型的本置】 图书《Go In Action》 176 | -------------------------------------------------------------------------------- /goroutine 调度器/什么是 go shceduler.md: -------------------------------------------------------------------------------- 1 | # 什么是 sheduler 2 | Go 程序的执行由两层组成:Go Program,Runtime,即用户程序和运行时。它们之间通过函数调用来实现内存管理、channel 通信、goroutines 创建等功能。用户程序进行的系统调用都会被 Runtime 拦截,以此来帮助它进行调度以及垃圾回收相关的工作。 3 | 4 | 一个展现了全景式的关系如下图: 5 | 6 | ![runtime overall](https://user-images.githubusercontent.com/7698088/62172655-9981cc00-b365-11e9-8912-b16b83930ad0.png) 7 | 8 | # 为什么要 scheduler 9 | 10 | Go scheduler 可以说是 Go 运行时的一个最重要的部分了。Runtime 维护所有的 goroutines,并通过 scheduler 来进行调度。Goroutines 和 threads 是独立的,但是 goroutines 要依赖 threads 才能执行。 11 | 12 | Go 程序执行的高效和 scheduler 的调度是分不开的。 13 | 14 | # scheduler 底层原理 15 | 16 | 实际上在操作系统看来,所有的程序都是在执行多线程。将 goroutines 调度到线程上执行,仅仅是 runtime 层面的一个概念,在操作系统之上的层面。 17 | 18 | 有三个基础的结构体来实现 goroutines 的调度。g,m,p。 19 | 20 | `g` 代表一个 goroutine,它包含:表示 goroutine 栈的一些字段,指示当前 goroutine 的状态,指示当前运行到的指令地址,也就是 PC 值。 21 | 22 | `m` 表示内核线程,包含正在运行的 goroutine 等字段。 23 | 24 | `p` 代表一个虚拟的 Processor,它维护一个处于 Runnable 状态的 g 队列,`m` 需要获得 `p` 才能运行 `g`。 25 | 26 | 当然还有一个核心的结构体:`sched`,它总览全局。 27 | 28 | Runtime 起始时会启动一些 G:垃圾回收的 G,执行调度的 G,运行用户代码的 G;并且会创建一个 M 用来开始 G 的运行。随着时间的推移,更多的 G 会被创建出来,更多的 M 也会被创建出来。 29 | 30 | 当然,在 Go 的早期版本,并没有 p 这个结构体,`m` 必须从一个全局的队列里获取要运行的 `g`,因此需要获取一个全局的锁,当并发量大的时候,锁就成了瓶颈。后来在大神 Dmitry Vyokov 的实现里,加上了 `p` 结构体。每个 `p` 自己维护一个处于 Runnable 状态的 `g` 的队列,解决了原来的全局锁问题。 31 | 32 | Go scheduler 的目标: 33 | 34 | > For scheduling goroutines onto kernel threads. 35 | 36 | ![Go scheduler goals](https://user-images.githubusercontent.com/7698088/61874535-3f26dc80-af1b-11e9-9d9c-127edf90fff9.png) 37 | 38 | Go scheduler 的核心思想是: 39 | 40 | 1. reuse threads; 41 | 2. 限制同时运行(不包含阻塞)的线程数为 N,N 等于 CPU 的核心数目; 42 | 3. 线程私有的 runqueues,并且可以从其他线程 stealing goroutine 来运行,线程阻塞后,可以将 runqueues 传递给其他线程。 43 | 44 | 为什么需要 P 这个组件,直接把 runqueues 放到 M 不行吗? 45 | 46 | > You might wonder now, why have contexts at all? Can't we just put the runqueues on the threads and get rid of contexts? Not really. The reason we have contexts is so that we can hand them off to other threads if the running thread needs to block for some reason. 47 | 48 | > An example of when we need to block, is when we call into a syscall. Since a thread cannot both be executing code and be blocked on a syscall, we need to hand off the context so it can keep scheduling. 49 | 50 | 翻译一下,当一个线程阻塞的时候,将和它绑定的 P 上的 goroutines 转移到其他线程。 51 | 52 | Go scheduler 会启动一个后台线程 sysmon,用来检测长时间(超过 10 ms)运行的 goroutine,将其调度到 global runqueues。这是一个全局的 runqueue,优先级比较低,以示惩罚。 53 | 54 | ![Go scheduler limitations](https://user-images.githubusercontent.com/7698088/61874781-d55b0280-af1b-11e9-9965-da4efe53d2db.png) 55 | 56 | ## 总览 57 | 58 | 通常讲到 Go scheduler 都会提到 GPM 模型,我们来一个个地看。 59 | 60 | 下图是我使用的 mac 的硬件信息,只有 2 个核。 61 | 62 | ![mac 硬件信息](https://user-images.githubusercontent.com/7698088/62016049-63680f00-b1e2-11e9-9b6f-8566fd5e3963.png) 63 | 64 | 但是配上 CPU 的超线程,1 个核可以变成 2 个,所以当我在 mac 上运行下面的程序时,会打印出 4。 65 | 66 | ```golang 67 | func main() { 68 | // NumCPU 返回当前进程可以用到的逻辑核心数 69 | fmt.Println(runtime.NumCPU()) 70 | } 71 | ``` 72 | 73 | 因为 NumCPU 返回的是逻辑核心数,而非物理核心数,所以最终结果是 4。 74 | 75 | Go 程序启动后,会给每个逻辑核心分配一个 P(Logical Processor);同时,会给每个 P 分配一个 M(Machine,表示内核线程),这些内核线程仍然由 OS scheduler 来调度。 76 | 77 | 总结一下,当我在本地启动一个 Go 程序时,会得到 4 个系统线程去执行任务,每个线程会搭配一个 P。 78 | 79 | 在初始化时,Go 程序会有一个 G(initial Goroutine),执行指令的单位。G 会在 M 上得到执行,内核线程是在 CPU 核心上调度,而 G 则是在 M 上进行调度。 80 | 81 | G、P、M 都说完了,还有两个比较重要的组件没有提到: 全局可运行队列(GRQ)和本地可运行队列(LRQ)。 LRQ 存储本地(也就是具体的 P)的可运行 goroutine,GRQ 存储全局的可运行 goroutine,这些 goroutine 还没有分配到具体的 P。 82 | 83 | ![GPM global review](https://user-images.githubusercontent.com/7698088/62016513-336e3b00-b1e5-11e9-8923-d5d1743a531b.png) 84 | 85 | Go scheduler 是 Go runtime 的一部分,它内嵌在 Go 程序里,和 Go 程序一起运行。因此它运行在用户空间,在 kernel 的上一层。和 Os scheduler 抢占式调度(preemptive)不一样,Go scheduler 采用协作式调度(cooperating)。 86 | 87 | > Being a cooperating scheduler means the scheduler needs well-defined user space events that happen at safe points in the code to make scheduling decisions. 88 | 89 | 协作式调度一般会由用户设置调度点,例如 python 中的 yield 会告诉 Os scheduler 可以将我调度出去了。 90 | 91 | 但是由于在 Go 语言里,goroutine 调度的事情是由 Go runtime 来做,并非由用户控制,所以我们依然可以将 Go scheduler 看成是抢占式调度,因为用户无法预测调度器下一步的动作是什么。 92 | 93 | 和线程类似,goroutine 的状态也是三种(简化版的): 94 | 95 | |状态|解释| 96 | |---|---| 97 | |Waiting| 等待状态,goroutine 在等待某件事的发生。例如等待网络数据、硬盘;调用操作系统 API;等待内存同步访问条件 ready,如 atomic, mutexes | 98 | |Runnable| 就绪状态,只要给 M 我就可以运行| 99 | |Executing| 运行状态。goroutine 在 M 上执行指令,这是我们想要的 | 100 | 101 | 下面这张 GPM 全局的运行示意图见得比较多,可以留着,看完后面的系列文章之后再回头来看,还是很有感触的: 102 | 103 | ![goroutine workflow](https://user-images.githubusercontent.com/7698088/62260181-a7a61a00-b443-11e9-849b-b597addeca57.png) 104 | -------------------------------------------------------------------------------- /map/float 类型可以作为 map 的 key 吗.md: -------------------------------------------------------------------------------- 1 | 从语法上看,是可以的。Go 语言中只要是可比较的类型都可以作为 key。除开 slice,map,functions 这几种类型,其他类型都是 OK 的。具体包括:布尔值、数字、字符串、指针、通道、接口类型、结构体、只包含上述类型的数组。这些类型的共同特征是支持 `==` 和 `!=` 操作符,`k1 == k2` 时,可认为 k1 和 k2 是同一个 key。如果是结构体,只有 hash 后的值相等以及字面值相等,才被认为是相同的 key。很多字面值相等的,hash出来的值不一定相等,比如引用。 2 | 3 | 顺便说一句,任何类型都可以作为 value,包括 map 类型。 4 | 5 | 来看个例子: 6 | 7 | ```golang 8 | func main() { 9 | m := make(map[float64]int) 10 | m[1.4] = 1 11 | m[2.4] = 2 12 | m[math.NaN()] = 3 13 | m[math.NaN()] = 3 14 | 15 | for k, v := range m { 16 | fmt.Printf("[%v, %d] ", k, v) 17 | } 18 | 19 | fmt.Printf("\nk: %v, v: %d\n", math.NaN(), m[math.NaN()]) 20 | fmt.Printf("k: %v, v: %d\n", 2.400000000001, m[2.400000000001]) 21 | fmt.Printf("k: %v, v: %d\n", 2.4000000000000000000000001, m[2.4000000000000000000000001]) 22 | 23 | fmt.Println(math.NaN() == math.NaN()) 24 | } 25 | ``` 26 | 27 | 程序的输出: 28 | 29 | ```shell 30 | [2.4, 2] [NaN, 3] [NaN, 3] [1.4, 1] 31 | k: NaN, v: 0 32 | k: 2.400000000001, v: 0 33 | k: 2.4, v: 2 34 | false 35 | ``` 36 | 37 | 例子中定义了一个 key 类型是 float 型的 map,并向其中插入了 4 个 key:1.4, 2.4, NAN,NAN。 38 | 39 | 打印的时候也打印出了 4 个 key,如果你知道 NAN != NAN,也就不奇怪了。因为他们比较的结果不相等,自然,在 map 看来就是两个不同的 key 了。 40 | 41 | 接着,我们查询了几个 key,发现 NAN 不存在,2.400000000001 也不存在,而 2.4000000000000000000000001 却存在。 42 | 43 | 有点诡异,不是吗? 44 | 45 | 接着,我通过汇编发现了如下的事实: 46 | 47 | 当用 float64 作为 key 的时候,先要将其转成 unit64 类型,再插入 key 中。 48 | 49 | 具体是通过 `Float64frombits` 函数完成: 50 | 51 | ```golang 52 | // Float64frombits returns the floating point number corresponding 53 | // the IEEE 754 binary representation b. 54 | func Float64frombits(b uint64) float64 { return *(*float64)(unsafe.Pointer(&b)) } 55 | ``` 56 | 57 | 也就是将浮点数表示成 IEEE 754 规定的格式。如赋值语句: 58 | 59 | ```asm 60 | 0x00bd 00189 (test18.go:9) LEAQ "".statictmp_0(SB), DX 61 | 0x00c4 00196 (test18.go:9) MOVQ DX, 16(SP) 62 | 0x00c9 00201 (test18.go:9) PCDATA $0, $2 63 | 0x00c9 00201 (test18.go:9) CALL runtime.mapassign(SB) 64 | ``` 65 | 66 | `"".statictmp_0(SB)` 变量是这样的: 67 | 68 | ```asm 69 | "".statictmp_0 SRODATA size=8 70 | 0x0000 33 33 33 33 33 33 03 40 71 | "".statictmp_1 SRODATA size=8 72 | 0x0000 ff 3b 33 33 33 33 03 40 73 | "".statictmp_2 SRODATA size=8 74 | 0x0000 33 33 33 33 33 33 03 40 75 | ``` 76 | 77 | 我们再来输出点东西: 78 | 79 | ```golang 80 | package main 81 | 82 | import ( 83 | "fmt" 84 | "math" 85 | ) 86 | 87 | func main() { 88 | m := make(map[float64]int) 89 | m[2.4] = 2 90 | 91 | fmt.Println(math.Float64bits(2.4)) 92 | fmt.Println(math.Float64bits(2.400000000001)) 93 | fmt.Println(math.Float64bits(2.4000000000000000000000001)) 94 | } 95 | ``` 96 | 97 | ```shell 98 | 4612586738352862003 99 | 4612586738352864255 100 | 4612586738352862003 101 | ``` 102 | 103 | 转成十六进制为: 104 | 105 | ```shell 106 | 0x4003333333333333 107 | 0x4003333333333BFF 108 | 0x4003333333333333 109 | ``` 110 | 111 | 和前面的 `"".statictmp_0` 比较一下,很清晰了吧。`2.4` 和 `2.4000000000000000000000001` 经过 `math.Float64bits()` 函数转换后的结果是一样的。自然,二者在 map 看来,就是同一个 key 了。 112 | 113 | 再来看一下 NAN(not a number): 114 | 115 | ```golang 116 | // NaN returns an IEEE 754 ``not-a-number'' value. 117 | func NaN() float64 { return Float64frombits(uvnan) } 118 | ``` 119 | 120 | uvan 的定义为: 121 | 122 | ```golang 123 | uvnan = 0x7FF8000000000001 124 | ``` 125 | 126 | NAN() 直接调用 `Float64frombits`,传入写死的 const 型变量 `0x7FF8000000000001`,得到 NAN 型值。既然,NAN 是从一个常量解析得来的,为什么插入 map 时,会被认为是不同的 key? 127 | 128 | 这是由类型的哈希函数决定的,例如,对于 64 位的浮点数,它的哈希函数如下: 129 | 130 | ```golang 131 | func f64hash(p unsafe.Pointer, h uintptr) uintptr { 132 | f := *(*float64)(p) 133 | switch { 134 | case f == 0: 135 | return c1 * (c0 ^ h) // +0, -0 136 | case f != f: 137 | return c1 * (c0 ^ h ^ uintptr(fastrand())) // any kind of NaN 138 | default: 139 | return memhash(p, h, 8) 140 | } 141 | } 142 | ``` 143 | 144 | 第二个 case,`f != f` 就是针对 `NAN`,这里会再加一个随机数。 145 | 146 | 这样,所有的谜题都解开了。 147 | 148 | 由于 NAN 的特性: 149 | 150 | ```shell 151 | NAN != NAN 152 | hash(NAN) != hash(NAN) 153 | ``` 154 | 155 | 因此向 map 中查找的 key 为 NAN 时,什么也查不到;如果向其中增加了 4 次 NAN,遍历会得到 4 个 NAN。 156 | 157 | 最后说结论:float 型可以作为 key,但是由于精度的问题,会导致一些诡异的问题,慎用之。 158 | 159 | --- 160 | 161 | 关于当 key 是引用类型时,判断两个 key 是否相等,需要 hash 后的值相等并且 key 的字面量相等。由 @WuMingyu 补充的例子: 162 | 163 | ```go 164 | func TestT(t *testing.T) { 165 | type S struct { 166 | ID int 167 | } 168 | s1 := S{ID: 1} 169 | s2 := S{ID: 1} 170 | 171 | var h = map[*S]int {} 172 | h[&s1] = 1 173 | t.Log(h[&s1]) 174 | t.Log(h[&s2]) 175 | t.Log(s1 == s2) 176 | } 177 | ``` 178 | test output: 179 | ```shell 180 | === RUN TestT 181 | --- PASS: TestT (0.00s) 182 | endpoint_test.go:74: 1 183 | endpoint_test.go:75: 0 184 | endpoint_test.go:76: true 185 | PASS 186 | 187 | Process finished with exit code 0 188 | ``` 189 | -------------------------------------------------------------------------------- /编译和链接/逃逸分析是怎么进行的.md: -------------------------------------------------------------------------------- 1 | 在编译原理中,分析指针动态范围的方法称之为逃逸分析。通俗来讲,当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了逃逸。 2 | 3 | Go语言的逃逸分析是编译器执行静态代码分析后,对内存管理进行的优化和简化,它可以决定一个变量是分配到堆还栈上。 4 | 5 | 写过C/C++的同学都知道,调用著名的malloc和new函数可以在堆上分配一块内存,这块内存的使用和销毁的责任都在程序员。一不小心,就会发生内存泄露。 6 | 7 | Go语言里,基本不用担心内存泄露了。虽然也有new函数,但是使用new函数得到的内存不一定就在堆上。堆和栈的区别对程序员“模糊化”了,当然这一切都是Go编译器在背后帮我们完成的。 8 | 9 | Go语言逃逸分析最基本的原则是:如果一个函数返回对一个变量的引用,那么它就会发生逃逸。 10 | 11 | 简单来说,编译器会分析代码的特征和代码生命周期,Go中的变量只有在编译器可以证明在函数返回后不会再被引用的,才分配到栈上,其他情况下都是分配到堆上。 12 | 13 | Go语言里没有一个关键字或者函数可以直接让变量被编译器分配到堆上,相反,编译器通过分析代码来决定将变量分配到何处。 14 | 15 | 对一个变量取地址,可能会被分配到堆上。但是编译器进行逃逸分析后,如果考察到在函数返回后,此变量不会被引用,那么还是会被分配到栈上。 16 | 17 | 编译器会根据变量是否被外部引用来决定是否逃逸: 18 | 19 | > 1. 如果函数外部没有引用,则优先放到栈中; 20 | >2. 如果函数外部存在引用,则必定放到堆中; 21 | 22 | 写C/C++代码时,为了提高效率,常常将pass-by-value(传值)“升级”成pass-by-reference,企图避免构造函数的运行,并且直接返回一个指针。 23 | 24 | 你一定还记得,这里隐藏了一个很大的坑:在函数内部定义了一个局部变量,然后返回这个局部变量的地址(指针)。这些局部变量是在栈上分配的(静态内存分配),一旦函数执行完毕,变量占据的内存会被销毁,任何对这个返回值作的动作(如解引用),都将扰乱程序的运行,甚至导致程序直接崩溃。比如下面的这段代码: 25 | 26 | ```c 27 | int *foo ( void ) 28 | { 29 | int t = 3; 30 | return &t; 31 | } 32 | ``` 33 | 34 | 有些同学可能知道上面这个坑,用了个更聪明的做法:在函数内部使用new函数构造一个变量(动态内存分配),然后返回此变量的地址。因为变量是在堆上创建的,所以函数退出时不会被销毁。但是,这样就行了吗?new出来的对象该在何时何地delete呢?调用者可能会忘记delete或者直接拿返回值传给其他函数,之后就再也不能delete它了,也就是发生了内存泄露。关于这个坑,大家可以去看看《Effective C++》条款21,讲得非常好! 35 | 36 | C++是公认的语法最复杂的语言,据说没有人可以完全掌握C++的语法。而这一切在Go语言中就大不相同了。像上面示例的C++代码放到Go里,没有任何问题。 37 | 38 | 前面讲的C/C++中出现的问题,在Go中作为一个语言特性被大力推崇。真是C/C++之砒霜Go之蜜糖! 39 | 40 | C/C++中动态分配的内存需要我们手动释放,导致我们平时在写程序时,如履薄冰。这样做有他的好处:程序员可以完全掌控内存。但是缺点也是很多的:经常出现忘记释放内存,导致内存泄露。所以,很多现代语言都加上了垃圾回收机制。 41 | 42 | Go的垃圾回收,让堆和栈对程序员保持透明。真正解放了程序员的双手,让他们可以专注于业务,“高效”地完成代码编写。把那些内存管理的复杂机制交给编译器,而程序员可以去享受生活。 43 | 44 | 逃逸分析这种“骚操作”把变量合理地分配到它该去的地方。即使你是用new申请到的内存,如果我发现你竟然在退出函数后没有用了,那么就把你丢到栈上,毕竟栈上的内存分配比堆上快很多;反之,即使你表面上只是一个普通的变量,但是经过逃逸分析后发现在退出函数之后还有其他地方在引用,那我就把你分配到堆上。 45 | 46 | 如果变量都分配到堆上,堆不像栈可以自动清理。它会引起Go频繁地进行垃圾回收,而垃圾回收会占用比较大的系统开销(占用CPU容量的25%)。 47 | 48 | 堆和栈相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。栈内存分配则会非常快。栈分配内存只需要两个CPU指令:“PUSH”和“RELEASE”,分配和释放;而堆分配内存首先需要去找到一块大小合适的内存块,之后要通过垃圾回收才能释放。 49 | 50 | 通过逃逸分析,可以尽量把那些不需要分配到堆上的变量直接分配到栈上,堆上的变量少了,会减轻分配堆内存的开销,同时也会减少gc的压力,提高程序的运行速度。 51 | 52 | 引申1:如何查看某个变量是否发生了逃逸? 53 | 两种方法:使用go命令,查看逃逸分析结果;反汇编源码; 54 | 55 | 比如用这个例子: 56 | 57 | ```golang 58 | package main 59 | 60 | import "fmt" 61 | 62 | func foo() *int { 63 | t := 3 64 | return &t; 65 | } 66 | 67 | func main() { 68 | x := foo() 69 | fmt.Println(*x) 70 | } 71 | ``` 72 | 73 | 使用go命令: 74 | ```shell 75 | go build -gcflags '-m -l' main.go 76 | ``` 77 | 78 | 加`-l`是为了不让foo函数被内联。得到如下输出: 79 | 80 | ```shell 81 | # command-line-arguments 82 | src/main.go:7:9: &t escapes to heap 83 | src/main.go:6:7: moved to heap: t 84 | src/main.go:12:14: *x escapes to heap 85 | src/main.go:12:13: main ... argument does not escape 86 | ``` 87 | 88 | foo函数里的变量`t`逃逸了,和我们预想的一致。让我们不解的是为什么main函数里的`x`也逃逸了?这是因为有些函数参数为interface类型,比如fmt.Println(a ...interface{}),编译期间很难确定其参数的具体类型,也会发生逃逸。 89 | 90 | 反汇编源码: 91 | 92 | ```shell 93 | go tool compile -S main.go 94 | ``` 95 | 96 | 截取部分结果,图中标记出来的说明`t`是在堆上分配内存,发生了逃逸。 97 | ![反汇编](https://user-images.githubusercontent.com/7698088/53530056-a33ea380-3b29-11e9-9388-8ed7f6ce79ee.png) 98 | 99 | 引申2:下面代码中的变量发生逃逸了吗? 100 | 示例1:[代码出处](http://www.agardner.me/golang/garbage/collection/gc/escape/analysis/2015/10/18/go-escape-analysis.html) 101 | ```golang 102 | package main 103 | type S struct {} 104 | 105 | func main() { 106 | var x S 107 | _ = identity(x) 108 | } 109 | 110 | func identity(x S) S { 111 | return x 112 | } 113 | ``` 114 | 分析:Go语言函数传递都是通过值的,调用函数的时候,直接在栈上copy出一份参数,不存在逃逸。 115 | 116 | 示例2: 117 | ```golang 118 | package main 119 | 120 | type S struct {} 121 | 122 | func main() { 123 | var x S 124 | y := &x 125 | _ = *identity(y) 126 | } 127 | 128 | func identity(z *S) *S { 129 | return z 130 | } 131 | ``` 132 | 分析:identity函数的输入直接当成返回值了,因为没有对z作引用,所以z没有逃逸。对x的引用也没有逃出main函数的作用域,因此x也没有发生逃逸。 133 | 134 | 示例3: 135 | ```golang 136 | package main 137 | 138 | type S struct {} 139 | 140 | func main() { 141 | var x S 142 | _ = *ref(x) 143 | } 144 | 145 | func ref(z S) *S { 146 | return &z 147 | } 148 | ``` 149 | 分析:z是对x的拷贝,ref函数中对z取了引用,所以z不能放在栈上,否则在ref函数之外,通过引用如何找到z,所以z必须要逃逸到堆上。仅管在main函数中,直接丢弃了ref的结果,但是Go的编译器还没有那么智能,分析不出来这种情况。而对x从来就没有取引用,所以x不会发生逃逸。 150 | 151 | 示例4:如果对一个结构体成员赋引用如何? 152 | 153 | ```golang 154 | package main 155 | 156 | type S struct { 157 | M *int 158 | } 159 | 160 | func main() { 161 | var i int 162 | refStruct(i) 163 | } 164 | 165 | func refStruct(y int) (z S) { 166 | z.M = &y 167 | return z 168 | } 169 | ``` 170 | 分析:refStruct函数对y取了引用,所以y发生了逃逸。 171 | 172 | 示例5: 173 | ```golang 174 | package main 175 | 176 | type S struct { 177 | M *int 178 | } 179 | 180 | func main() { 181 | var i int 182 | refStruct(&i) 183 | } 184 | 185 | func refStruct(y *int) (z S) { 186 | z.M = y 187 | return z 188 | } 189 | ``` 190 | 分析:在main函数里对i取了引用,并且把它传给了refStruct函数,i的引用一直在main函数的作用域用,因此i没有发生逃逸。和上一个例子相比,有一点小差别,但是导致的程序效果是不同的:例子4中,i先在main的栈帧中分配,之后又在refStruct栈帧中分配,然后又逃逸到堆上,到堆上分配了一次,共3次分配。本例中,i只分配了一次,然后通过引用传递。 191 | 192 | 示例6: 193 | ```golang 194 | package main 195 | 196 | type S struct { 197 | M *int 198 | } 199 | 200 | func main() { 201 | var x S 202 | var i int 203 | ref(&i, &x) 204 | } 205 | 206 | func ref(y *int, z *S) { 207 | z.M = y 208 | } 209 | ``` 210 | 分析:本例i发生了逃逸,按照前面例子5的分析,i不会逃逸。两个例子的区别是例子5中的S是在返回值里的,输入只能“流入”到输出,本例中的S是在输入参数中,所以逃逸分析失败,i要逃逸到堆上。 211 | -------------------------------------------------------------------------------- /interface/类型转换和断言的区别.md: -------------------------------------------------------------------------------- 1 | 我们知道,Go 语言中不允许隐式类型转换,也就是说 `=` 两边,不允许出现类型不相同的变量。 2 | 3 | `类型转换`、`类型断言`本质都是把一个类型转换成另外一个类型。不同之处在于,类型断言是对接口变量进行的操作。 4 | 5 | # 类型转换 6 | 对于`类型转换`而言,转换前后的两个类型要相互兼容才行。类型转换的语法为: 7 | 8 | ><结果类型> := <目标类型> ( <表达式> ) 9 | 10 | ```golang 11 | package main 12 | 13 | import "fmt" 14 | 15 | func main() { 16 | var i int = 9 17 | 18 | var f float64 19 | f = float64(i) 20 | fmt.Printf("%T, %v\n", f, f) 21 | 22 | f = 10.8 23 | a := int(f) 24 | fmt.Printf("%T, %v\n", a, a) 25 | 26 | // s := []int(i) 27 | } 28 | ``` 29 | 30 | 上面的代码里,我定义了一个 `int` 型和 `float64` 型的变量,尝试在它们之前相互转换,结果是成功的:`int` 型和 `float64` 是相互兼容的。 31 | 32 | 如果我把最后一行代码的注释去掉,编译器会报告类型不兼容的错误: 33 | 34 | ```shell 35 | cannot convert i (type int) to type []int 36 | ``` 37 | 38 | # 断言 39 | 前面说过,因为空接口 `interface{}` 没有定义任何函数,因此 Go 中所有类型都实现了空接口。当一个函数的形参是 `interface{}`,那么在函数中,需要对形参进行断言,从而得到它的真实类型。 40 | 41 | 断言的语法为: 42 | > <目标类型的值>,<布尔参数> := <表达式>.( 目标类型 ) // 安全类型断言 43 | ><目标类型的值> := <表达式>.( 目标类型 )  //非安全类型断言 44 | 45 | 类型转换和类型断言有些相似,不同之处,在于类型断言是对接口进行的操作。 46 | 47 | 还是来看一个简短的例子: 48 | 49 | ```golang 50 | package main 51 | 52 | import "fmt" 53 | 54 | type Student struct { 55 | Name string 56 | Age int 57 | } 58 | 59 | func main() { 60 | var i interface{} = new(Student) 61 | s := i.(Student) 62 | 63 | fmt.Println(s) 64 | } 65 | ``` 66 | 67 | 运行一下: 68 | 69 | ```shell 70 | panic: interface conversion: interface {} is *main.Student, not main.Student 71 | ``` 72 | 73 | 直接 `panic` 了,这是因为 `i` 是 `*Student` 类型,并非 `Student` 类型,断言失败。这里直接发生了 `panic`,线上代码可能并不适合这样做,可以采用“安全断言”的语法: 74 | 75 | ```golang 76 | func main() { 77 | var i interface{} = new(Student) 78 | s, ok := i.(Student) 79 | if ok { 80 | fmt.Println(s) 81 | } 82 | } 83 | ``` 84 | 85 | 这样,即使断言失败也不会 `panic`。 86 | 87 | 断言其实还有另一种形式,就是用在利用 `switch` 语句判断接口的类型。每一个 `case` 会被顺序地考虑。当命中一个 `case` 时,就会执行 `case` 中的语句,因此 `case` 语句的顺序是很重要的,因为很有可能会有多个 `case` 匹配的情况。 88 | 89 | 代码示例如下: 90 | 91 | ```golang 92 | func main() { 93 | //var i interface{} = new(Student) 94 | //var i interface{} = (*Student)(nil) 95 | var i interface{} 96 | 97 | fmt.Printf("%p %v\n", &i, i) 98 | 99 | judge(i) 100 | } 101 | 102 | func judge(v interface{}) { 103 | fmt.Printf("%p %v\n", &v, v) 104 | 105 | switch v := v.(type) { 106 | case nil: 107 | fmt.Printf("%p %v\n", &v, v) 108 | fmt.Printf("nil type[%T] %v\n", v, v) 109 | 110 | case Student: 111 | fmt.Printf("%p %v\n", &v, v) 112 | fmt.Printf("Student type[%T] %v\n", v, v) 113 | 114 | case *Student: 115 | fmt.Printf("%p %v\n", &v, v) 116 | fmt.Printf("*Student type[%T] %v\n", v, v) 117 | 118 | default: 119 | fmt.Printf("%p %v\n", &v, v) 120 | fmt.Printf("unknow\n") 121 | } 122 | } 123 | 124 | type Student struct { 125 | Name string 126 | Age int 127 | } 128 | 129 | ``` 130 | 131 | `main` 函数里有三行不同的声明,每次运行一行,注释另外两行,得到三组运行结果: 132 | 133 | ```shell 134 | // --- var i interface{} = new(Student) 135 | 0xc4200701b0 [Name: ], [Age: 0] 136 | 0xc4200701d0 [Name: ], [Age: 0] 137 | 0xc420080020 [Name: ], [Age: 0] 138 | *Student type[*main.Student] [Name: ], [Age: 0] 139 | 140 | // --- var i interface{} = (*Student)(nil) 141 | 0xc42000e1d0 142 | 0xc42000e1f0 143 | 0xc42000c030 144 | *Student type[*main.Student] 145 | 146 | // --- var i interface{} 147 | 0xc42000e1d0 148 | 0xc42000e1e0 149 | 0xc42000e1f0 150 | nil type[] 151 | ``` 152 | 153 | 对于第一行语句: 154 | 155 | ```golang 156 | var i interface{} = new(Student) 157 | ``` 158 | 159 | `i` 是一个 `*Student` 类型,匹配上第三个 case,从打印的三个地址来看,这三处的变量实际上都是不一样的。在 `main` 函数里有一个局部变量 `i`;调用函数时,实际上是复制了一份参数,因此函数里又有一个变量 `v`,它是 `i` 的拷贝;断言之后,又生成了一份新的拷贝。所以最终打印的三个变量的地址都不一样。 160 | 161 | 对于第二行语句: 162 | 163 | ```golang 164 | var i interface{} = (*Student)(nil) 165 | ``` 166 | 167 | 这里想说明的其实是 `i` 在这里动态类型是 `(*Student)`, 数据为 `nil`,它的类型并不是 `nil`,它与 `nil` 作比较的时候,得到的结果也是 `false`。 168 | 169 | 最后一行语句: 170 | 171 | ```golang 172 | var i interface{} 173 | ``` 174 | 175 | 这回 `i` 才是 `nil` 类型。 176 | 177 | 【引申1】 178 | `fmt.Println` 函数的参数是 `interface`。对于内置类型,函数内部会用穷举法,得出它的真实类型,然后转换为字符串打印。而对于自定义类型,首先确定该类型是否实现了 `String()` 方法,如果实现了,则直接打印输出 `String()` 方法的结果;否则,会通过反射来遍历对象的成员进行打印。 179 | 180 | 再来看一个简短的例子,比较简单,不要紧张: 181 | 182 | ```golang 183 | package main 184 | 185 | import "fmt" 186 | 187 | type Student struct { 188 | Name string 189 | Age int 190 | } 191 | 192 | func main() { 193 | var s = Student{ 194 | Name: "qcrao", 195 | Age: 18, 196 | } 197 | 198 | fmt.Println(s) 199 | } 200 | ``` 201 | 202 | 因为 `Student` 结构体没有实现 `String()` 方法,所以 `fmt.Println` 会利用反射挨个打印成员变量: 203 | 204 | ```shell 205 | {qcrao 18} 206 | ``` 207 | 208 | 增加一个 `String()` 方法的实现: 209 | 210 | ```golang 211 | func (s Student) String() string { 212 | return fmt.Sprintf("[Name: %s], [Age: %d]", s.Name, s.Age) 213 | } 214 | ``` 215 | 216 | 打印结果: 217 | 218 | ```shell 219 | [Name: qcrao], [Age: 18] 220 | ``` 221 | 222 | 按照我们自定义的方法来打印了。 223 | 224 | 【引申2】 225 | 针对上面的例子,如果改一下: 226 | 227 | ```goalng 228 | func (s *Student) String() string { 229 | return fmt.Sprintf("[Name: %s], [Age: %d]", s.Name, s.Age) 230 | } 231 | ``` 232 | 233 | 注意看两个函数的接受者类型不同,现在 `Student` 结构体只有一个接受者类型为 `指针类型` 的 `String()` 函数,打印结果: 234 | 235 | ```shell 236 | {qcrao 18} 237 | ``` 238 | 239 | 为什么? 240 | 241 | >类型 `T` 只有接受者是 `T` 的方法;而类型 `*T` 拥有接受者是 `T` 和 `*T` 的方法。语法上 `T` 能直接调 `*T` 的方法仅仅是 `Go` 的语法糖。 242 | 243 | 所以, `Student` 结构体定义了接受者类型是值类型的 `String()` 方法时,通过 244 | 245 | ```golang 246 | fmt.Println(s) 247 | fmt.Println(&s) 248 | ``` 249 | 250 | 均可以按照自定义的格式来打印。 251 | 252 | 如果 `Student` 结构体定义了接受者类型是指针类型的 `String()` 方法时,只有通过 253 | 254 | ```golang 255 | fmt.Println(&s) 256 | ``` 257 | 258 | 才能按照自定义的格式打印。 259 | 260 | # 参考资料 261 | 【类型转换和断言】https://www.cnblogs.com/zrtqsk/p/4157350.html 262 | 263 | 【断言】https://studygolang.com/articles/11419 -------------------------------------------------------------------------------- /map/map 的遍历过程是怎样的.md: -------------------------------------------------------------------------------- 1 | 本来 map 的遍历过程比较简单:遍历所有的 bucket 以及它后面挂的 overflow bucket,然后挨个遍历 bucket 中的所有 cell。每个 bucket 中包含 8 个 cell,从有 key 的 cell 中取出 key 和 value,这个过程就完成了。 2 | 3 | 但是,现实并没有这么简单。还记得前面讲过的扩容过程吗?扩容过程不是一个原子的操作,它每次最多只搬运 2 个 bucket,所以如果触发了扩容操作,那么在很长时间里,map 的状态都是处于一个中间态:有些 bucket 已经搬迁到新家,而有些 bucket 还待在老地方。 4 | 5 | 因此,遍历如果发生在扩容的过程中,就会涉及到遍历新老 bucket 的过程,这是难点所在。 6 | 7 | 我先写一个简单的代码样例,假装不知道遍历过程具体调用的是什么函数: 8 | 9 | ```golang 10 | package main 11 | 12 | import "fmt" 13 | 14 | func main() { 15 | ageMp := make(map[string]int) 16 | ageMp["qcrao"] = 18 17 | 18 | for name, age := range ageMp { 19 | fmt.Println(name, age) 20 | } 21 | } 22 | ``` 23 | 24 | 执行命令: 25 | 26 | ```shell 27 | go tool compile -S main.go 28 | ``` 29 | 30 | 得到汇编命令。这里就不逐行讲解了,可以去看之前的几篇文章,说得很详细。 31 | 32 | 关键的几行汇编代码如下: 33 | 34 | ```golang 35 | // ...... 36 | 0x0124 00292 (test16.go:9) CALL runtime.mapiterinit(SB) 37 | 38 | // ...... 39 | 0x01fb 00507 (test16.go:9) CALL runtime.mapiternext(SB) 40 | 0x0200 00512 (test16.go:9) MOVQ ""..autotmp_4+160(SP), AX 41 | 0x0208 00520 (test16.go:9) TESTQ AX, AX 42 | 0x020b 00523 (test16.go:9) JNE 302 43 | 44 | // ...... 45 | ``` 46 | 47 | 这样,关于 map 迭代,底层的函数调用关系一目了然。先是调用 `mapiterinit` 函数初始化迭代器,然后循环调用 `mapiternext` 函数进行 map 迭代。 48 | 49 | ![map iter loop](https://user-images.githubusercontent.com/7698088/57976471-ad2ebf00-7a13-11e9-8dd8-d7be54f96440.png) 50 | 51 | 迭代器的结构体定义: 52 | 53 | ```golang 54 | type hiter struct { 55 | // key 指针 56 | key unsafe.Pointer 57 | // value 指针 58 | value unsafe.Pointer 59 | // map 类型,包含如 key size 大小等 60 | t *maptype 61 | // map header 62 | h *hmap 63 | // 初始化时指向的 bucket 64 | buckets unsafe.Pointer 65 | // 当前遍历到的 bmap 66 | bptr *bmap 67 | overflow [2]*[]*bmap 68 | // 起始遍历的 bucet 编号 69 | startBucket uintptr 70 | // 遍历开始时 cell 的编号(每个 bucket 中有 8 个 cell) 71 | offset uint8 72 | // 是否从头遍历了 73 | wrapped bool 74 | // B 的大小 75 | B uint8 76 | // 指示当前 cell 序号 77 | i uint8 78 | // 指向当前的 bucket 79 | bucket uintptr 80 | // 因为扩容,需要检查的 bucket 81 | checkBucket uintptr 82 | } 83 | ``` 84 | 85 | `mapiterinit` 就是对 hiter 结构体里的字段进行初始化赋值操作。 86 | 87 | 前面已经提到过,即使是对一个写死的 map 进行遍历,每次出来的结果也是无序的。下面我们就可以近距离地观察他们的实现了。 88 | 89 | ```golang 90 | // 生成随机数 r 91 | r := uintptr(fastrand()) 92 | if h.B > 31-bucketCntBits { 93 | r += uintptr(fastrand()) << 31 94 | } 95 | 96 | // 从哪个 bucket 开始遍历 97 | it.startBucket = r & (uintptr(1)<> h.B & (bucketCnt - 1)) 100 | ``` 101 | 102 | 例如,B = 2,那 `uintptr(1)< 0 -> 1 -> 2。 117 | 118 | 因为 3 号 bucket 对应老的 1 号 bucket,因此先检查老 1 号 bucket 是否已经被搬迁过。判断方法就是: 119 | 120 | ```golang 121 | func evacuated(b *bmap) bool { 122 | h := b.tophash[0] 123 | return h > empty && h < minTopHash 124 | } 125 | ``` 126 | 127 | 如果 b.tophash[0] 的值在标志值范围内,即在 (0,4) 区间里,说明已经被搬迁过了。 128 | 129 | ```golang 130 | empty = 0 131 | evacuatedEmpty = 1 132 | evacuatedX = 2 133 | evacuatedY = 3 134 | minTopHash = 4 135 | ``` 136 | 137 | 在本例中,老 1 号 bucket 已经被搬迁过了。所以它的 tophash[0] 值在 (0,4) 范围内,因此只用遍历新的 3 号 bucket。 138 | 139 | 依次遍历 3 号 bucket 的 cell,这时候会找到第一个非空的 key:元素 e。到这里,mapiternext 函数返回,这时我们的遍历结果仅有一个元素: 140 | 141 | ![iter res](https://user-images.githubusercontent.com/7698088/57980302-56010c80-7a5c-11e9-8263-c11ddcec2ecc.png) 142 | 143 | 由于返回的 key 不为空,所以会继续调用 mapiternext 函数。 144 | 145 | 继续从上次遍历到的地方往后遍历,从新 3 号 overflow bucket 中找到了元素 f 和 元素 g。 146 | 147 | 遍历结果集也因此壮大: 148 | 149 | ![iter res](https://user-images.githubusercontent.com/7698088/57980349-2d2d4700-7a5d-11e9-819a-a59964f70a7c.png) 150 | 151 | 新 3 号 bucket 遍历完之后,回到了新 0 号 bucket。0 号 bucket 对应老的 0 号 bucket,经检查,老 0 号 bucket 并未搬迁,因此对新 0 号 bucket 的遍历就改为遍历老 0 号 bucket。那是不是把老 0 号 bucket 中的所有 key 都取出来呢? 152 | 153 | 并没有这么简单,回忆一下,老 0 号 bucket 在搬迁后将裂变成 2 个 bucket:新 0 号、新 2 号。而我们此时正在遍历的只是新 0 号 bucket(注意,遍历都是遍历的 `*bucket` 指针,也就是所谓的新 buckets)。所以,我们只会取出老 0 号 bucket 中那些在裂变之后,分配到新 0 号 bucket 中的那些 key。 154 | 155 | 因此,`lowbits == 00` 的将进入遍历结果集: 156 | 157 | ![iter res](https://user-images.githubusercontent.com/7698088/57980449-6fa35380-7a5e-11e9-9dbf-86332ea0e215.png) 158 | 159 | 和之前的流程一样,继续遍历新 1 号 bucket,发现老 1 号 bucket 已经搬迁,只用遍历新 1 号 bucket 中现有的元素就可以了。结果集变成: 160 | 161 | ![iter res](https://user-images.githubusercontent.com/7698088/57980487-e8a2ab00-7a5e-11e9-8e47-050437a099fc.png) 162 | 163 | 继续遍历新 2 号 bucket,它来自老 0 号 bucket,因此需要在老 0 号 bucket 中那些会裂变到新 2 号 bucket 中的 key,也就是 `lowbit == 10` 的那些 key。 164 | 165 | 这样,遍历结果集变成: 166 | 167 | ![iter res](https://user-images.githubusercontent.com/7698088/57980574-ae85d900-7a5f-11e9-8050-ae314a90ee05.png) 168 | 169 | 最后,继续遍历到新 3 号 bucket 时,发现所有的 bucket 都已经遍历完毕,整个迭代过程执行完毕。 170 | 171 | 顺便说一下,如果碰到 key 是 `math.NaN()` 这种的,处理方式类似。核心还是要看它被分裂后具体落入哪个 bucket。只不过只用看它 top hash 的最低位。如果 top hash 的最低位是 0 ,分配到 X part;如果是 1 ,则分配到 Y part。据此决定是否取出 key,放到遍历结果集里。 172 | 173 | map 遍历的核心在于理解 2 倍扩容时,老 bucket 会分裂到 2 个新 bucket 中去。而遍历操作,会按照新 bucket 的序号顺序进行,碰到老 bucket 未搬迁的情况时,要在老 bucket 中找到将来要搬迁到新 bucket 来的 key。 -------------------------------------------------------------------------------- /编译和链接/Go 编译相关的命令详解.md: -------------------------------------------------------------------------------- 1 | 直接在终端执行: 2 | 3 | ```shell 4 | go 5 | ``` 6 | 7 | 就能得到和 go 相关的命令简介: 8 | 9 | ![go commands](https://user-images.githubusercontent.com/7698088/60248752-e2bda680-98f5-11e9-8b3b-7deaf70a919c.png) 10 | 11 | 和编译相关的命令主要是: 12 | 13 | ```shell 14 | go build 15 | go install 16 | go run 17 | ``` 18 | 19 | # go build 20 | `go build` 用来编译指定 packages 里的源码文件以及它们的依赖包,编译的时候会到 `$GoPath/src/package` 路径下寻找源码文件。`go build` 还可以直接编译指定的源码文件,并且可以同时指定多个。 21 | 22 | 通过执行 `go help build` 命令得到 `go build` 的使用方法: 23 | 24 | ```shell 25 | usage: go build [-o output] [-i] [build flags] [packages] 26 | ``` 27 | 28 | `-o` 只能在编译单个包的时候出现,它指定输出的可执行文件的名字。 29 | 30 | `-i` 会安装编译目标所依赖的包,安装是指生成与代码包相对应的 `.a` 文件,即静态库文件(后面要参与链接),并且放置到当前工作区的 pkg 目录下,且库文件的目录层级和源码层级一致。 31 | 32 | 至于 build flags 参数,`build, clean, get, install, list, run, test` 这些命令会共用一套: 33 | 34 | |参数|作用| 35 | |---|---| 36 | |-a|强制重新编译所有涉及到的包,包括标准库中的代码包,这会重写 /usr/local/go 目录下的 `.a` 文件| 37 | |-n|打印命令执行过程,不真正执行| 38 | |-p n|指定编译过程中命令执行的并行数,n 默认为 CPU 核数| 39 | |-race|检测并报告程序中的数据竞争问题| 40 | |-v|打印命令执行过程中所涉及到的代码包名称| 41 | |-x|打印命令执行过程中所涉及到的命令,并执行| 42 | |-work|打印编译过程中的临时文件夹。通常情况下,编译完成后会被删除| 43 | 44 | 我们知道,Go 语言的源码文件分为三类:命令源码、库源码、测试源码。 45 | 46 | > 命令源码文件:是 Go 程序的入口,包含 `func main()` 函数,且第一行用 `package main` 声明属于 main 包。 47 | 48 | > 库源码文件:主要是各种函数、接口等,例如工具类的函数。 49 | 50 | > 测试源码文件:以 `_test.go` 为后缀的文件,用于测试程序的功能和性能。 51 | 52 | 注意,`go build` 会忽略 `*_test.go` 文件。 53 | 54 | 我们通过一个很简单的例子来演示 `go build` 命令。我用 Goland 新建了一个 `hello-world` 项目(为了展示引用自定义的包,和之前的 hello-world 程序不同),项目的结构如下: 55 | 56 | ![example structure](https://user-images.githubusercontent.com/7698088/60383032-5b5f6700-9a9e-11e9-9613-03d9ba13b889.png) 57 | 58 | 最左边可以看到项目的结构,包含三个文件夹:bin,pkg,src。其中 src 目录下有一个 main.go,里面定义了 main 函数,是整个项目的入口,也就是前面提过的所谓的命令源码文件;src 目录下还有一个 util 目录,里面有 util.go 文件,定义了一个可以获取本机 IP 地址的函数,也就是所谓的库源码文件。 59 | 60 | 中间是 main.go 的源码,引用了两个包,一个是标准库的 fmt;一个是 util 包,util 的导入路径是 `util`。所谓的导入路径是指相对于 Go 的源码目录 `$GoRoot/src` 或者 `$GoPath/src` 的下的子路径。例如 main 包里引用的 fmt 的源码路径是 `/usr/local/go/src/fmt`,而 util 的源码路径是 `/Users/qcrao/hello-world/src/util`,正好我们设置的 GoPath = /Users/qcrao/hello-world。 61 | 62 | 最右边是库函数的源码,实现了获取本机 IP 的函数。 63 | 64 | 在 src 目录下,直接执行 `go build` 命令,在同级目录生成了一个可执行文件,文件名为 `src`,使用 `./src` 命令直接执行,输出: 65 | 66 | ```shell 67 | hello world! 68 | Local IP: 192.168.1.3 69 | ``` 70 | 71 | 我们也可以指定生成的可执行文件的名称: 72 | 73 | ```shell 74 | go build -o bin/hello 75 | ``` 76 | 77 | 这样,在 bin 目录下会生成一个可执行文件,运行结果和上面的 `src` 一样。 78 | 79 | 其实,util 包可以单独被编译。我们可以在项目根目录下执行: 80 | 81 | ```shell 82 | go build util 83 | ``` 84 | 85 | 编译程序会去 $GoPath/src 路径找 util 包(其实是找文件夹)。还可以在 `./src/util` 目录下直接执行 `go build` 编译。 86 | 87 | 当然,直接编译库源码文件不会生成 .a 文件,因为: 88 | 89 | >go build 命令在编译只包含库源码文件的代码包(或者同时编译多个代码包)时,只会做检查性的编译,而不会输出任何结果文件。 90 | 91 | 为了展示整个编译链接的运行过程,我们在项目根目录执行如下的命令: 92 | 93 | ```shell 94 | go build -v -x -work -o bin/hello src/main.go 95 | ``` 96 | 97 | `-v` 会打印所编译过的包名字,`-x` 打印编译期间所执行的命令,`-work` 打印编译期间生成的临时文件路径,并且编译完成之后不会被删除。 98 | 99 | 执行结果: 100 | 101 | ![编译过程](https://user-images.githubusercontent.com/7698088/60386219-e3586780-9ac4-11e9-871f-5acfa83372d0.png) 102 | 103 | 从结果来看,图中用箭头标注了本次编译过程涉及 2 个包:util,command-line-arguments。第二个包比较诡异,源码里根本就没有这个名字好吗?其实这是 `go build` 命令检测到 [packages] 处填的是一个 `.go` 文件,因此创建了一个虚拟的包:command-line-arguments。 104 | 105 | 同时,用红框圈出了 compile, link,也就是先编译了 util 包和 `main.go` 文件,分别得到 `.a` 文件,之后将两者进行链接,最终生成可执行文件,并且移动到 bin 目录下,改名为 hello。 106 | 107 | 另外,第一行显示了编译过程中的工作目录,此目录的文件结构是: 108 | 109 | ![临时工作目录](https://user-images.githubusercontent.com/7698088/60386682-06861580-9acb-11e9-8367-d37ce03a46cc.png) 110 | 111 | 可以看到,和 hello-world 目录的层级基本一致。command-line-arguments 就是虚拟的 main.go 文件所处的包。exe 目录下的可执行文件在最后一步被移动到了 bin 目录下,所以这里是空的。 112 | 113 | 整体来看,`go build` 在执行时,会先递归寻找 main.go 所依赖的包,以及依赖的依赖,直至最底层的包。这里可以是深度优先遍历也可以是宽度优先遍历。如果发现有循环依赖,就会直接退出,这也是经常会发生的循环引用编译错误。 114 | 115 | 正常情况下,这些依赖关系会形成一棵倒着生长的树,树根在最上面,就是 main.go 文件,最下面是没有任何其他依赖的包。编译器会从最左的节点所代表的包开始挨个编译,完成之后,再去编译上一层的包。 116 | 117 | 这里,引用郝林老师几年前在 github 上发表的 go 命令教程,可以从参考资料找到原文地址。 118 | 119 | > 从代码包编译的角度来说,如果代码包 A 依赖代码包 B,则称代码包 B 是代码包 A 的依赖代码包(以下简称依赖包),代码包 A 是代码包 B 的触发代码包(以下简称触发包)。 120 | 121 | > 执行 `go build` 命令的计算机如果拥有多个逻辑 CPU 核心,那么编译代码包的顺序可能会存在一些不确定性。但是,它一定会满足这样的约束条件:依赖代码包 -> 当前代码包 -> 触发代码包。 122 | 123 | 顺便推荐一个浏览器插件 Octotree,在看 github 项目的时候,此插件可以在浏览器里直接展示整个项目的文件结构,非常方便: 124 | 125 | ![github 插件](https://user-images.githubusercontent.com/7698088/60390988-d9f7eb00-9b16-11e9-83ec-64c3c0beb6ad.png) 126 | 127 | 到这里,你一定会发现,对于 hello-wrold 文件夹下的 pkg 目录好像一直没有涉及到。 128 | 129 | 其实,pkg 目录下面应该存放的是涉及到的库文件编译后的包,也就是一些 `.a` 文件。但是 go build 执行过程中,这些 `.a` 文件放在临时文件夹中,编译完成后会被直接删掉,因此一般不会用到。 130 | 131 | 前面我们提到过,在 go build 命令里加上 `-i` 参数会安装这些库文件编译的包,也就是这些 `.a` 文件会放到 pkg 目录下。 132 | 133 | 在项目根目录执行 `go build -i src/main.go` 后,pkg 目录里增加了 util.a 文件: 134 | 135 | ![pkg](https://user-images.githubusercontent.com/7698088/60386864-84e3b700-9acd-11e9-9513-68a52ff460bb.png) 136 | 137 | `darwin_amd64` 表示的是: 138 | 139 | >GOOS 和 GOARCH。这两个环境变量不用我们设置,系统默认的。 140 | 141 | >GOOS 是 Go 所在的操作系统类型,GOARCH 是 Go 所在的计算架构。 142 | 143 | >Mac 平台上这个目录名就是 darwin_amd64。 144 | 145 | 生成了 util.a 文件后,再次编译的时候,就不会再重新编译 util.go 文件,加快了编译速度。 146 | 147 | 同时,在根目录下生成了名称为 main 的可执行文件,这是以 main.go 的文件名命令的。 148 | 149 | hello-world 这个项目的代码已经上传到了 github 项目 `Go-Questions`,这个项目由问题导入,企图串连 Go 的所有知识点,正在完善,期待你的 star。 地址见参考资料【Go-Questions hello-world项目】。 150 | 151 | # go install 152 | `go install` 用于编译并安装指定的代码包及它们的依赖包。相比 `go build`,它只是多了一个“安装编译后的结果文件到指定目录”的步骤。 153 | 154 | 还是使用之前 hello-world 项目的例子,我们先将 pkg 目录删掉,在项目根目录执行: 155 | 156 | ```shell 157 | go install src/main.go 158 | 159 | 或者 160 | 161 | go install util 162 | ``` 163 | 164 | 两者都会在根目录下新建一个 `pkg` 目录,并且生成一个 `util.a` 文件。 165 | 166 | 并且,在执行前者的时候,会在 GOBIN 目录下生成名为 main 的可执行文件。 167 | 168 | 所以,运行 `go install` 命令,库源码包对应的 `.a` 文件会被放置到 `pkg` 目录下,命令源码包生成的可执行文件会被放到 GOBIN 目录。 169 | 170 | `go install` 在 GoPath 有多个目录的时候,会产生一些问题,具体可以去看郝林老师的 `Go 命令教程`,这里不展开了。 171 | 172 | # go run 173 | `go run` 用于编译并运行命令源码文件。 174 | 175 | 在 hello-world 项目的根目录,执行 go run 命令: 176 | 177 | ```shell 178 | go run -x -work src/main.go 179 | ``` 180 | 181 | -x 可以打印整个过程涉及到的命令,-work 可以看到临时的工作目录: 182 | 183 | ![go run 过程](https://user-images.githubusercontent.com/7698088/60391387-ae2d3300-9b1f-11e9-9355-a8f59c2eac9b.png) 184 | 185 | 从上图中可以看到,仍然是先编译,再连接,最后直接执行,并打印出了执行结果。 186 | 187 | 第一行打印的就是工作目录,最终生成的可执行文件就是放置于此: 188 | 189 | ![go run 结果](https://user-images.githubusercontent.com/7698088/60391357-30692780-9b1f-11e9-8be4-48041779e293.png) 190 | 191 | main 就是最终生成的可执行文件。 -------------------------------------------------------------------------------- /channel/如何优雅地关闭 channel.md: -------------------------------------------------------------------------------- 1 | 关于 channel 的使用,有几点不方便的地方: 2 | 3 | 1. 在不改变 channel 自身状态的情况下,无法获知一个 channel 是否关闭。 4 | 2. 关闭一个 closed channel 会导致 panic。所以,如果关闭 channel 的一方在不知道 channel 是否处于关闭状态时就去贸然关闭 channel 是很危险的事情。 5 | 3. 向一个 closed channel 发送数据会导致 panic。所以,如果向 channel 发送数据的一方不知道 channel 是否处于关闭状态时就去贸然向 channel 发送数据是很危险的事情。 6 | 7 | 一个比较粗糙的检查 channel 是否关闭的函数: 8 | 9 | ```golang 10 | func IsClosed(ch <-chan T) bool { 11 | select { 12 | case <-ch: 13 | return true 14 | default: 15 | } 16 | 17 | return false 18 | } 19 | 20 | func main() { 21 | c := make(chan T) 22 | fmt.Println(IsClosed(c)) // false 23 | close(c) 24 | fmt.Println(IsClosed(c)) // true 25 | } 26 | ``` 27 | 28 | 看一下代码,其实存在很多问题。首先,IsClosed 函数是一个有副作用的函数。每调用一次,都会读出 channel 里的一个元素,改变了 channel 的状态。这不是一个好的函数,干活就干活,还顺手牵羊! 29 | 30 | 其次,IsClosed 函数返回的结果仅代表调用那个瞬间,并不能保证调用之后会不会有其他 goroutine 对它进行了一些操作,改变了它的这种状态。例如,IsClosed 函数返回 true,但这时有另一个 goroutine 关闭了 channel,而你还拿着这个过时的 “channel 未关闭”的信息,向其发送数据,就会导致 panic 的发生。当然,一个 channel 不会被重复关闭两次,如果 IsClosed 函数返回的结果是 true,说明 channel 是真的关闭了。 31 | 32 | 有一条广泛流传的关闭 channel 的原则: 33 | 34 | > don't close a channel from the receiver side and don't close a channel if the channel has multiple concurrent senders. 35 | 36 | 不要从一个 receiver 侧关闭 channel,也不要在有多个 sender 时,关闭 channel。 37 | 38 | 比较好理解,向 channel 发送元素的就是 sender,因此 sender 可以决定何时不发送数据,并且关闭 channel。但是如果有多个 sender,某个 sender 同样没法确定其他 sender 的情况,这时也不能贸然关闭 channel。 39 | 40 | 但是上面所说的并不是最本质的,最本质的原则就只有一条: 41 | 42 | > don't close (or send values to) closed channels. 43 | 44 | 有两个不那么优雅地关闭 channel 的方法: 45 | 46 | 1. 使用 defer-recover 机制,放心大胆地关闭 channel 或者向 channel 发送数据。即使发生了 panic,有 defer-recover 在兜底。 47 | 48 | 2. 使用 sync.Once 来保证只关闭一次。 49 | 50 | 那到底应该如何优雅地关闭 channel? 51 | 52 | 根据 sender 和 receiver 的个数,分下面几种情况: 53 | 54 | 1. 一个 sender,一个 receiver 55 | 2. 一个 sender, M 个 receiver 56 | 3. N 个 sender,一个 reciver 57 | 4. N 个 sender, M 个 receiver 58 | 59 | 对于 1,2,只有一个 sender 的情况就不用说了,直接从 sender 端关闭就好了,没有问题。重点关注第 3,4 种情况。 60 | 61 | 第 3 种情形下,优雅关闭 channel 的方法是:the only receiver says "please stop sending more" by closing an additional signal channel。 62 | 63 | 解决方案就是增加一个传递关闭信号的 channel,receiver 通过信号 channel 下达关闭数据 channel 指令。senders 监听到关闭信号后,停止接收数据。代码如下: 64 | 65 | ```golang 66 | func main() { 67 | rand.Seed(time.Now().UnixNano()) 68 | 69 | const Max = 100000 70 | const NumSenders = 1000 71 | 72 | dataCh := make(chan int, 100) 73 | stopCh := make(chan struct{}) 74 | 75 | // senders 76 | for i := 0; i < NumSenders; i++ { 77 | go func() { 78 | for { 79 | select { 80 | case <- stopCh: 81 | return 82 | case dataCh <- rand.Intn(Max): 83 | } 84 | } 85 | }() 86 | } 87 | 88 | // the receiver 89 | go func() { 90 | for value := range dataCh { 91 | if value == Max-1 { 92 | fmt.Println("send stop signal to senders.") 93 | close(stopCh) 94 | return 95 | } 96 | 97 | fmt.Println(value) 98 | } 99 | }() 100 | 101 | select { 102 | case <- time.After(time.Hour): 103 | } 104 | } 105 | ``` 106 | 107 | 这里的 stopCh 就是信号 channel,它本身只有一个 sender,因此可以直接关闭它。senders 收到了关闭信号后,select 分支 “case <- stopCh” 被选中,退出函数,不再发送数据。 108 | 109 | 需要说明的是,上面的代码并没有明确关闭 dataCh。在 Go 语言中,对于一个 channel,如果最终没有任何 goroutine 引用它,不管 channel 有没有被关闭,最终都会被 gc 回收。所以,在这种情形下,所谓的优雅地关闭 channel 就是不关闭 channel,让 gc 代劳。 110 | 111 | 最后一种情况,优雅关闭 channel 的方法是:any one of them says "let's end the game" by notifying a moderator to close an additional signal channel。 112 | 113 | 和第 3 种情况不同,这里有 M 个 receiver,如果直接还是采取第 3 种解决方案,由 receiver 直接关闭 stopCh 的话,就会重复关闭一个 channel,导致 panic。因此需要增加一个中间人,M 个 receiver 都向它发送关闭 dataCh 的“请求”,中间人收到第一个请求后,就会直接下达关闭 dataCh 的指令(通过关闭 stopCh,这时就不会发生重复关闭的情况,因为 stopCh 的发送方只有中间人一个)。另外,这里的 N 个 sender 也可以向中间人发送关闭 dataCh 的请求。 114 | 115 | ```golang 116 | func main() { 117 | rand.Seed(time.Now().UnixNano()) 118 | 119 | const Max = 100000 120 | const NumReceivers = 10 121 | const NumSenders = 1000 122 | 123 | dataCh := make(chan int, 100) 124 | stopCh := make(chan struct{}) 125 | 126 | // It must be a buffered channel. 127 | toStop := make(chan string, 1) 128 | 129 | var stoppedBy string 130 | 131 | // moderator 132 | go func() { 133 | stoppedBy = <-toStop 134 | close(stopCh) 135 | }() 136 | 137 | // senders 138 | for i := 0; i < NumSenders; i++ { 139 | go func(id string) { 140 | for { 141 | value := rand.Intn(Max) 142 | if value == 0 { 143 | select { 144 | case toStop <- "sender#" + id: 145 | default: 146 | } 147 | return 148 | } 149 | 150 | select { 151 | case <- stopCh: 152 | return 153 | case dataCh <- value: 154 | } 155 | } 156 | }(strconv.Itoa(i)) 157 | } 158 | 159 | // receivers 160 | for i := 0; i < NumReceivers; i++ { 161 | go func(id string) { 162 | for { 163 | select { 164 | case <- stopCh: 165 | return 166 | case value := <-dataCh: 167 | if value == Max-1 { 168 | select { 169 | case toStop <- "receiver#" + id: 170 | default: 171 | } 172 | return 173 | } 174 | 175 | fmt.Println(value) 176 | } 177 | } 178 | }(strconv.Itoa(i)) 179 | } 180 | 181 | select { 182 | case <- time.After(time.Hour): 183 | } 184 | 185 | } 186 | ``` 187 | 188 | 代码里 toStop 就是中间人的角色,使用它来接收 senders 和 receivers 发送过来的关闭 dataCh 请求。 189 | 190 | 这里将 toStop 声明成了一个 缓冲型的 channel。假设 toStop 声明的是一个非缓冲型的 channel,那么第一个发送的关闭 dataCh 请求可能会丢失。因为无论是 sender 还是 receiver 都是通过 select 语句来发送请求,如果中间人所在的 goroutine 没有准备好,那 select 语句就不会选中,直接走 default 选项,什么也不做。这样,第一个关闭 dataCh 的请求就会丢失。 191 | 192 | 如果,我们把 toStop 的容量声明成 Num(senders) + Num(receivers),那发送 dataCh 请求的部分可以改成更简洁的形式: 193 | 194 | ```golang 195 | ... 196 | toStop := make(chan string, NumReceivers + NumSenders) 197 | ... 198 | value := rand.Intn(Max) 199 | if value == 0 { 200 | toStop <- "sender#" + id 201 | return 202 | } 203 | ... 204 | if value == Max-1 { 205 | toStop <- "receiver#" + id 206 | return 207 | } 208 | ... 209 | ``` 210 | 211 | 直接向 toStop 发送请求,因为 toStop 容量足够大,所以不用担心阻塞,自然也就不用 select 语句再加一个 default case 来避免阻塞。 212 | 213 | 可以看到,这里同样没有真正关闭 dataCh,原样同第 3 种情况。 214 | 215 | 以上,就是最基本的一些情形,但已经能覆盖几乎所有的情况及其变种了。只要记住: 216 | 217 | > don't close a channel from the receiver side and don't close a channel if the channel has multiple concurrent senders. 218 | 219 | 以及更本质的原则: 220 | 221 | > don't close (or send values to) closed channels. 222 | 223 | # 参考资料 224 | https://go101.org/article/channel-closing.html 225 | -------------------------------------------------------------------------------- /interface/接口的构造过程是怎样的.md: -------------------------------------------------------------------------------- 1 | 我们已经看过了 `iface` 和 `eface` 的源码,知道 `iface` 最重要的是 `itab` 和 `_type`。 2 | 3 | 为了研究清楚接口是如何构造的,接下来我会拿起汇编的武器,还原背后的真相。 4 | 5 | 来看一个示例代码: 6 | 7 | ```golang 8 | package main 9 | 10 | import "fmt" 11 | 12 | type Person interface { 13 | growUp() 14 | } 15 | 16 | type Student struct { 17 | age int 18 | } 19 | 20 | func (p Student) growUp() { 21 | p.age += 1 22 | return 23 | } 24 | 25 | func main() { 26 | var qcrao = Person(Student{age: 18}) 27 | 28 | fmt.Println(qcrao) 29 | } 30 | 31 | ``` 32 | 33 | 执行命令: 34 | 35 | ```shell 36 | go tool compile -S main.go 37 | ``` 38 | 39 | 得到 main 函数的汇编代码如下: 40 | 41 | ```asm 42 | 0x0000 00000 (./src/main.go:30) TEXT "".main(SB), $80-0 43 | 0x0000 00000 (./src/main.go:30) MOVQ (TLS), CX 44 | 0x0009 00009 (./src/main.go:30) CMPQ SP, 16(CX) 45 | 0x000d 00013 (./src/main.go:30) JLS 157 46 | 0x0013 00019 (./src/main.go:30) SUBQ $80, SP 47 | 0x0017 00023 (./src/main.go:30) MOVQ BP, 72(SP) 48 | 0x001c 00028 (./src/main.go:30) LEAQ 72(SP), BP 49 | 0x0021 00033 (./src/main.go:30) FUNCDATA$0, gclocals·69c1753bd5f81501d95132d08af04464(SB) 50 | 0x0021 00033 (./src/main.go:30) FUNCDATA$1, gclocals·e226d4ae4a7cad8835311c6a4683c14f(SB) 51 | 0x0021 00033 (./src/main.go:31) MOVQ $18, ""..autotmp_1+48(SP) 52 | 0x002a 00042 (./src/main.go:31) LEAQ go.itab."".Student,"".Person(SB), AX 53 | 0x0031 00049 (./src/main.go:31) MOVQ AX, (SP) 54 | 0x0035 00053 (./src/main.go:31) LEAQ ""..autotmp_1+48(SP), AX 55 | 0x003a 00058 (./src/main.go:31) MOVQ AX, 8(SP) 56 | 0x003f 00063 (./src/main.go:31) PCDATA $0, $0 57 | 0x003f 00063 (./src/main.go:31) CALL runtime.convT2I64(SB) 58 | 0x0044 00068 (./src/main.go:31) MOVQ 24(SP), AX 59 | 0x0049 00073 (./src/main.go:31) MOVQ 16(SP), CX 60 | 0x004e 00078 (./src/main.go:33) TESTQ CX, CX 61 | 0x0051 00081 (./src/main.go:33) JEQ 87 62 | 0x0053 00083 (./src/main.go:33) MOVQ 8(CX), CX 63 | 0x0057 00087 (./src/main.go:33) MOVQ $0, ""..autotmp_2+56(SP) 64 | 0x0060 00096 (./src/main.go:33) MOVQ $0, ""..autotmp_2+64(SP) 65 | 0x0069 00105 (./src/main.go:33) MOVQ CX, ""..autotmp_2+56(SP) 66 | 0x006e 00110 (./src/main.go:33) MOVQ AX, ""..autotmp_2+64(SP) 67 | 0x0073 00115 (./src/main.go:33) LEAQ ""..autotmp_2+56(SP), AX 68 | 0x0078 00120 (./src/main.go:33) MOVQ AX, (SP) 69 | 0x007c 00124 (./src/main.go:33) MOVQ $1, 8(SP) 70 | 0x0085 00133 (./src/main.go:33) MOVQ $1, 16(SP) 71 | 0x008e 00142 (./src/main.go:33) PCDATA $0, $1 72 | 0x008e 00142 (./src/main.go:33) CALL fmt.Println(SB) 73 | 0x0093 00147 (./src/main.go:34) MOVQ 72(SP), BP 74 | 0x0098 00152 (./src/main.go:34) ADDQ $80, SP 75 | 0x009c 00156 (./src/main.go:34) RET 76 | 0x009d 00157 (./src/main.go:34) NOP 77 | 0x009d 00157 (./src/main.go:30) PCDATA $0, $-1 78 | 0x009d 00157 (./src/main.go:30) CALL runtime.morestack_noctxt(SB) 79 | 0x00a2 00162 (./src/main.go:30) JMP 0 80 | ``` 81 | 82 | 我们从第 10 行开始看,如果不理解前面几行汇编代码的话,可以回去看看公众号前面两篇文章,这里我就省略了。 83 | 84 | |汇编行数|操作| 85 | |---|---| 86 | |10-14|构造调用 `runtime.convT2I64(SB)` 的参数| 87 | 88 | 我们来看下这个函数的参数形式: 89 | 90 | ```golang 91 | func convT2I64(tab *itab, elem unsafe.Pointer) (i iface) { 92 | // …… 93 | } 94 | ``` 95 | 96 | `convT2I64` 会构造出一个 `inteface`,也就是我们的 `Person` 接口。 97 | 98 | 第一个参数的位置是 `(SP)`,这里被赋上了 `go.itab."".Student,"".Person(SB)` 的地址。 99 | 100 | 我们从生成的汇编找到: 101 | 102 | ```asm 103 | go.itab."".Student,"".Person SNOPTRDATA dupok size=40 104 | 0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 105 | 0x0010 00 00 00 00 00 00 00 00 da 9f 20 d4 106 | rel 0+8 t=1 type."".Person+0 107 | rel 8+8 t=1 type."".Student+0 108 | ``` 109 | 110 | `size=40` 大小为40字节,回顾一下: 111 | 112 | ```golang 113 | type itab struct { 114 | inter *interfacetype // 8字节 115 | _type *_type // 8字节 116 | link *itab // 8字节 117 | hash uint32 // 4字节 118 | bad bool // 1字节 119 | inhash bool // 1字节 120 | unused [2]byte // 2字节 121 | fun [1]uintptr // variable sized // 8字节 122 | } 123 | ``` 124 | 125 | 把每个字段的大小相加,`itab` 结构体的大小就是 40 字节。上面那一串数字实际上是 `itab` 序列化后的内容,注意到大部分数字是 0,从 24 字节开始的 4 个字节 `da 9f 20 d4` 实际上是 `itab` 的 `hash` 值,这在判断两个类型是否相同的时候会用到。 126 | 127 | 下面两行是链接指令,简单说就是将所有源文件综合起来,给每个符号赋予一个全局的位置值。这里的意思也比较明确:前8个字节最终存储的是 `type."".Person` 的地址,对应 `itab` 里的 `inter` 字段,表示接口类型;8-16 字节最终存储的是 `type."".Student` 的地址,对应 `itab` 里 `_type` 字段,表示具体类型。 128 | 129 | 第二个参数就比较简单了,它就是数字 `18` 的地址,这也是初始化 `Student` 结构体的时候会用到。 130 | 131 | |汇编行数|操作| 132 | |---|---| 133 | |15|调用 `runtime.convT2I64(SB)`| 134 | 135 | 具体看下代码: 136 | 137 | ```golang 138 | func convT2I64(tab *itab, elem unsafe.Pointer) (i iface) { 139 | t := tab._type 140 | 141 | //... 142 | 143 | var x unsafe.Pointer 144 | if *(*uint64)(elem) == 0 { 145 | x = unsafe.Pointer(&zeroVal[0]) 146 | } else { 147 | x = mallocgc(8, t, false) 148 | *(*uint64)(x) = *(*uint64)(elem) 149 | } 150 | i.tab = tab 151 | i.data = x 152 | return 153 | } 154 | ``` 155 | 156 | 这块代码比较简单,把 `tab` 赋给了 `iface` 的 `tab` 字段;`data` 部分则是在堆上申请了一块内存,然后将 `elem` 指向的 `18` 拷贝过去。这样 `iface` 就组装好了。 157 | 158 | |汇编行数|操作| 159 | |---|---| 160 | |17|把 `i.tab` 赋给 `CX`| 161 | |18|把 `i.data` 赋给 `AX`| 162 | |19-21|检测 `i.tab` 是否是 nil,如果不是的话,把 CX 移动 8 个字节,也就是把 `itab` 的 `_type` 字段赋给了 CX,这也是接口的实体类型,最终要作为 `fmt.Println` 函数的参数| 163 | 164 | 后面,就是调用 `fmt.Println` 函数及之前的参数准备工作了,不再赘述。 165 | 166 | 这样,我们就把一个 `interface` 的构造过程说完了。 167 | 168 | 【引申1】 169 | 如何打印出接口类型的 `Hash` 值? 170 | 171 | 这里参考曹大神翻译的一篇文章,参考资料里会写上。具体做法如下: 172 | 173 | ```golang 174 | type iface struct { 175 | tab *itab 176 | data unsafe.Pointer 177 | } 178 | type itab struct { 179 | inter uintptr 180 | _type uintptr 181 | link uintptr 182 | hash uint32 183 | _ [4]byte 184 | fun [1]uintptr 185 | } 186 | 187 | func main() { 188 | var qcrao = Person(Student{age: 18}) 189 | 190 | iface := (*iface)(unsafe.Pointer(&qcrao)) 191 | fmt.Printf("iface.tab.hash = %#x\n", iface.tab.hash) 192 | } 193 | ``` 194 | 195 | 定义了一个`山寨版`的 `iface` 和 `itab`,说它`山寨`是因为 `itab` 里的一些关键数据结构都不具体展开了,比如 `_type`,对比一下正宗的定义就可以发现,但是`山寨版`依然能工作,因为 `_type` 就是一个指针而已嘛。 196 | 197 | 在 `main` 函数里,先构造出一个接口对象 `qcrao`,然后强制类型转换,最后读取出 `hash` 值,非常妙!你也可以自己动手试一下。 198 | 199 | 运行结果: 200 | 201 | ```shell 202 | iface.tab.hash = 0xd4209fda 203 | ``` 204 | 205 | 值得一提的是,构造接口 `qcrao` 的时候,即使我把 `age` 写成其他值,得到的 `hash` 值依然不变的,这应该是可以预料的,`hash` 值只和他的字段、方法相关。 206 | 207 | # 参考资料 208 | 【曹大神翻译的文章,非常硬核】http://xargin.com/go-and-interface/#reconstructing-an-itab-from-an-executable -------------------------------------------------------------------------------- /interface/接口转换的原理.md: -------------------------------------------------------------------------------- 1 | 通过前面提到的 `iface` 的源码可以看到,实际上它包含接口的类型 `interfacetype` 和 实体类型的类型 `_type`,这两者都是 `iface` 的字段 `itab` 的成员。也就是说生成一个 `itab` 同时需要接口的类型和实体的类型。 2 | 3 | > ->itable 4 | 5 | 当判定一种类型是否满足某个接口时,Go 使用类型的方法集和接口所需要的方法集进行匹配,如果类型的方法集完全包含接口的方法集,则可认为该类型实现了该接口。 6 | 7 | 例如某类型有 `m` 个方法,某接口有 `n` 个方法,则很容易知道这种判定的时间复杂度为 `O(mn)`,Go 会对方法集的函数按照函数名的字典序进行排序,所以实际的时间复杂度为 `O(m+n)`。 8 | 9 | 这里我们来探索将一个接口转换给另外一个接口背后的原理,当然,能转换的原因必然是类型兼容。 10 | 11 | 直接来看一个例子: 12 | 13 | ```golang 14 | package main 15 | 16 | import "fmt" 17 | 18 | type coder interface { 19 | code() 20 | run() 21 | } 22 | 23 | type runner interface { 24 | run() 25 | } 26 | 27 | type Gopher struct { 28 | language string 29 | } 30 | 31 | func (g Gopher) code() { 32 | return 33 | } 34 | 35 | func (g Gopher) run() { 36 | return 37 | } 38 | 39 | func main() { 40 | var c coder = Gopher{} 41 | 42 | var r runner 43 | r = c 44 | fmt.Println(c, r) 45 | } 46 | ``` 47 | 48 | 简单解释下上述代码:定义了两个 `interface`: `coder` 和 `runner`。定义了一个实体类型 `Gopher`,类型 `Gopher` 实现了两个方法,分别是 `run()` 和 `code()`。main 函数里定义了一个接口变量 `c`,绑定了一个 `Gopher` 对象,之后将 `c` 赋值给另外一个接口变量 `r` 。赋值成功的原因是 `c` 中包含 `run()` 方法。这样,两个接口变量完成了转换。 49 | 50 | 执行命令: 51 | 52 | ```shell 53 | go tool compile -S ./src/main.go 54 | ``` 55 | 56 | 得到 main 函数的汇编命令,可以看到: `r = c` 这一行语句实际上是调用了 `runtime.convI2I(SB)`,也就是 `convI2I` 函数,从函数名来看,就是将一个 `interface` 转换成另外一个 `interface`,看下它的源代码: 57 | 58 | ```golang 59 | func convI2I(inter *interfacetype, i iface) (r iface) { 60 | tab := i.tab 61 | if tab == nil { 62 | return 63 | } 64 | if tab.inter == inter { 65 | r.tab = tab 66 | r.data = i.data 67 | return 68 | } 69 | r.tab = getitab(inter, tab._type, false) 70 | r.data = i.data 71 | return 72 | } 73 | ``` 74 | 75 | 代码比较简单,函数参数 `inter` 表示接口类型,`i` 表示绑定了实体类型的接口,`r` 则表示接口转换了之后的新的 `iface`。通过前面的分析,我们又知道, `iface` 是由 `tab` 和 `data` 两个字段组成。所以,实际上 `convI2I` 函数真正要做的事,找到新 `interface` 的 `tab` 和 `data`,就大功告成了。 76 | 77 | 我们还知道,`tab` 是由接口类型 `interfacetype` 和 实体类型 `_type`。所以最关键的语句是 `r.tab = getitab(inter, tab._type, false)`。 78 | 79 | 因此,重点来看下 `getitab` 函数的源码,只看关键的地方: 80 | 81 | ```golang 82 | func getitab(inter *interfacetype, typ *_type, canfail bool) *itab { 83 | // …… 84 | 85 | // 根据 inter, typ 计算出 hash 值 86 | h := itabhash(inter, typ) 87 | 88 | // look twice - once without lock, once with. 89 | // common case will be no lock contention. 90 | var m *itab 91 | var locked int 92 | for locked = 0; locked < 2; locked++ { 93 | if locked != 0 { 94 | lock(&ifaceLock) 95 | } 96 | 97 | // 遍历哈希表的一个 slot 98 | for m = (*itab)(atomic.Loadp(unsafe.Pointer(&hash[h]))); m != nil; m = m.link { 99 | 100 | // 如果在 hash 表中已经找到了 itab(inter 和 typ 指针都相同) 101 | if m.inter == inter && m._type == typ { 102 | // …… 103 | 104 | if locked != 0 { 105 | unlock(&ifaceLock) 106 | } 107 | return m 108 | } 109 | } 110 | } 111 | 112 | // 在 hash 表中没有找到 itab,那么新生成一个 itab 113 | m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys)) 114 | m.inter = inter 115 | m._type = typ 116 | 117 | // 添加到全局的 hash 表中 118 | additab(m, true, canfail) 119 | unlock(&ifaceLock) 120 | if m.bad { 121 | return nil 122 | } 123 | return m 124 | } 125 | ``` 126 | 127 | 简单总结一下:getitab 函数会根据 `interfacetype` 和 `_type` 去全局的 itab 哈希表中查找,如果能找到,则直接返回;否则,会根据给定的 `interfacetype` 和 `_type` 新生成一个 `itab`,并插入到 itab 哈希表,这样下一次就可以直接拿到 `itab`。 128 | 129 | 这里查找了两次,并且第二次上锁了,这是因为如果第一次没找到,在第二次仍然没有找到相应的 `itab` 的情况下,需要新生成一个,并且写入哈希表,因此需要加锁。这样,其他协程在查找相同的 `itab` 并且也没有找到时,第二次查找时,会被挂住,之后,就会查到第一个协程写入哈希表的 `itab`。 130 | 131 | 再来看一下 `additab` 函数的代码: 132 | 133 | ```golang 134 | // 检查 _type 是否符合 interface_type 并且创建对应的 itab 结构体 将其放到 hash 表中 135 | func additab(m *itab, locked, canfail bool) { 136 | inter := m.inter 137 | typ := m._type 138 | x := typ.uncommon() 139 | 140 | // both inter and typ have method sorted by name, 141 | // and interface names are unique, 142 | // so can iterate over both in lock step; 143 | // the loop is O(ni+nt) not O(ni*nt). 144 | // 145 | // inter 和 typ 的方法都按方法名称进行了排序 146 | // 并且方法名都是唯一的。所以循环的次数是固定的 147 | // 只用循环 O(ni+nt),而非 O(ni*nt) 148 | ni := len(inter.mhdr) 149 | nt := int(x.mcount) 150 | xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt] 151 | j := 0 152 | for k := 0; k < ni; k++ { 153 | i := &inter.mhdr[k] 154 | itype := inter.typ.typeOff(i.ityp) 155 | name := inter.typ.nameOff(i.name) 156 | iname := name.name() 157 | ipkg := name.pkgPath() 158 | if ipkg == "" { 159 | ipkg = inter.pkgpath.name() 160 | } 161 | for ; j < nt; j++ { 162 | t := &xmhdr[j] 163 | tname := typ.nameOff(t.name) 164 | // 检查方法名字是否一致 165 | if typ.typeOff(t.mtyp) == itype && tname.name() == iname { 166 | pkgPath := tname.pkgPath() 167 | if pkgPath == "" { 168 | pkgPath = typ.nameOff(x.pkgpath).name() 169 | } 170 | if tname.isExported() || pkgPath == ipkg { 171 | if m != nil { 172 | // 获取函数地址,并加入到itab.fun数组中 173 | ifn := typ.textOff(t.ifn) 174 | *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn 175 | } 176 | goto nextimethod 177 | } 178 | } 179 | } 180 | // …… 181 | 182 | m.bad = true 183 | break 184 | nextimethod: 185 | } 186 | if !locked { 187 | throw("invalid itab locking") 188 | } 189 | 190 | // 计算 hash 值 191 | h := itabhash(inter, typ) 192 | // 加到Hash Slot链表中 193 | m.link = hash[h] 194 | m.inhash = true 195 | atomicstorep(unsafe.Pointer(&hash[h]), unsafe.Pointer(m)) 196 | } 197 | ``` 198 | 199 | `additab` 会检查 `itab` 持有的 `interfacetype` 和 `_type` 是否符合,就是看 `_type` 是否完全实现了 `interfacetype` 的方法,也就是看两者的方法列表重叠的部分就是 `interfacetype` 所持有的方法列表。注意到其中有一个双层循环,乍一看,循环次数是 `ni * nt`,但由于两者的函数列表都按照函数名称进行了排序,因此最终只执行了 `ni + nt` 次,代码里通过一个小技巧来实现:第二层循环并没有从 0 开始计数,而是从上一次遍历到的位置开始。 200 | 201 | 求 hash 值的函数比较简单: 202 | 203 | ```golang 204 | func itabhash(inter *interfacetype, typ *_type) uint32 { 205 | h := inter.typ.hash 206 | h += 17 * typ.hash 207 | return h % hashSize 208 | } 209 | ``` 210 | 211 | `hashSize` 的值是 1009。 212 | 213 | 更一般的,当把实体类型赋值给接口的时候,会调用 `conv` 系列函数,例如空接口调用 `convT2E` 系列、非空接口调用 `convT2I` 系列。这些函数比较相似: 214 | 215 | > 1. 具体类型转空接口时,_type 字段直接复制源类型的 _type;调用 mallocgc 获得一块新内存,把值复制进去,data 再指向这块新内存。 216 | > 2. 具体类型转非空接口时,入参 tab 是编译器在编译阶段预先生成好的,新接口 tab 字段直接指向入参 tab 指向的 itab;调用 mallocgc 获得一块新内存,把值复制进去,data 再指向这块新内存。 217 | > 3. 而对于接口转接口,itab 调用 getitab 函数获取。只用生成一次,之后直接从 hash 表中获取。 218 | 219 | # 参考资料 220 | 【接口赋值、反射】http://wudaijun.com/2018/01/go-interface-implement/ 221 | 222 | 【itab】http://legendtkl.com/2017/07/01/golang-interface-implement/ 223 | 224 | 【和 C++ 的对比】https://www.jianshu.com/p/b38b1719636e 225 | 226 | 【itab 原理】https://ninokop.github.io/2017/10/29/Go-%E6%96%B9%E6%B3%95%E8%B0%83%E7%94%A8%E4%B8%8E%E6%8E%A5%E5%8F%A3/ 227 | 228 | 【getitab源码说明】https://www.twblogs.net/a/5c245d59bd9eee16b3db561d -------------------------------------------------------------------------------- /标准库/context/context 有什么作用.md: -------------------------------------------------------------------------------- 1 | Go 常用来写后台服务,通常只需要几行代码,就可以搭建一个 http server。 2 | 3 | 在 Go 的 server 里,通常每来一个请求都会启动若干个 goroutine 同时工作:有些去数据库拿数据,有些调用下游接口获取相关数据…… 4 | 5 | ![request](https://user-images.githubusercontent.com/7698088/59235934-643ee480-8c26-11e9-8931-456333900657.png) 6 | 7 | 这些 goroutine 需要共享这个请求的基本数据,例如登陆的 token,处理请求的最大超时时间(如果超过此值再返回数据,请求方因为超时接收不到)等等。当请求被取消或是处理时间太长,这有可能是使用者关闭了浏览器或是已经超过了请求方规定的超时时间,请求方直接放弃了这次请求结果。这时,所有正在为这个请求工作的 goroutine 需要快速退出,因为它们的“工作成果”不再被需要了。在相关联的 goroutine 都退出后,系统就可以回收相关的资源。 8 | 9 | 再多说一点,Go 语言中的 server 实际上是一个“协程模型”,也就是说一个协程处理一个请求。例如在业务的高峰期,某个下游服务的响应变慢,而当前系统的请求又没有超时控制,或者超时时间设置地过大,那么等待下游服务返回数据的协程就会越来越多。而我们知道,协程是要消耗系统资源的,后果就是协程数激增,内存占用飙涨,甚至导致服务不可用。更严重的会导致雪崩效应,整个服务对外表现为不可用,这肯定是 P0 级别的事故。这时,肯定有人要背锅了。 10 | 11 | 其实前面描述的 P0 级别事故,通过设置“允许下游最长处理时间”就可以避免。例如,给下游设置的 timeout 是 50 ms,如果超过这个值还没有接收到返回数据,就直接向客户端返回一个默认值或者错误。例如,返回商品的一个默认库存数量。注意,这里设置的超时时间和创建一个 http client 设置的读写超时时间不一样,这里不详细展开。可以去看看参考资料`【Go 在今日头条的实践】`一文,有很精彩的论述。 12 | 13 | context 包就是为了解决上面所说的这些问题而开发的:在 一组 goroutine 之间传递共享的值、取消信号、deadline…… 14 | 15 | ![request with context](https://user-images.githubusercontent.com/7698088/59235969-a405cc00-8c26-11e9-9448-2c6c86e8263b.png) 16 | 17 | 用简练一些的话来说,在Go 里,我们不能直接杀死协程,协程的关闭一般会用 `channel+select` 方式来控制。但是在某些场景下,例如处理一个请求衍生了很多协程,这些协程之间是相互关联的:需要共享一些全局变量、有共同的 deadline 等,而且可以同时被关闭。再用 `channel+select` 就会比较麻烦,这时就可以通过 context 来实现。 18 | 19 | 一句话:context 用来解决 goroutine 之间`退出通知`、`元数据传递`的功能。 20 | 21 | 【引申1】举例说明 context 在实际项目中如何使用。 22 | 23 | context 使用起来非常方便。源码里对外提供了一个创建根节点 context 的函数: 24 | 25 | ```golang 26 | func Background() Context 27 | ``` 28 | 29 | background 是一个空的 context, 它不能被取消,没有值,也没有超时时间。 30 | 31 | 有了根节点 context,又提供了四个函数创建子节点 context: 32 | 33 | ```golang 34 | func WithCancel(parent Context) (ctx Context, cancel CancelFunc) 35 | func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) 36 | func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) 37 | func WithValue(parent Context, key, val interface{}) Context 38 | ``` 39 | 40 | context 会在函数传递间传递。只需要在适当的时间调用 cancel 函数向 goroutines 发出取消信号或者调用 Value 函数取出 context 中的值。 41 | 42 | 在官方博客里,对于使用 context 提出了几点建议: 43 | 44 | 1. Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx. 45 | 2. Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use. 46 | 3. Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions. 47 | 4. The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines. 48 | 49 | 我翻译一下: 50 | 51 | 1. 不要将 Context 塞到结构体里。直接将 Context 类型作为函数的第一参数,而且一般都命名为 ctx。 52 | 2. 不要向函数传入一个 nil 的 context,如果你实在不知道传什么,标准库给你准备好了一个 context:todo。 53 | 3. 不要把本应该作为函数参数的类型塞到 context 中,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。 54 | 4. 同一个 context 可能会被传递到多个 goroutine,别担心,context 是并发安全的。 55 | 56 | # 传递共享的数据 57 | 58 | 对于 Web 服务端开发,往往希望将一个请求处理的整个过程串起来,这就非常依赖于 Thread Local(对于 Go 可理解为单个协程所独有) 的变量,而在 Go 语言中并没有这个概念,因此需要在函数调用的时候传递 context。 59 | 60 | ```golang 61 | package main 62 | 63 | import ( 64 | "context" 65 | "fmt" 66 | ) 67 | 68 | func main() { 69 | ctx := context.Background() 70 | process(ctx) 71 | 72 | ctx = context.WithValue(ctx, "traceId", "qcrao-2019") 73 | process(ctx) 74 | } 75 | 76 | func process(ctx context.Context) { 77 | traceId, ok := ctx.Value("traceId").(string) 78 | if ok { 79 | fmt.Printf("process over. trace_id=%s\n", traceId) 80 | } else { 81 | fmt.Printf("process over. no trace_id\n") 82 | } 83 | } 84 | ``` 85 | 86 | 运行结果: 87 | 88 | ```shell 89 | process over. no trace_id 90 | process over. trace_id=qcrao-2019 91 | ``` 92 | 93 | 第一次调用 process 函数时,ctx 是一个空的 context,自然取不出来 traceId。第二次,通过 `WithValue` 函数创建了一个 context,并赋上了 `traceId` 这个 key,自然就能取出来传入的 value 值。 94 | 95 | 当然,现实场景中可能是从一个 HTTP 请求中获取到的 Request-ID。所以,下面这个样例可能更适合: 96 | 97 | ```golang 98 | const requestIDKey int = 0 99 | 100 | func WithRequestID(next http.Handler) http.Handler { 101 | return http.HandlerFunc( 102 | func(rw http.ResponseWriter, req *http.Request) { 103 | // 从 header 中提取 request-id 104 | reqID := req.Header.Get("X-Request-ID") 105 | // 创建 valueCtx。使用自定义的类型,不容易冲突 106 | ctx := context.WithValue( 107 | req.Context(), requestIDKey, reqID) 108 | 109 | // 创建新的请求 110 | req = req.WithContext(ctx) 111 | 112 | // 调用 HTTP 处理函数 113 | next.ServeHTTP(rw, req) 114 | } 115 | ) 116 | } 117 | 118 | // 获取 request-id 119 | func GetRequestID(ctx context.Context) string { 120 | ctx.Value(requestIDKey).(string) 121 | } 122 | 123 | func Handle(rw http.ResponseWriter, req *http.Request) { 124 | // 拿到 reqId,后面可以记录日志等等 125 | reqID := GetRequestID(req.Context()) 126 | ... 127 | } 128 | 129 | func main() { 130 | handler := WithRequestID(http.HandlerFunc(Handle)) 131 | http.ListenAndServe("/", handler) 132 | } 133 | ``` 134 | 135 | # 取消 goroutine 136 | 137 | 我们先来设想一个场景:打开外卖的订单页,地图上显示外卖小哥的位置,而且是每秒更新 1 次。app 端向后台发起 websocket 连接(现实中可能是轮询)请求后,后台启动一个协程,每隔 1 秒计算 1 次小哥的位置,并发送给端。如果用户退出此页面,则后台需要“取消”此过程,退出 goroutine,系统回收资源。 138 | 139 | 后端可能的实现如下: 140 | 141 | ```golang 142 | func Perform() { 143 | for { 144 | calculatePos() 145 | sendResult() 146 | time.Sleep(time.Second) 147 | } 148 | } 149 | ``` 150 | 151 | 如果需要实现“取消”功能,并且在不了解 context 功能的前提下,可能会这样做:给函数增加一个指针型的 bool 变量,在 for 语句的开始处判断 bool 变量是发由 true 变为 false,如果改变,则退出循环。 152 | 153 | 上面给出的简单做法,可以实现想要的效果,没有问题,但是并不优雅,并且一旦协程数量多了之后,并且各种嵌套,就会很麻烦。优雅的做法,自然就要用到 context。 154 | 155 | ```golang 156 | func Perform(ctx context.Context) { 157 | for { 158 | calculatePos() 159 | sendResult() 160 | 161 | select { 162 | case <-ctx.Done(): 163 | // 被取消,直接返回 164 | return 165 | case <-time.After(time.Second): 166 | // block 1 秒钟 167 | } 168 | } 169 | } 170 | ``` 171 | 172 | 主流程可能是这样的: 173 | 174 | ```golang 175 | ctx, cancel := context.WithTimeout(context.Background(), time.Hour) 176 | go Perform(ctx) 177 | 178 | // …… 179 | // app 端返回页面,调用cancel 函数 180 | cancel() 181 | ``` 182 | 183 | 注意一个细节,WithTimeOut 函数返回的 context 和 cancelFun 是分开的。context 本身并没有取消函数,这样做的原因是取消函数只能由外层函数调用,防止子节点 context 调用取消函数,从而严格控制信息的流向:由父节点 context 流向子节点 context。 184 | 185 | # 防止 goroutine 泄漏 186 | 前面那个例子里,goroutine 还是会自己执行完,最后返回,只不过会多浪费一些系统资源。这里改编一个“如果不用 context 取消,goroutine 就会泄漏的例子”,来自参考资料:`【避免协程泄漏】`。 187 | 188 | ```golang 189 | func gen() <-chan int { 190 | ch := make(chan int) 191 | go func() { 192 | var n int 193 | for { 194 | ch <- n 195 | n++ 196 | time.Sleep(time.Second) 197 | } 198 | }() 199 | return ch 200 | } 201 | ``` 202 | 203 | 这是一个可以生成无限整数的协程,但如果我只需要它产生的前 5 个数,那么就会发生 goroutine 泄漏: 204 | 205 | ```golang 206 | func main() { 207 | for n := range gen() { 208 | fmt.Println(n) 209 | if n == 5 { 210 | break 211 | } 212 | } 213 | // …… 214 | } 215 | ``` 216 | 217 | 当 n == 5 的时候,直接 break 掉。那么 gen 函数的协程就会执行无限循环,永远不会停下来。发生了 goroutine 泄漏。 218 | 219 | 用 context 改进这个例子: 220 | 221 | ```golang 222 | func gen(ctx context.Context) <-chan int { 223 | ch := make(chan int) 224 | go func() { 225 | var n int 226 | for { 227 | select { 228 | case <-ctx.Done(): 229 | return 230 | case ch <- n: 231 | n++ 232 | time.Sleep(time.Second) 233 | } 234 | } 235 | }() 236 | return ch 237 | } 238 | 239 | func main() { 240 | ctx, cancel := context.WithCancel(context.Background()) 241 | defer cancel() // 避免其他地方忘记 cancel,且重复调用不影响 242 | 243 | for n := range gen(ctx) { 244 | fmt.Println(n) 245 | if n == 5 { 246 | cancel() 247 | break 248 | } 249 | } 250 | // …… 251 | } 252 | ``` 253 | 254 | 增加一个 context,在 break 前调用 cancel 函数,取消 goroutine。gen 函数在接收到取消信号后,直接退出,系统回收资源。 -------------------------------------------------------------------------------- /goroutine 调度器/mian gorutine 如何创建.md: -------------------------------------------------------------------------------- 1 | 上一讲我们讲完了 Go scheduler 的初始化,现在调度器一切就绪,就差被调度的实体了。本文就来讲述 main goroutine 是如何诞生,并且被调度的。 2 | 3 | 继续看代码,前面我们完成了 `schedinit` 函数,这是 runtime·rt0_go 函数里的一步,接着往后看: 4 | 5 | ```asm 6 | // 创建一个新的 goroutine 来启动程序 7 | MOVQ $runtime·mainPC(SB), AX // entry 8 | // newproc 的第二个参数入栈,也就是新的 goroutine 需要执行的函数 9 | // AX = &funcval{runtime·main}, 10 | PUSHQ AX 11 | // newproc 的第一个参数入栈,该参数表示 runtime.main 函数需要的参数大小, 12 | // 因为 runtime.main 没有参数,所以这里是 0 13 | PUSHQ $0 // arg size 14 | // 创建 main goroutine 15 | CALL runtime·newproc(SB) 16 | POPQ AX 17 | POPQ AX 18 | 19 | // start this M 20 | // 主线程进入调度循环,运行刚刚创建的 goroutine 21 | CALL runtime·mstart(SB) 22 | 23 | // 永远不会返回,万一返回了,crash 掉 24 | MOVL $0xf1, 0xf1 // crash 25 | RET 26 | ``` 27 | 28 | 代码前面几行是在为调用 newproc 函数构“造栈”,执行完 `runtime·newproc(SB)` 后,就会以一个新的 goroutine 来执行 mainPC 也就是 `runtime.main()` 函数。`runtime.main()` 函数最终会执行到我们写的 main 函数,舞台交给我们。 29 | 30 | 重点来看 `newproc` 函数: 31 | 32 | ```golang 33 | // src/runtime/proc.go 34 | // 创建一个新的 g,运行 fn 函数,需要 siz byte 的参数 35 | // 将其放至 G 队列等待运行 36 | // 编译器会将 go 关键字的语句转化成此函数 37 | 38 | //go:nosplit 39 | func newproc(siz int32, fn *funcval) 40 | ``` 41 | 42 | 从这里开始要进入 hard 模式了,打起精神!当我们随手一句: 43 | 44 | ```golang 45 | go func() { 46 | // 要做的事 47 | }() 48 | ``` 49 | 50 | 就启动了一个 goroutine 的时候,一定要知道,在 Go 编译器的作用下,这条语句最终会转化成 newproc 函数。 51 | 52 | 因此,`newproc` 函数需要两个参数:一个是新创建的 goroutine 需要执行的任务,也就是 fn,它代表一个函数 func;还有一个是 fn 的参数大小。 53 | 54 | 再回过头看,构造 newproc 函数调用栈的时候,第一个参数是 0,因为 runtime.main 函数没有参数: 55 | 56 | ```golang 57 | // src/runtime/proc.go 58 | 59 | func main() 60 | ``` 61 | 62 | 第二个参数则是 runtime.main 函数的地址。 63 | 64 | 可能会感到奇怪,为什么要给 `newproc` 传一个表示 fn 的参数大小的参数呢? 65 | 66 | 我们知道,goroutine 和线程一样,都有自己的栈,不同的是 goroutine 的初始栈比较小,只有 2K,而且是可伸缩的,这也是创建 goroutine 的代价比创建线程代价小的原因。 67 | 68 | 换句话说,每个 goroutine 都有自己的栈空间,newproc 函数会新创建一个新的 goroutine 来执行 fn 函数,在新 goroutine 上执行指令,就要用新 goroutine 的栈。而执行函数需要参数,这个参数又是在老的 goroutine 上,所以需要将其拷贝到新 goroutine 的栈上。拷贝的起始位置就是栈顶,这好办,那拷贝多少数据呢?由 siz 来确定。 69 | 70 | 继续看代码,newproc 函数的第二个参数: 71 | 72 | ```golang 73 | type funcval struct { 74 | fn uintptr 75 | // variable-size, fn-specific data here 76 | } 77 | ``` 78 | 79 | 它是一个变长结构,第一个字段是一个指针 fn,内存中,紧挨着 fn 的是函数的参数。 80 | 81 | 参考资料【欧神 关键字 go】有一个例子: 82 | 83 | ```golang 84 | package main 85 | 86 | func hello(msg string) { 87 | println(msg) 88 | } 89 | 90 | func main() { 91 | go hello("hello world") 92 | } 93 | ``` 94 | 95 | 栈布局是这样的: 96 | 97 | ![fn 与函数参数](https://user-images.githubusercontent.com/7698088/63561049-0cf4b300-c58b-11e9-8745-57d7dadf0a87.png) 98 | 99 | 栈顶是 siz,再往上是函数的地址,再往上就是传给 hello 函数的参数,string 在这里是一个地址。因此前面代码里先 push 参数的地址,再 push 参数大小。 100 | 101 | ```golang 102 | // src/runtime/proc.go 103 | 104 | //go:nosplit 105 | func newproc(siz int32, fn *funcval) { 106 | // 获取第一个参数地址 107 | argp := add(unsafe.Pointer(&fn), sys.PtrSize) 108 | // 获取调用者的指令地址,也就是调用 newproc 时由 call 指令压栈的函数返回地址 109 | pc := getcallerpc(unsafe.Pointer(&siz)) 110 | // systemstack 的作用是切换到 g0 栈执行作为参数的函数 111 | // 用 g0 系统栈创建 goroutine 对象 112 | // 传递的参数包括 fn 函数入口地址,argp 参数起始地址,siz 参数长度,调用方 pc(goroutine) 113 | 114 | systemstack(func() { 115 | newproc1(fn, (*uint8)(argp), siz, 0, pc) 116 | }) 117 | } 118 | ``` 119 | 120 | 因此,argp 跳过 fn,向上跳一个指针的长度,拿到 fn 参数的地址。 121 | 122 | 接着通过 getcallerpc 获取调用者的指令地址,也就是调用 newproc 时由 call 指令压栈的函数返回地址,也就是 `runtime·rt0_go` 函数里 `CALL runtime·newproc(SB)` 指令后面的 `POPQ AX` 这条指令的地址。 123 | 124 | 最后,调用 systemstack 函数在 g0 栈执行 fn 函数。由于本文讲述的是初始化过程中,由 `runtime·rt0_go` 函数调用,本身是在 g0 栈执行,因此会直接执行 fn 函数。而如果是我们在程序中写的 `go xxx` 代码,在执行时,就会先切换到 g0 栈执行,然后再切回来。 125 | 126 | 一鼓作气,继续看 `newproc1` 函数,为了连贯性,我先将整个函数的代码贴出来,并且加上了注释。当然,这篇文章不会涉及到所有的代码,只会讲部分内容。放在这里,方便阅读后面的文章时对照: 127 | 128 | ```golang 129 | // 创建一个新的 g 来跑 fn 130 | func newproc1(fn *funcval, argp *uint8, narg int32, nret int32, callerpc uintptr) *g { 131 | // 当前 goroutine 的指针 132 | // 因为已经切换到 g0 栈,所以无论什么场景都是 _g_ = g0 133 | // g0 是指当前工作线程的 g0 134 | _g_ := getg() 135 | 136 | if fn == nil { 137 | _g_.m.throwing = -1 // do not dump full stacks 138 | throw("go of nil func value") 139 | } 140 | _g_.m.locks++ // disable preemption because it can be holding p in a local var 141 | 142 | // 参数加返回值所需要的空间(经过内存对齐) 143 | siz := narg + nret 144 | siz = (siz + 7) &^ 7 145 | 146 | // ………………………… 147 | 148 | // 当前工作线程所绑定的 p 149 | // 初始化时 _p_ = g0.m.p,也就是 _p_ = allp[0] 150 | _p_ := _g_.m.p.ptr() 151 | // 从 p 的本地缓冲里获取一个没有使用的 g,初始化时为空,返回 nil 152 | newg := gfget(_p_) 153 | if newg == nil { 154 | // new 一个 g 结构体对象,然后从堆上为其分配栈,并设置 g 的 stack 成员和两个 stackgard 成员 155 | newg = malg(_StackMin) 156 | // 初始化 g 的状态为 _Gdead 157 | casgstatus(newg, _Gidle, _Gdead) 158 | // 放入全局变量 allgs 切片中 159 | allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack. 160 | } 161 | if newg.stack.hi == 0 { 162 | throw("newproc1: newg missing stack") 163 | } 164 | 165 | if readgstatus(newg) != _Gdead { 166 | throw("newproc1: new g is not Gdead") 167 | } 168 | 169 | // 计算运行空间大小,对齐 170 | totalSize := 4*sys.RegSize + uintptr(siz) + sys.MinFrameSize // extra space in case of reads slightly beyond frame 171 | totalSize += -totalSize & (sys.SpAlign - 1) // align to spAlign 172 | // 确定 sp 位置 173 | sp := newg.stack.hi - totalSize 174 | // 确定参数入栈位置 175 | spArg := sp 176 | 177 | // ………………………… 178 | 179 | if narg > 0 { 180 | // 将参数从执行 newproc 函数的栈拷贝到新 g 的栈 181 | memmove(unsafe.Pointer(spArg), unsafe.Pointer(argp), uintptr(narg)) 182 | 183 | // ………………………… 184 | } 185 | 186 | // 把 newg.sched 结构体成员的所有成员设置为 0 187 | memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched)) 188 | // 设置 newg 的 sched 成员,调度器需要依靠这些字段才能把 goroutine 调度到 CPU 上运行 189 | newg.sched.sp = sp 190 | newg.stktopsp = sp 191 | // newg.sched.pc 表示当 newg 被调度起来运行时从这个地址开始执行指令 192 | newg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function 193 | newg.sched.g = guintptr(unsafe.Pointer(newg)) 194 | gostartcallfn(&newg.sched, fn) 195 | newg.gopc = callerpc 196 | // 设置 newg 的 startpc 为 fn.fn,该成员主要用于函数调用栈的 traceback 和栈收缩 197 | // newg 真正从哪里开始执行并不依赖于这个成员,而是 sched.pc 198 | newg.startpc = fn.fn 199 | if _g_.m.curg != nil { 200 | newg.labels = _g_.m.curg.labels 201 | } 202 | if isSystemGoroutine(newg) { 203 | atomic.Xadd(&sched.ngsys, +1) 204 | } 205 | newg.gcscanvalid = false 206 | // 设置 g 的状态为 _Grunnable,可以运行了 207 | casgstatus(newg, _Gdead, _Grunnable) 208 | 209 | if _p_.goidcache == _p_.goidcacheend { 210 | _p_.goidcache = atomic.Xadd64(&sched.goidgen, _GoidCacheBatch) 211 | _p_.goidcache -= _GoidCacheBatch - 1 212 | _p_.goidcacheend = _p_.goidcache + _GoidCacheBatch 213 | } 214 | // 设置 goid 215 | newg.goid = int64(_p_.goidcache) 216 | _p_.goidcache++ 217 | 218 | // …………………… 219 | 220 | // 将 G 放入 _p_ 的本地待运行队列 221 | runqput(_p_, newg, true) 222 | 223 | if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted { 224 | wakep() 225 | } 226 | _g_.m.locks-- 227 | if _g_.m.locks == 0 && _g_.preempt { 228 | _g_.stackguard0 = stackPreempt 229 | } 230 | return newg 231 | } 232 | ``` 233 | 234 | 当前代码在 g0 栈上执行,因此执行完 `_g_ := getg()` 之后,无论是在什么情况下都可以得到 `_g_ = g0`。之后通过 g0 找到其绑定的 P,也就是 p0。 235 | 236 | 接着,尝试从 p0 上找一个空闲的 G: 237 | 238 | ```golang 239 | // 从 p 的本地缓冲里获取一个没有使用的 g,初始化时为空,返回 nil 240 | newg := gfget(_p_) 241 | ``` 242 | 243 | 如果拿不到,则会在堆上创建一个新的 G,为其分配 2KB 大小的栈,并设置好新 goroutine 的 stack 成员,设置其状态为 _Gdead,并将其添加到全局变量 allgs 中。创建完成之后,我们就在堆上有了一个 2K 大小的栈。于是,我们的图再次丰富: 244 | 245 | ![创建了新的 goroutine](https://user-images.githubusercontent.com/7698088/64071207-1ecf0800-cca7-11e9-874f-a907e272581c.png) 246 | 247 | 这样,main goroutine 就诞生了。 248 | 249 | # 参考资料 250 | 251 | 【欧神 关键字 go】https://github.com/changkun/go-under-the-hood/blob/master/book/zh-cn/part3compile/ch11keyword/go.md 252 | 253 | 【欧神 Go scheduler】https://github.com/changkun/go-under-the-hood/blob/master/book/zh-cn/part2runtime/ch06sched/init.md -------------------------------------------------------------------------------- /数组和切片/切片的容量是怎样增长的.md: -------------------------------------------------------------------------------- 1 | 一般都是在向 slice 追加了元素之后,才会引起扩容。追加元素调用的是 `append` 函数。 2 | 3 | 先来看看 `append` 函数的原型: 4 | ```golang 5 | func append(slice []Type, elems ...Type) []Type 6 | ``` 7 | 8 | append 函数的参数长度可变,因此可以追加多个值到 slice 中,还可以用 `...` 传入 slice,直接追加一个切片。 9 | 10 | ```golang 11 | slice = append(slice, elem1, elem2) 12 | slice = append(slice, anotherSlice...) 13 | ``` 14 | 15 | `append`函数返回值是一个新的slice,Go编译器不允许调用了 append 函数后不使用返回值。 16 | 17 | ```golang 18 | append(slice, elem1, elem2) 19 | append(slice, anotherSlice...) 20 | ``` 21 | 22 | 所以上面的用法是错的,不能编译通过。 23 | 24 | 使用 append 可以向 slice 追加元素,实际上是往底层数组添加元素。但是底层数组的长度是固定的,如果索引 `len-1` 所指向的元素已经是底层数组的最后一个元素,就没法再添加了。 25 | 26 | 这时,slice 会迁移到新的内存位置,新底层数组的长度也会增加,这样就可以放置新增的元素。同时,为了应对未来可能再次发生的 append 操作,新的底层数组的长度,也就是新 `slice` 的容量是留了一定的 `buffer` 的。否则,每次添加元素的时候,都会发生迁移,成本太高。 27 | 28 | 新 slice 预留的 `buffer` 大小是有一定规律的。网上大多数的文章都是这样描述的: 29 | >当原 slice 容量小于 `1024` 的时候,新 slice 容量变成原来的 `2` 倍;原 slice 容量超过 `1024`,新 slice 容量变成原来的`1.25`倍。 30 | 31 | 我在这里先说结论:以上描述是错误的。 32 | 33 | 为了说明上面的规律是错误的,我写了一小段玩具代码: 34 | 35 | ```golang 36 | package main 37 | 38 | import "fmt" 39 | 40 | func main() { 41 | s := make([]int, 0) 42 | 43 | oldCap := cap(s) 44 | 45 | for i := 0; i < 2048; i++ { 46 | s = append(s, i) 47 | 48 | newCap := cap(s) 49 | 50 | if newCap != oldCap { 51 | fmt.Printf("[%d -> %4d] cap = %-4d | after append %-4d cap = %-4d\n", 0, i-1, oldCap, i, newCap) 52 | oldCap = newCap 53 | } 54 | } 55 | } 56 | ``` 57 | 我先创建了一个空的 `slice`,然后,在一个循环里不断往里面 `append` 新的元素。然后记录容量的变化,并且每当容量发生变化的时候,记录下老的容量,以及添加完元素之后的容量,同时记下此时 `slice` 里的元素。这样,我就可以观察,新老 `slice` 的容量变化情况,从而找出规律。 58 | 59 | 运行结果: 60 | 61 | ```shell 62 | [0 -> -1] cap = 0 | after append 0 cap = 1 63 | [0 -> 0] cap = 1 | after append 1 cap = 2 64 | [0 -> 1] cap = 2 | after append 2 cap = 4 65 | [0 -> 3] cap = 4 | after append 4 cap = 8 66 | [0 -> 7] cap = 8 | after append 8 cap = 16 67 | [0 -> 15] cap = 16 | after append 16 cap = 32 68 | [0 -> 31] cap = 32 | after append 32 cap = 64 69 | [0 -> 63] cap = 64 | after append 64 cap = 128 70 | [0 -> 127] cap = 128 | after append 128 cap = 256 71 | [0 -> 255] cap = 256 | after append 256 cap = 512 72 | [0 -> 511] cap = 512 | after append 512 cap = 1024 73 | [0 -> 1023] cap = 1024 | after append 1024 cap = 1280 74 | [0 -> 1279] cap = 1280 | after append 1280 cap = 1696 75 | [0 -> 1695] cap = 1696 | after append 1696 cap = 2304 76 | ``` 77 | 78 | 在老 slice 容量小于1024的时候,新 slice 的容量的确是老 slice 的2倍。目前还算正确。 79 | 80 | 但是,当老 slice 容量大于等于 `1024` 的时候,情况就有变化了。当向 slice 中添加元素 `1280` 的时候,老 slice 的容量为 `1280`,之后变成了 `1696`,两者并不是 `1.25` 倍的关系(1696/1280=1.325)。添加完 `1696` 后,新的容量 `2304` 当然也不是 `1696` 的 `1.25` 倍。 81 | 82 | 可见,现在网上各种文章中的扩容策略并不正确。我们直接搬出源码:源码面前,了无秘密。 83 | 84 | 从前面汇编代码我们也看到了,向 slice 追加元素的时候,若容量不够,会调用 `growslice` 函数,所以我们直接看它的代码。 85 | 86 | ```golang 87 | // go 1.9.5 src/runtime/slice.go:82 88 | func growslice(et *_type, old slice, cap int) slice { 89 | // …… 90 | newcap := old.cap 91 | doublecap := newcap + newcap 92 | if cap > doublecap { 93 | newcap = cap 94 | } else { 95 | if old.len < 1024 { 96 | newcap = doublecap 97 | } else { 98 | for newcap < cap { 99 | newcap += newcap / 4 100 | } 101 | } 102 | } 103 | // …… 104 | 105 | capmem = roundupsize(uintptr(newcap) * ptrSize) 106 | newcap = int(capmem / ptrSize) 107 | } 108 | ``` 109 | 110 | 看到了吗?如果只看前半部分,现在网上各种文章里说的 `newcap` 的规律是对的。现实是,后半部分还对 `newcap` 作了一个`内存对齐`,这个和内存分配策略相关。进行内存对齐之后,新 slice 的容量是要 `大于等于` 老 slice 容量的 `2倍`或者`1.25倍`。 111 | 112 | 之后,向 Go 内存管理器申请内存,将老 slice 中的数据复制过去,并且将 append 的元素添加到新的底层数组中。 113 | 114 | 最后,向 `growslice` 函数调用者返回一个新的 slice,这个 slice 的长度并没有变化,而容量却增大了。 115 | 116 | 【引申1】 117 | 118 | 来看一个例子,来源于[这里](https://jiajunhuang.com/articles/2017_07_18-golang_slice.md.html) 119 | 120 | ```golang 121 | package main 122 | 123 | import "fmt" 124 | 125 | func main() { 126 | s := []int{5} 127 | s = append(s, 7) 128 | s = append(s, 9) 129 | x := append(s, 11) 130 | y := append(s, 12) 131 | fmt.Println(s, x, y) 132 | } 133 | ``` 134 | 135 | |代码|切片对应状态| 136 | |---|---| 137 | |s := []int{5}|s 只有一个元素,`[5]`| 138 | |s = append(s, 7)|s 扩容,容量变为2,`[5, 7]`| 139 | |s = append(s, 9)|s 扩容,容量变为4,`[5, 7, 9]`。注意,这时 s 长度是3,只有3个元素| 140 | |x := append(s, 11)|由于 s 的底层数组仍然有空间,因此并不会扩容。这样,底层数组就变成了 `[5, 7, 9, 11]`。注意,此时 s = `[5, 7, 9]`,容量为4;x = `[5, 7, 9, 11]`,容量为4。这里 s 不变| 141 | |y := append(s, 12)|这里还是在 s 元素的尾部追加元素,由于 s 的长度为3,容量为4,所以直接在底层数组索引为3的地方填上12。结果:s = `[5, 7, 9]`,y = `[5, 7, 9, 12]`,x = `[5, 7, 9, 12]`,x,y 的长度均为4,容量也均为4| 142 | 143 | 所以最后程序的执行结果是: 144 | 145 | ```shell 146 | [5 7 9] [5 7 9 12] [5 7 9 12] 147 | ``` 148 | 149 | 这里要注意的是,append函数执行完后,返回的是一个全新的 slice,并且对传入的 slice 并不影响。 150 | 151 | 【引申2】 152 | 153 | 关于 `append`,我们最后来看一个例子,来源于 [Golang Slice的扩容规则](https://jodezer.github.io/2017/05/golangSlice%E7%9A%84%E6%89%A9%E5%AE%B9%E8%A7%84%E5%88%99)。 154 | 155 | ```golang 156 | package main 157 | 158 | import "fmt" 159 | 160 | func main() { 161 | s := []int{1,2} 162 | s = append(s,4,5,6) 163 | fmt.Printf("len=%d, cap=%d",len(s),cap(s)) 164 | } 165 | ``` 166 | 167 | 运行结果是: 168 | ```shell 169 | len=5, cap=6 170 | ``` 171 | 172 | 如果按网上各种文章中总结的那样:小于原 slice 长度小于 1024 的时候,容量每次增加 1 倍。添加元素 4 的时候,容量变为4;添加元素 5 的时候不变;添加元素 6 的时候容量增加 1 倍,变成 8。 173 | 174 | 那上面代码的运行结果就是: 175 | 176 | ```shell 177 | len=5, cap=8 178 | ``` 179 | 180 | 这是错误的!我们来仔细看看,为什么会这样,再次搬出代码: 181 | 182 | ```golang 183 | // go 1.9.5 src/runtime/slice.go:82 184 | func growslice(et *_type, old slice, cap int) slice { 185 | // …… 186 | newcap := old.cap 187 | doublecap := newcap + newcap 188 | if cap > doublecap { 189 | newcap = cap 190 | } else { 191 | // …… 192 | } 193 | // …… 194 | 195 | capmem = roundupsize(uintptr(newcap) * ptrSize) 196 | newcap = int(capmem / ptrSize) 197 | } 198 | ``` 199 | 这个函数的参数依次是 `元素的类型,老的 slice,新 slice 最小求的容量`。 200 | 201 | 例子中 `s` 原来只有 2 个元素,`len` 和 `cap` 都为 2,`append` 了三个元素后,长度变为 5,容量最小要变成 5,即调用 `growslice` 函数时,传入的第三个参数应该为 5。即 `cap=5`。而一方面,`doublecap` 是原 `slice`容量的 2 倍,等于 4。满足第一个 `if` 条件,所以 `newcap` 变成了 5。 202 | 203 | 接着调用了 `roundupsize` 函数,传入 40。(代码中ptrSize是指一个指针的大小,在64位机上是8) 204 | 205 | 我们再看内存对齐,搬出 `roundupsize` 函数的代码: 206 | 207 | ```golang 208 | // src/runtime/msize.go:13 209 | func roundupsize(size uintptr) uintptr { 210 | if size < _MaxSmallSize { 211 | if size <= smallSizeMax-8 { 212 | return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]]) 213 | } else { 214 | //…… 215 | } 216 | } 217 | //…… 218 | } 219 | 220 | const _MaxSmallSize = 32768 221 | const smallSizeMax = 1024 222 | const smallSizeDiv = 8 223 | ``` 224 | 225 | 很明显,我们最终将返回这个式子的结果: 226 | 227 | ```golang 228 | class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]] 229 | ``` 230 | 231 | 这是 `Go` 源码中有关内存分配的两个 `slice`。`class_to_size`通过 `spanClass`获取 `span`划分的 `object`大小。而 `size_to_class8` 表示通过 `size` 获取它的 `spanClass`。 232 | 233 | ```golang 234 | var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31} 235 | 236 | var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768} 237 | ``` 238 | 239 | 我们传进去的 `size` 等于 40。所以 `(size+smallSizeDiv-1)/smallSizeDiv = 5`;获取 `size_to_class8` 数组中索引为 `5` 的元素为 `4`;获取 `class_to_size` 中索引为 `4` 的元素为 `48`。 240 | 241 | 最终,新的 slice 的容量为 `6`: 242 | 243 | ```golang 244 | newcap = int(capmem / ptrSize) // 6 245 | ``` 246 | 247 | 至于,上面的两个`魔法数组`的由来,就不展开了。 248 | 249 | 【引申2】 250 | 向一个nil的slice添加元素会发生什么?为什么? 251 | 252 | 其实 `nil slice` 或者 `empty slice` 都是可以通过调用 append 函数来获得底层数组的扩容。最终都是调用 `mallocgc` 来向 Go 的内存管理器申请到一块内存,然后再赋给原来的`nil slice` 或 `empty slice`,然后摇身一变,成为“真正”的 `slice` 了。 253 | -------------------------------------------------------------------------------- /goroutine 调度器/goroutine 如何退出.md: -------------------------------------------------------------------------------- 1 | 上一讲说到调度器将 main goroutine 推上舞台,为它铺好了道路,开始执行 `runtime.main` 函数。这一讲,我们探索 main goroutine 以及普通 goroutine 从执行到退出的整个过程。 2 | 3 | ```golang 4 | // The main goroutine. 5 | func main() { 6 | // g = main goroutine,不再是 g0 了 7 | g := getg() 8 | 9 | // …………………… 10 | 11 | if sys.PtrSize == 8 { 12 | maxstacksize = 1000000000 13 | } else { 14 | maxstacksize = 250000000 15 | } 16 | 17 | // Allow newproc to start new Ms. 18 | mainStarted = true 19 | 20 | systemstack(func() { 21 | // 创建监控线程,该线程独立于调度器,不需要跟 p 关联即可运行 22 | newm(sysmon, nil) 23 | }) 24 | 25 | lockOSThread() 26 | 27 | if g.m != &m0 { 28 | throw("runtime.main not on m0") 29 | } 30 | 31 | // 调用 runtime 包的初始化函数,由编译器实现 32 | runtime_init() // must be before defer 33 | if nanotime() == 0 { 34 | throw("nanotime returning zero") 35 | } 36 | 37 | // Defer unlock so that runtime.Goexit during init does the unlock too. 38 | needUnlock := true 39 | defer func() { 40 | if needUnlock { 41 | unlockOSThread() 42 | } 43 | }() 44 | 45 | // Record when the world started. Must be after runtime_init 46 | // because nanotime on some platforms depends on startNano. 47 | runtimeInitTime = nanotime() 48 | 49 | // 开启垃圾回收器 50 | gcenable() 51 | 52 | main_init_done = make(chan bool) 53 | 54 | // …………………… 55 | 56 | // main 包的初始化,递归的调用我们 import 进来的包的初始化函数 57 | fn := main_init 58 | fn() 59 | close(main_init_done) 60 | 61 | needUnlock = false 62 | unlockOSThread() 63 | 64 | // …………………… 65 | 66 | // 调用 main.main 函数 67 | fn = main_main 68 | fn() 69 | if raceenabled { 70 | racefini() 71 | } 72 | 73 | // …………………… 74 | 75 | // 进入系统调用,退出进程,可以看出 main goroutine 并未返回,而是直接进入系统调用退出进程了 76 | exit(0) 77 | // 保护性代码,如果 exit 意外返回,下面的代码会让该进程 crash 死掉 78 | for { 79 | var x *int32 80 | *x = 0 81 | } 82 | } 83 | ``` 84 | 85 | `main` 函数执行流程如下图: 86 | 87 | ![runtime.main 启动流程](https://user-images.githubusercontent.com/7698088/63644048-70f5b380-c712-11e9-9926-8abde27164fa.png) 88 | 89 | 从流程图可知,main goroutine 执行完之后就直接调用 `exit(0)` 退出了,这会导致整个进程退出,太粗暴了。 90 | 91 | 不过,main goroutine 实际上就是代表用户的 main 函数,它都执行完了,肯定是用户的任务都执行完了,直接退出就可以了,就算有其他的 goroutine 没执行完,同样会直接退出。 92 | 93 | ```golang 94 | package main 95 | 96 | import "fmt" 97 | 98 | func main() { 99 | go func() {fmt.Println("hello qcrao.com")}() 100 | } 101 | ``` 102 | 103 | 在这个例子中,main gorutine 退出时,还来不及执行 `go 出去` 的函数,整个进程就直接退出了,打印语句不会执行。因此,main goroutine 不会等待其他 goroutine 执行完再退出,知道这个有时能解释一些现象,比如上面那个例子。 104 | 105 | 这时,心中可能会跳出疑问,我们在新创建 goroutine 的时候,不是整出了个“偷天换日”,风风火火地设置了 goroutine 退出时应该跳到 `runtime.goexit` 函数吗,怎么这会不用了,闲得慌? 106 | 107 | 回顾一下上一讲的内容,跳转到 main 函数的两行代码: 108 | 109 | ```asm 110 | // 把 sched.pc 值放入 BX 寄存器 111 | MOVQ gobuf_pc(BX), BX 112 | // JMP 把 BX 寄存器的包含的地址值放入 CPU 的 IP 寄存器,于是,CPU 跳转到该地址继续执行指令 113 | JMP BX 114 | ``` 115 | 116 | 直接使用了一个跳转,并没有使用 `CALL` 指令,而 runtime.main 函数中确实也没有 `RET` 返回的指令。所以,main goroutine 执行完后,直接调用 exit(0) 退出整个进程。 117 | 118 | 那之前整地“偷天换日”还有用吗?有的!这是针对非 main goroutine 起作用。 119 | 120 | 参考资料【阿波张 非 goroutine 的退出】中用调试工具验证了非 main goroutine 的退出,感兴趣的可以去跟着实践一遍。 121 | 122 | 我们继续探索非 main goroutine (后文我们就称 gp 好了)的退出流程。 123 | 124 | `gp` 执行完后,RET 指令弹出 `goexit` 函数地址(实际上是 funcPC(goexit)+1),CPU 跳转到 `goexit` 的第二条指令继续执行: 125 | 126 | ```golang 127 | // src/runtime/asm_amd64.s 128 | 129 | // The top-most function running on a goroutine 130 | // returns to goexit+PCQuantum. 131 | TEXT runtime·goexit(SB),NOSPLIT,$0-0 132 | BYTE $0x90 // NOP 133 | CALL runtime·goexit1(SB) // does not return 134 | // traceback from goexit1 must hit code range of goexit 135 | BYTE $0x90 // NOP 136 | ``` 137 | 138 | 直接调用 `runtime·goexit1`: 139 | 140 | ```golang 141 | // src/runtime/proc.go 142 | // Finishes execution of the current goroutine. 143 | func goexit1() { 144 | // …………………… 145 | mcall(goexit0) 146 | } 147 | ``` 148 | 149 | 调用 `mcall` 函数: 150 | 151 | ```golang 152 | // 切换到 g0 栈,执行 fn(g) 153 | // Fn 不能返回 154 | TEXT runtime·mcall(SB), NOSPLIT, $0-8 155 | // 取出参数的值放入 DI 寄存器,它是 funcval 对象的指针,此场景中 fn.fn 是 goexit0 的地址 156 | MOVQ fn+0(FP), DI 157 | 158 | get_tls(CX) 159 | // AX = g 160 | MOVQ g(CX), AX // save state in g->sched 161 | // mcall 返回地址放入 BX 162 | MOVQ 0(SP), BX // caller's PC 163 | // g.sched.pc = BX,保存 g 的 PC 164 | MOVQ BX, (g_sched+gobuf_pc)(AX) 165 | LEAQ fn+0(FP), BX // caller's SP 166 | // 保存 g 的 SP 167 | MOVQ BX, (g_sched+gobuf_sp)(AX) 168 | MOVQ AX, (g_sched+gobuf_g)(AX) 169 | MOVQ BP, (g_sched+gobuf_bp)(AX) 170 | 171 | // switch to m->g0 & its stack, call fn 172 | MOVQ g(CX), BX 173 | MOVQ g_m(BX), BX 174 | // SI = g0 175 | MOVQ m_g0(BX), SI 176 | CMPQ SI, AX // if g == m->g0 call badmcall 177 | JNE 3(PC) 178 | MOVQ $runtime·badmcall(SB), AX 179 | JMP AX 180 | // 把 g0 的地址设置到线程本地存储中 181 | MOVQ SI, g(CX) // g = m->g0 182 | // 从 g 的栈切换到了 g0 的栈D 183 | MOVQ (g_sched+gobuf_sp)(SI), SP // sp = m->g0->sched.sp 184 | // AX = g,参数入栈 185 | PUSHQ AX 186 | MOVQ DI, DX 187 | // DI 是结构体 funcval 实例对象的指针,它的第一个成员才是 goexit0 的地址 188 | // 读取第一个成员到 DI 寄存器 189 | MOVQ 0(DI), DI 190 | // 调用 goexit0(g) 191 | CALL DI 192 | POPQ AX 193 | MOVQ $runtime·badmcall2(SB), AX 194 | JMP AX 195 | RET 196 | ``` 197 | 198 | 函数参数是: 199 | 200 | ```golang 201 | type funcval struct { 202 | fn uintptr 203 | // variable-size, fn-specific data here 204 | } 205 | ``` 206 | 207 | 字段 fn 就表示 goexit0 函数的地址。 208 | 209 | L5 将函数参数保存到 DI 寄存器,这里 fn.fn 就是 goexit0 的地址。 210 | 211 | L7 将 tls 保存到 CX 寄存器,L9 将 当前线程指向的 goroutine (非 main goroutine,称为 gp)保存到 AX 寄存器,L11 将调用者(调用 mcall 函数)的栈顶,这里就是 mcall 完成后的返回地址,存入 BX 寄存器。 212 | 213 | L13 将 mcall 的返回地址保存到 gp 的 g.sched.pc 字段,L14 将 gp 的栈顶,也就是 SP 保存到 BX 寄存器,L16 将 SP 保存到 gp 的 g.sched.sp 字段,L17 将 g 保存到 gp 的 g.sched.g 字段,L18 将 BP 保存 到 gp 的 g.sched.bp 字段。这一段主要是保存 gp 的调度信息。 214 | 215 | L21 将当前指向的 g 保存到 BX 寄存器,L22 将 g.m 字段保存到 BX 寄存器,L23 将 g.m.g0 字段保存到 SI,g.m.g0 就是当前工作线程的 g0。 216 | 217 | 现在,SI = g0, AX = gp,L25 判断 gp 是否是 g0,如果 gp == g0 说明有问题,执行 runtime·badmcall。正常情况下,PC 值加 3,跳过下面的两条指令,直接到达 L30。 218 | 219 | L30 将 g0 的地址设置到线程本地存储中,L32 将 g0.SP 设置到 CPU 的 SP 寄存器,这也就意味着我们从 gp 栈切换到了 g0 的栈,要变天了! 220 | 221 | L34 将参数 gp 入栈,为调用 goexit0 构造参数。L35 将 DI 寄存器的内容设置到 DX 寄存器,DI 是结构体 funcval 实例对象的指针,它的第一个成员才是 goexit0 的地址。L36 读取 DI 第一成员,也就是 goexit0 函数的地址。 222 | 223 | L40 调用 goexit0 函数,这已经是在 g0 栈上执行了,函数参数就是 gp。 224 | 225 | 到这里,就会去执行 goexit0 函数,注意,这里永远都不会返回。所以,在 CALL 指令后面,如果返回了,又会去调用 `runtime.badmcall2` 函数去处理意外情况。 226 | 227 | 来继续看 goexit0: 228 | 229 | ```golang 230 | // goexit continuation on g0. 231 | // 在 g0 上执行 232 | func goexit0(gp *g) { 233 | // g0 234 | _g_ := getg() 235 | 236 | casgstatus(gp, _Grunning, _Gdead) 237 | if isSystemGoroutine(gp) { 238 | atomic.Xadd(&sched.ngsys, -1) 239 | } 240 | 241 | // 清空 gp 的一些字段 242 | gp.m = nil 243 | gp.lockedm = nil 244 | _g_.m.lockedg = nil 245 | gp.paniconfault = false 246 | gp._defer = nil // should be true already but just in case. 247 | gp._panic = nil // non-nil for Goexit during panic. points at stack-allocated data. 248 | gp.writebuf = nil 249 | gp.waitreason = "" 250 | gp.param = nil 251 | gp.labels = nil 252 | gp.timer = nil 253 | 254 | // Note that gp's stack scan is now "valid" because it has no 255 | // stack. 256 | gp.gcscanvalid = true 257 | // 解除 g 与 m 的关系 258 | dropg() 259 | 260 | if _g_.m.locked&^_LockExternal != 0 { 261 | print("invalid m->locked = ", _g_.m.locked, "\n") 262 | throw("internal lockOSThread error") 263 | } 264 | _g_.m.locked = 0 265 | // 将 g 放入 free 队列缓存起来 266 | gfput(_g_.m.p.ptr(), gp) 267 | schedule() 268 | } 269 | ``` 270 | 271 | 它主要完成最后的清理工作: 272 | 273 | > 1. 把 g 的状态从 `_Grunning` 更新为 `_Gdead`; 274 | 275 | > 2. 清空 g 的一些字段; 276 | 277 | > 3. 调用 dropg 函数解除 g 和 m 之间的关系,其实就是设置 g->m = nil, m->currg = nil; 278 | 279 | > 4. 把 g 放入 p 的 freeg 队列缓存起来供下次创建 g 时快速获取而不用从内存分配。freeg 就是 g 的一个对象池; 280 | 281 | > 5. 调用 schedule 函数再次进行调度。 282 | 283 | 到这里,gp 就完成了它的历史使命,功成身退,进入了 goroutine 缓存池,待下次有任务再重新启用。 284 | 285 | 而工作线程,又继续调用 schedule 函数进行新一轮的调度,整个过程形成了一个循环。 286 | 287 | 总结一下,main goroutine 和普通 goroutine 的退出过程: 288 | 289 | 对于 main goroutine,在执行完用户定义的 main 函数的所有代码后,直接调用 exit(0) 退出整个进程,非常霸道。 290 | 291 | 对于普通 goroutine 则没这么“舒服”,需要经历一系列的过程。先是跳转到提前设置好的 goexit 函数的第二条指令,然后调用 runtime.goexit1,接着调用 `mcall(goexit0)`,而 mcall 函数会切换到 g0 栈,运行 goexit0 函数,清理 goroutine 的一些字段,并将其添加到 goroutine 缓存池里,然后进入 schedule 调度循环。到这里,普通 goroutine 才算完成使命。 292 | 293 | # 参考资料 294 | 295 | 【阿波张 非 main goroutine 的退出及调度循环】https://mp.weixin.qq.com/s/XttP9q7-PO7VXhskaBzGqA -------------------------------------------------------------------------------- /goroutine 调度器/schedule 循环如何启动.md: -------------------------------------------------------------------------------- 1 | 上一讲新创建了一个 goroutine,设置好了 sched 成员的 sp 和 pc 字段,并且将其添加到了 p0 的本地可运行队列,坐等调度器的调度。 2 | 3 | 我们继续看代码。搞了半天,我们其实还在 `runtime·rt0_go` 函数里,执行完 `runtime·newproc(SB)` 后,两条 POP 指令将之前为调用它构建的参数弹出栈。好消息是,最后就只剩下一个函数了: 4 | 5 | ```golang 6 | // start this M 7 | // 主线程进入调度循环,运行刚刚创建的 goroutine 8 | CALL runtime·mstart(SB) 9 | ``` 10 | 11 | 这到达了本系列的核心区,前面铺垫了半天,调度器终于要开始运转了。 12 | 13 | `mstart` 函数设置了 stackguard0 和 stackguard1 字段后,就直接调用 mstart1() 函数: 14 | 15 | ```golang 16 | func mstart1() { 17 | // 启动过程时 _g_ = m0.g0 18 | _g_ := getg() 19 | 20 | if _g_ != _g_.m.g0 { 21 | throw("bad runtime·mstart") 22 | } 23 | 24 | // Record top of stack for use by mcall. 25 | // Once we call schedule we're never coming back, 26 | // so other calls can reuse this stack space. 27 | // 28 | // 一旦调用 schedule() 函数,永不返回 29 | // 所以栈帧可以被复用 30 | gosave(&_g_.m.g0.sched) 31 | _g_.m.g0.sched.pc = ^uintptr(0) // make sure it is never used 32 | asminit() 33 | minit() 34 | 35 | // …………………… 36 | 37 | // 执行启动函数。初始化过程中,fn == nil 38 | if fn := _g_.m.mstartfn; fn != nil { 39 | fn() 40 | } 41 | 42 | if _g_.m.helpgc != 0 { 43 | _g_.m.helpgc = 0 44 | stopm() 45 | } else if _g_.m != &m0 { 46 | acquirep(_g_.m.nextp.ptr()) 47 | _g_.m.nextp = 0 48 | } 49 | 50 | // 进入调度循环。永不返回 51 | schedule() 52 | } 53 | ``` 54 | 55 | 调用 `gosave` 函数来保存调度信息到 `g0.sched` 结构体,来看源码: 56 | 57 | ```golang 58 | // void gosave(Gobuf*) 59 | // save state in Gobuf; setjmp 60 | TEXT runtime·gosave(SB), NOSPLIT, $0-8 61 | // 将 gobuf 赋值给 AX 62 | MOVQ buf+0(FP), AX // gobuf 63 | // 取参数地址,也就是 caller 的 SP 64 | LEAQ buf+0(FP), BX // caller's SP 65 | // 保存 caller's SP,再次运行时的栈顶 66 | MOVQ BX, gobuf_sp(AX) 67 | MOVQ 0(SP), BX // caller's PC 68 | // 保存 caller's PC,再次运行时的指令地址 69 | MOVQ BX, gobuf_pc(AX) 70 | MOVQ $0, gobuf_ret(AX) 71 | MOVQ BP, gobuf_bp(AX) 72 | // Assert ctxt is zero. See func save. 73 | MOVQ gobuf_ctxt(AX), BX 74 | TESTQ BX, BX 75 | JZ 2(PC) 76 | CALL runtime·badctxt(SB) 77 | // 获取 tls 78 | get_tls(CX) 79 | // 将 g 的地址存入 BX 80 | MOVQ g(CX), BX 81 | // 保存 g 的地址 82 | MOVQ BX, gobuf_g(AX) 83 | RET 84 | ``` 85 | 86 | 主要是设置了 g0.sched.sp 和 g0.sched.pc,前者指向 mstart1 函数栈上参数的位置,后者则指向 gosave 函数返回后的下一条指令。如下图: 87 | 88 | ![调用 gosave 函数后](https://user-images.githubusercontent.com/7698088/64071341-cef24000-ccaa-11e9-896e-94c3511526b9.png) 89 | 90 | 图中 sched.pc 并不直接指向返回地址,所以图中的虚线并没有箭头。 91 | 92 | 接下来,进入 schedule 函数,永不返回。 93 | 94 | ```golang 95 | // 执行一轮调度器的工作:找到一个 runnable 的 goroutine,并且执行它 96 | // 永不返回 97 | func schedule() { 98 | // _g_ = 每个工作线程 m 对应的 g0,初始化时是 m0 的 g0 99 | _g_ := getg() 100 | 101 | // …………………… 102 | 103 | top: 104 | // …………………… 105 | 106 | var gp *g 107 | var inheritTime bool 108 | 109 | // …………………… 110 | 111 | if gp == nil { 112 | // Check the global runnable queue once in a while to ensure fairness. 113 | // Otherwise two goroutines can completely occupy the local runqueue 114 | // by constantly respawning each other. 115 | // 为了公平,每调用 schedule 函数 61 次就要从全局可运行 goroutine 队列中获取 116 | if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 { 117 | lock(&sched.lock) 118 | // 从全局队列最大获取 1 个 gorutine 119 | gp = globrunqget(_g_.m.p.ptr(), 1) 120 | unlock(&sched.lock) 121 | } 122 | } 123 | 124 | // 从 P 本地获取 G 任务 125 | if gp == nil { 126 | gp, inheritTime = runqget(_g_.m.p.ptr()) 127 | if gp != nil && _g_.m.spinning { 128 | throw("schedule: spinning with local work") 129 | } 130 | } 131 | 132 | if gp == nil { 133 | // 从本地运行队列和全局运行队列都没有找到需要运行的 goroutine, 134 | // 调用 findrunnable 函数从其它工作线程的运行队列中偷取,如果偷不到,则当前工作线程进入睡眠 135 | // 直到获取到 runnable goroutine 之后 findrunnable 函数才会返回。 136 | gp, inheritTime = findrunnable() // blocks until work is available 137 | } 138 | 139 | // This thread is going to run a goroutine and is not spinning anymore, 140 | // so if it was marked as spinning we need to reset it now and potentially 141 | // start a new spinning M. 142 | if _g_.m.spinning { 143 | resetspinning() 144 | } 145 | 146 | if gp.lockedm != nil { 147 | // Hands off own p to the locked m, 148 | // then blocks waiting for a new p. 149 | startlockedm(gp) 150 | goto top 151 | } 152 | 153 | // 执行 goroutine 任务函数 154 | // 当前运行的是 runtime 的代码,函数调用栈使用的是 g0 的栈空间 155 | // 调用 execute 切换到 gp 的代码和栈空间去运行 156 | execute(gp, inheritTime) 157 | } 158 | ``` 159 | 160 | 调用 `runqget`,从 P 本地可运行队列先选出一个可运行的 goroutine;为了公平,调度器每调度 61 次的时候,都会尝试从全局队列里取出待运行的 goroutine 来运行,调用 `globrunqget`;如果还没找到,就要去其他 P 里面去偷一些 goroutine 来执行,调用 `findrunnable` 函数。 161 | 162 | 经过千辛万苦,终于找到了可以运行的 goroutine,调用 `execute(gp, inheritTime)` 切换到选出的 goroutine 栈执行,调度器的调度次数会在这里更新,源码如下: 163 | 164 | ```golang 165 | // 调度 gp 在当前 M 上运行 166 | // 如果 inheritTime 为真,gp 执行当前的时间片 167 | // 否则,开启一个新的时间片 168 | // 169 | //go:yeswritebarrierrec 170 | func execute(gp *g, inheritTime bool) { 171 | // g0 172 | _g_ := getg() 173 | 174 | // 将 gp 的状态改为 running 175 | casgstatus(gp, _Grunnable, _Grunning) 176 | gp.waitsince = 0 177 | gp.preempt = false 178 | gp.stackguard0 = gp.stack.lo + _StackGuard 179 | if !inheritTime { 180 | // 调度器调度次数增加 1 181 | _g_.m.p.ptr().schedtick++ 182 | } 183 | 184 | // 将 gp 和 m 关联起来 185 | _g_.m.curg = gp 186 | gp.m = _g_.m 187 | 188 | // ………………………… 189 | 190 | // gogo 完成从 g0 到 gp 真正的切换 191 | // CPU 执行权的转让以及栈的切换 192 | // 执行流的切换从本质上来说就是 CPU 寄存器以及函数调用栈的切换, 193 | // 然而不管是 go 还是 c 这种高级语言都无法精确控制 CPU 寄存器的修改, 194 | // 因而高级语言在这里也就无能为力了,只能依靠汇编指令来达成目的 195 | gogo(&gp.sched) 196 | } 197 | ``` 198 | 199 | 将 gp 的状态改为 `_Grunning`,将 m 和 gp 相互关联起来。最后,调用 `gogo` 完成从 g0 到 gp 的切换,CPU 的执行权将从 g0 转让到 gp。 `gogo` 函数用汇编语言写成,原因如下: 200 | 201 | > `gogo` 函数也是通过汇编语言编写的,这里之所以需要使用汇编,是因为 goroutine 的调度涉及不同执行流之间的切换。 202 | 203 | > 前面我们在讨论操作系统切换线程时已经看到过,执行流的切换从本质上来说就是 CPU 寄存器以及函数调用栈的切换,然而不管是 go 还是 c 这种高级语言都无法精确控制 CPU 寄存器,因而高级语言在这里也就无能为力了,只能依靠汇编指令来达成目的。 204 | 205 | 继续看 `gogo` 函数的实现,传入 `&gp.sched` 参数,源码如下: 206 | 207 | ```golang 208 | TEXT runtime·gogo(SB), NOSPLIT, $16-8 209 | // 0(FP) 表示第一个参数,即 buf = &gp.sched 210 | MOVQ buf+0(FP), BX // gobuf 211 | 212 | // …………………… 213 | 214 | MOVQ buf+0(FP), BX 215 | 216 | nilctxt: 217 | // DX = gp.sched.g 218 | MOVQ gobuf_g(BX), DX 219 | MOVQ 0(DX), CX // make sure g != nil 220 | get_tls(CX) 221 | // 将 g 放入到 tls[0] 222 | // 把要运行的 g 的指针放入线程本地存储,这样后面的代码就可以通过线程本地存储 223 | // 获取到当前正在执行的 goroutine 的 g 结构体对象,从而找到与之关联的 m 和 p 224 | // 运行这条指令之前,线程本地存储存放的是 g0 的地址 225 | MOVQ DX, g(CX) 226 | // 把 CPU 的 SP 寄存器设置为 sched.sp,完成了栈的切换 227 | MOVQ gobuf_sp(BX), SP // restore SP 228 | // 恢复调度上下文到CPU相关寄存器 229 | MOVQ gobuf_ret(BX), AX 230 | MOVQ gobuf_ctxt(BX), DX 231 | MOVQ gobuf_bp(BX), BP 232 | // 清空 sched 的值,因为我们已把相关值放入 CPU 对应的寄存器了,不再需要,这样做可以少 GC 的工作量 233 | MOVQ $0, gobuf_sp(BX) // clear to help garbage collector 234 | MOVQ $0, gobuf_ret(BX) 235 | MOVQ $0, gobuf_ctxt(BX) 236 | MOVQ $0, gobuf_bp(BX) 237 | // 把 sched.pc 值放入 BX 寄存器 238 | MOVQ gobuf_pc(BX), BX 239 | // JMP 把 BX 寄存器的包含的地址值放入 CPU 的 IP 寄存器,于是,CPU 跳转到该地址继续执行指令 240 | JMP BX 241 | ``` 242 | 243 | 注释地比较详细了。核心的地方是: 244 | 245 | ```golang 246 | MOVQ gobuf_g(BX), DX 247 | // …… 248 | get_tls(CX) 249 | MOVQ DX, g(CX) 250 | ``` 251 | 252 | 第一行,将 gp.sched.g 保存到 DX 寄存器;第二行,我们见得已经比较多了,`get_tls` 将 tls 保存到 CX 寄存器,再将 gp.sched.g 放到 tls[0] 处。这样,当下次再调用 `get_tls` 时,取出的就是 gp,而不再是 g0,这一行完成从 g0 栈切换到 gp。 253 | 254 | 可能需要提一下的是,Go plan9 汇编中的一些奇怪的符号: 255 | 256 | ```golang 257 | MOVQ buf+0(FP), BX # &gp.sched --> BX 258 | ``` 259 | 260 | `FP` 是个伪奇存器,前面加 0 表示是第一个寄存器,表示参数的位置,最前面的 buf 表示一个符号。关于 Go 汇编语言的一些知识,可以参考曹大在夜读上的分享和《Go 语言高级编程》的相关章节,地址见参考资料。 261 | 262 | 接下来,将 gp.sched 的相关成员恢复到 CPU 对应的寄存器。最重要的是 sched.sp 和 sched.pc,前者被恢复到了 SP 寄存器,后者被保存到 BX 寄存器,最后一条跳转指令跳转到新的地址开始执行。通过之前的文章,我们知道,这里保存的就是 `runtime.main` 函数的地址。 263 | 264 | 最终,调度器完成了这个值得铭记的时刻,从 g0 转到 gp,开始执行 `runtime.main` 函数。 265 | 266 | 用一张流程图总结一下从 g0 切换到 main goroutine 的过程: 267 | 268 | ![从 g0 到 gp](https://user-images.githubusercontent.com/7698088/63644111-b6ff4700-c713-11e9-8961-664ec101030a.png) 269 | 270 | # 参考资料 271 | 【欧神 调度循环】https://github.com/changkun/go-under-the-hood/blob/master/book/zh-cn/part2runtime/ch06sched/exec.md 272 | 273 | 【go 语言核心编程技术 调度器系列】https://mp.weixin.qq.com/s/8eJm5hjwKXya85VnT4y8Cw 274 | 275 | 【曹大 Go plan9 汇编】https://github.com/cch123/asmshare/blob/master/layout.md 276 | 277 | 【Go 语言高级编程】https://chai2010.cn/advanced-go-programming-book/ch3-asm/readme.html 278 | 279 | -------------------------------------------------------------------------------- /channel/向 channel 发送数据的过程是怎样的.md: -------------------------------------------------------------------------------- 1 | # 源码分析 2 | 发送操作最终转化为 `chansend` 函数,直接上源码,同样大部分都注释了,可以看懂主流程: 3 | 4 | ```golang 5 | // 位于 src/runtime/chan.go 6 | 7 | func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { 8 | // 如果 channel 是 nil 9 | if c == nil { 10 | // 不能阻塞,直接返回 false,表示未发送成功 11 | if !block { 12 | return false 13 | } 14 | // 当前 goroutine 被挂起 15 | gopark(nil, nil, "chan send (nil chan)", traceEvGoStop, 2) 16 | throw("unreachable") 17 | } 18 | 19 | // 省略 debug 相关…… 20 | 21 | // 对于不阻塞的 send,快速检测失败场景 22 | // 23 | // 如果 channel 未关闭且 channel 没有多余的缓冲空间。这可能是: 24 | // 1. channel 是非缓冲型的,且等待接收队列里没有 goroutine 25 | // 2. channel 是缓冲型的,但循环数组已经装满了元素 26 | if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) || 27 | (c.dataqsiz > 0 && c.qcount == c.dataqsiz)) { 28 | return false 29 | } 30 | 31 | var t0 int64 32 | if blockprofilerate > 0 { 33 | t0 = cputicks() 34 | } 35 | 36 | // 锁住 channel,并发安全 37 | lock(&c.lock) 38 | 39 | // 如果 channel 关闭了 40 | if c.closed != 0 { 41 | // 解锁 42 | unlock(&c.lock) 43 | // 直接 panic 44 | panic(plainError("send on closed channel")) 45 | } 46 | 47 | // 如果接收队列里有 goroutine,直接将要发送的数据拷贝到接收 goroutine 48 | if sg := c.recvq.dequeue(); sg != nil { 49 | send(c, sg, ep, func() { unlock(&c.lock) }, 3) 50 | return true 51 | } 52 | 53 | // 对于缓冲型的 channel,如果还有缓冲空间 54 | if c.qcount < c.dataqsiz { 55 | // qp 指向 buf 的 sendx 位置 56 | qp := chanbuf(c, c.sendx) 57 | 58 | // …… 59 | 60 | // 将数据从 ep 处拷贝到 qp 61 | typedmemmove(c.elemtype, qp, ep) 62 | // 发送游标值加 1 63 | c.sendx++ 64 | // 如果发送游标值等于容量值,游标值归 0 65 | if c.sendx == c.dataqsiz { 66 | c.sendx = 0 67 | } 68 | // 缓冲区的元素数量加一 69 | c.qcount++ 70 | 71 | // 解锁 72 | unlock(&c.lock) 73 | return true 74 | } 75 | 76 | // 如果不需要阻塞,则直接返回错误 77 | if !block { 78 | unlock(&c.lock) 79 | return false 80 | } 81 | 82 | // channel 满了,发送方会被阻塞。接下来会构造一个 sudog 83 | 84 | // 获取当前 goroutine 的指针 85 | gp := getg() 86 | mysg := acquireSudog() 87 | mysg.releasetime = 0 88 | if t0 != 0 { 89 | mysg.releasetime = -1 90 | } 91 | 92 | mysg.elem = ep 93 | mysg.waitlink = nil 94 | mysg.g = gp 95 | mysg.selectdone = nil 96 | mysg.c = c 97 | gp.waiting = mysg 98 | gp.param = nil 99 | 100 | // 当前 goroutine 进入发送等待队列 101 | c.sendq.enqueue(mysg) 102 | 103 | // 当前 goroutine 被挂起 104 | goparkunlock(&c.lock, "chan send", traceEvGoBlockSend, 3) 105 | 106 | // 从这里开始被唤醒了(channel 有机会可以发送了) 107 | if mysg != gp.waiting { 108 | throw("G waiting list is corrupted") 109 | } 110 | gp.waiting = nil 111 | if gp.param == nil { 112 | if c.closed == 0 { 113 | throw("chansend: spurious wakeup") 114 | } 115 | // 被唤醒后,channel 关闭了。坑爹啊,panic 116 | panic(plainError("send on closed channel")) 117 | } 118 | gp.param = nil 119 | if mysg.releasetime > 0 { 120 | blockevent(mysg.releasetime-t0, 2) 121 | } 122 | // 去掉 mysg 上绑定的 channel 123 | mysg.c = nil 124 | releaseSudog(mysg) 125 | return true 126 | } 127 | ``` 128 | 129 | 上面的代码注释地比较详细了,我们来详细看看。 130 | 131 | - 如果检测到 channel 是空的,当前 goroutine 会被挂起。 132 | 133 | - 对于不阻塞的发送操作,如果 channel 未关闭并且没有多余的缓冲空间(说明:a. channel 是非缓冲型的,且等待接收队列里没有 goroutine;b. channel 是缓冲型的,但循环数组已经装满了元素) 134 | 135 | 对于这一点,runtime 源码里注释了很多。这一条判断语句是为了在不阻塞发送的场景下快速检测到发送失败,好快速返回。 136 | 137 | ```golang 138 | if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) || (c.dataqsiz > 0 && c.qcount == c.dataqsiz)) { 139 | return false 140 | } 141 | ``` 142 | 143 | 注释里主要讲为什么这一块可以不加锁,我详细解释一下。`if` 条件里先读了两个变量:block 和 c.closed。block 是函数的参数,不会变;c.closed 可能被其他 goroutine 改变,因为没加锁嘛,这是“与”条件前面两个表达式。 144 | 145 | 最后一项,涉及到三个变量:c.dataqsiz,c.recvq.first,c.qcount。`c.dataqsiz == 0 && c.recvq.first == nil` 指的是非缓冲型的 channel,并且 recvq 里没有等待接收的 goroutine;`c.dataqsiz > 0 && c.qcount == c.dataqsiz` 指的是缓冲型的 channel,但循环数组已经满了。这里 `c.dataqsiz` 实际上也是不会被修改的,在创建的时候就已经确定了。不加锁真正影响地是 `c.qcount` 和 `c.recvq.first`。 146 | 147 | 这一部分的条件就是两个 `word-sized read`,就是读两个 word 操作:`c.closed` 和 `c.recvq.first`(非缓冲型) 或者 `c.qcount`(缓冲型)。 148 | 149 | 当我们发现 `c.closed == 0` 为真,也就是 channel 未被关闭,再去检测第三部分的条件时,观测到 `c.recvq.first == nil` 或者 `c.qcount == c.dataqsiz` 时(这里忽略 `c.dataqsiz`),就断定要将这次发送操作作失败处理,快速返回 false。 150 | 151 | 这里涉及到两个观测项:channel 未关闭、channel not ready for sending。这两项都会因为没加锁而出现观测前后不一致的情况。例如我先观测到 channel 未被关闭,再观察到 channel not ready for sending,这时我以为能满足这个 if 条件了,但是如果这时 c.closed 变成 1,这时其实就不满足条件了,谁让你不加锁呢! 152 | 153 | 但是,因为一个 closed channel 不能将 channel 状态从 'ready for sending' 变成 'not ready for sending',所以当我观测到 'not ready for sending' 时,channel 不是 closed。即使 `c.closed == 1`,即 channel 是在这两个观测中间被关闭的,那也说明在这两个观测中间,channel 满足两个条件:`not closed` 和 `not ready for sending`,这时,我直接返回 false 也是没有问题的。 154 | 155 | 这部分解释地比较绕,其实这样做的目的就是少获取一次锁,提升性能。 156 | 157 | - 如果检测到 channel 已经关闭,直接 panic。 158 | 159 | - 如果能从等待接收队列 recvq 里出队一个 sudog(代表一个 goroutine),说明此时 channel 是空的,没有元素,所以才会有等待接收者。这时会调用 send 函数将元素直接从发送者的栈拷贝到接收者的栈,关键操作由 `sendDirect` 函数完成。 160 | 161 | ```golang 162 | // send 函数处理向一个空的 channel 发送操作 163 | 164 | // ep 指向被发送的元素,会被直接拷贝到接收的 goroutine 165 | // 之后,接收的 goroutine 会被唤醒 166 | // c 必须是空的(因为等待队列里有 goroutine,肯定是空的) 167 | // c 必须被上锁,发送操作执行完后,会使用 unlockf 函数解锁 168 | // sg 必须已经从等待队列里取出来了 169 | // ep 必须是非空,并且它指向堆或调用者的栈 170 | 171 | func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) { 172 | // 省略一些用不到的 173 | // …… 174 | 175 | // sg.elem 指向接收到的值存放的位置,如 val <- ch,指的就是 &val 176 | if sg.elem != nil { 177 | // 直接拷贝内存(从发送者到接收者) 178 | sendDirect(c.elemtype, sg, ep) 179 | sg.elem = nil 180 | } 181 | // sudog 上绑定的 goroutine 182 | gp := sg.g 183 | // 解锁 184 | unlockf() 185 | gp.param = unsafe.Pointer(sg) 186 | if sg.releasetime != 0 { 187 | sg.releasetime = cputicks() 188 | } 189 | // 唤醒接收的 goroutine. skip 和打印栈相关,暂时不理会 190 | goready(gp, skip+1) 191 | } 192 | ``` 193 | 194 | 继续看 `sendDirect` 函数: 195 | 196 | ```golang 197 | // 向一个非缓冲型的 channel 发送数据、从一个无元素的(非缓冲型或缓冲型但空)的 channel 198 | // 接收数据,都会导致一个 goroutine 直接操作另一个 goroutine 的栈 199 | // 由于 GC 假设对栈的写操作只能发生在 goroutine 正在运行中并且由当前 goroutine 来写 200 | // 所以这里实际上违反了这个假设。可能会造成一些问题,所以需要用到写屏障来规避 201 | func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) { 202 | // src 在当前 goroutine 的栈上,dst 是另一个 goroutine 的栈 203 | 204 | // 直接进行内存"搬迁" 205 | // 如果目标地址的栈发生了栈收缩,当我们读出了 sg.elem 后 206 | // 就不能修改真正的 dst 位置的值了 207 | // 因此需要在读和写之前加上一个屏障 208 | dst := sg.elem 209 | typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size) 210 | memmove(dst, src, t.size) 211 | } 212 | ``` 213 | 214 | 这里涉及到一个 goroutine 直接写另一个 goroutine 栈的操作,一般而言,不同 goroutine 的栈是各自独有的。而这也违反了 GC 的一些假设。为了不出问题,写的过程中增加了写屏障,保证正确地完成写操作。这样做的好处是减少了一次内存 copy:不用先拷贝到 channel 的 buf,直接由发送者到接收者,没有中间商赚差价,效率得以提高,完美。 215 | 216 | 然后,解锁、唤醒接收者,等待调度器的光临,接收者也得以重见天日,可以继续执行接收操作之后的代码了。 217 | 218 | - 如果 `c.qcount < c.dataqsiz`,说明缓冲区可用(肯定是缓冲型的 channel)。先通过函数取出待发送元素应该去到的位置: 219 | 220 | ```golang 221 | qp := chanbuf(c, c.sendx) 222 | 223 | // 返回循环队列里第 i 个元素的地址处 224 | func chanbuf(c *hchan, i uint) unsafe.Pointer { 225 | return add(c.buf, uintptr(i)*uintptr(c.elemsize)) 226 | } 227 | ``` 228 | 229 | `c.sendx` 指向下一个待发送元素在循环数组中的位置,然后调用 `typedmemmove` 函数将其拷贝到循环数组中。之后 `c.sendx` 加 1,元素总量加 1 :`c.qcount++`,最后,解锁并返回。 230 | 231 | - 如果没有命中以上条件的,说明 channel 已经满了。不管这个 channel 是缓冲型的还是非缓冲型的,都要将这个 sender “关起来”(goroutine 被阻塞)。如果 block 为 false,直接解锁,返回 false。 232 | 233 | - 最后就是真的需要被阻塞的情况。先构造一个 sudog,将其入队(channel 的 sendq 字段)。然后调用 `goparkunlock` 将当前 goroutine 挂起,并解锁,等待合适的时机再唤醒。 234 | 235 | 唤醒之后,从 `goparkunlock` 下一行代码开始继续往下执行。 236 | 237 | 这里有一些绑定操作,sudog 通过 g 字段绑定 goroutine,而 goroutine 通过 waiting 绑定 sudog,sudog 还通过 `elem` 字段绑定待发送元素的地址,以及 `c` 字段绑定被“坑”在此处的 channel。 238 | 239 | 所以,待发送的元素地址其实是存储在 sudog 结构体里,也就是当前 goroutine 里。 240 | 241 | # 案例分析 242 | 好了,看完源码。我们接着来分析例子,代码如下: 243 | 244 | ```golang 245 | func goroutineA(a <-chan int) { 246 | val := <- a 247 | fmt.Println("goroutine A received data: ", val) 248 | return 249 | } 250 | 251 | func goroutineB(b <-chan int) { 252 | val := <- b 253 | fmt.Println("goroutine B received data: ", val) 254 | return 255 | } 256 | 257 | func main() { 258 | ch := make(chan int) 259 | go goroutineA(ch) 260 | go goroutineB(ch) 261 | ch <- 3 262 | time.Sleep(time.Second) 263 | 264 | ch1 := make(chan struct{}) 265 | } 266 | ``` 267 | 268 | 在发送小节里我们说到 G1 和 G2 现在被挂起来了,等待 sender 的解救。在第 17 行,主协程向 ch 发送了一个元素 3,来看下接下来会发生什么。 269 | 270 | 根据前面源码分析的结果,我们知道,sender 发现 ch 的 recvq 里有 receiver 在等待着接收,就会出队一个 sudog,把 recvq 里 first 指针的 sudo “推举”出来了,并将其加入到 P 的可运行 goroutine 队列中。 271 | 272 | 然后,sender 把发送元素拷贝到 sudog 的 elem 地址处,最后会调用 goready 将 G1 唤醒,状态变为 runnable。 273 | 274 | ![G1 runnable](https://user-images.githubusercontent.com/7698088/61342598-4bf16380-a87d-11e9-8667-c22b02030d6b.png) 275 | 276 | 当调度器光顾 G1 时,将 G1 变成 running 状态,执行 goroutineA 接下来的代码。G 表示其他可能有的 goroutine。 277 | 278 | 这里其实涉及到一个协程写另一个协程栈的操作。有两个 receiver 在 channel 的一边虎视眈眈地等着,这时 channel 另一边来了一个 sender 准备向 channel 发送数据,为了高效,用不着通过 channel 的 buf “中转”一次,直接从源地址把数据 copy 到目的地址就可以了,效率高啊! 279 | 280 | ![send direct](https://user-images.githubusercontent.com/7698088/61342620-64fa1480-a87d-11e9-8cac-eacd2f4892f8.png) 281 | 282 | 上图是一个示意图,`3` 会被拷贝到 G1 栈上的某个位置,也就是 val 的地址处,保存在 elem 字段。 283 | 284 | # 参考资料 285 | 【深入 channel 底层】https://codeburst.io/diving-deep-into-the-golang-channels-549fd4ed21a8 286 | 287 | 【Kavya在Gopher Con 上关于 channel 的设计,非常好】https://speakerd.s3.amazonaws.com/presentations/10ac0b1d76a6463aa98ad6a9dec917a7/GopherCon_v10.0.pdf 288 | -------------------------------------------------------------------------------- /编译和链接/Go 编译链接过程概述.md: -------------------------------------------------------------------------------- 1 | 我们从一个 `Hello World` 的例子开始: 2 | 3 | ```golang 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | fmt.Println("hello world") 10 | } 11 | ``` 12 | 13 | 当我们用键盘敲完上面的 hello world 代码时,保存在硬盘上的 `hello.go` 文件就是一个字节序列了,每个字节代表一个字符。 14 | 15 | 用 vim 打开 hello.go 文件,在命令行模式下,输入命令: 16 | 17 | ```shell 18 | :%!xxd 19 | ``` 20 | 21 | 就能在 vim 里以十六进制查看文件内容: 22 | 23 | ![hex .go](https://user-images.githubusercontent.com/7698088/59696461-9d76e600-921e-11e9-9253-533d55e2c8f5.png) 24 | 25 | 最左边的一列代表地址值,中间一列代表文本对应的 ASCII 字符,最右边的列就是我们的代码。再在终端里执行 `man ascii`: 26 | 27 | ![ASCII](https://user-images.githubusercontent.com/7698088/59696702-15dda700-921f-11e9-838f-897a5d3f21fb.png) 28 | 29 | 和 ASCII 字符表一对比,就能发现,中间的列和最右边的列是一一对应的。也就是说,刚刚写完的 hello.go 文件都是由 ASCII 字符表示的,它被称为`文本文件`,其他文件被称为`二进制文件`。 30 | 31 | 当然,更深入地看,计算机中的所有数据,像磁盘文件、网络中的数据其实都是一串比特位组成,取决于如何看待它。在不同的情景下,一个相同的字节序列可能表示成一个整数、浮点数、字符串或者是机器指令。 32 | 33 | 而像 hello.go 这个文件,8 个 bit,也就是一个字节看成一个单位(假定源程序的字符都是 ASCII 码),最终解释成人类能读懂的 Go 源码。 34 | 35 | Go 程序并不能直接运行,每条 Go 语句必须转化为一系列的低级机器语言指令,将这些指令打包到一起,并以二进制磁盘文件的形式存储起来,也就是可执行目标文件。 36 | 37 | 从源文件到可执行目标文件的转化过程: 38 | 39 | ![compile](https://user-images.githubusercontent.com/7698088/60523966-44c74300-9d1e-11e9-9ba9-d1f594607edc.png) 40 | 41 | 完成以上各个阶段的就是 Go 编译系统。你肯定知道大名鼎鼎的 GCC(GNU Compile Collection),中文名为 GNU 编译器套装,它支持像 C,C++,Java,Python,Objective-C,Ada,Fortran,Pascal,能够为很多不同的机器生成机器码。 42 | 43 | 可执行目标文件可以直接在机器上执行。一般而言,先执行一些初始化的工作;找到 main 函数的入口,执行用户写的代码;执行完成后,main 函数退出;再执行一些收尾的工作,整个过程完毕。 44 | 45 | 在接下来的文章里,我们将探索`编译`和`运行`的过程。 46 | 47 | Go 源码里的编译器源码位于 `src/cmd/compile` 路径下,链接器源码位于 `src/cmd/link` 路径下。 48 | 49 | # 编译过程 50 | 我比较喜欢用 IDE(集成开发环境)来写代码, Go 源码用的 Goland,有时候直接点击 IDE 菜单栏里的“运行”按钮,程序就跑起来了。这实际上隐含了编译和链接的过程,我们通常将编译和链接合并到一起的过程称为构建(Build)。 51 | 52 | 编译过程就是对源文件进行词法分析、语法分析、语义分析、优化,最后生成汇编代码文件,以 `.s` 作为文件后缀。 53 | 54 | 之后,汇编器会将汇编代码转变成机器可以执行的指令。由于每一条汇编语句几乎都与一条机器指令相对应,所以只是一个简单的一一对应,比较简单,没有语法、语义分析,也没有优化这些步骤。 55 | 56 | 编译器是将高级语言翻译成机器语言的一个工具,编译过程一般分为 6 步:扫描、语法分析、语义分析、源代码优化、代码生成、目标代码优化。下图来自《程序员的自我修养》: 57 | 58 | ![编译过程总览](https://user-images.githubusercontent.com/7698088/59910602-d4c6dc00-9444-11e9-8155-fbe59eec4e89.png) 59 | 60 | # 词法分析 61 | 通过前面的例子,我们知道,Go 程序文件在机器看来不过是一堆二进制位。我们能读懂,是因为 Goland 按照 ASCII 码(实际上是 UTF-8)把这堆二进制位进行了编码。例如,把 8个 bit 位分成一组,对应一个字符,通过对照 ASCII 码表就可以查出来。 62 | 63 | 当把所有的二进制位都对应成了 ASCII 码字符后,我们就能看到有意义的字符串。它可能是关键字,例如:package;可能是字符串,例如:“Hello World”。 64 | 65 | 词法分析其实干的就是这个。输入是原始的 Go 程序文件,在词法分析器看来,就是一堆二进制位,根本不知道是什么东西,经过它的分析后,变成有意义的记号。简单来说,词法分析是计算机科学中将字符序列转换为标记(token)序列的过程。 66 | 67 | 我们来看一下维基百科上给出的定义: 68 | 69 | >词法分析(lexical analysis)是计算机科学中将字符序列转换为标记(token)序列的过程。进行词法分析的程序或者函数叫作词法分析器(lexical analyzer,简称lexer),也叫扫描器(scanner)。词法分析器一般以函数的形式存在,供语法分析器调用。 70 | 71 | `.go` 文件被输入到扫描器(Scanner),它使用一种类似于`有限状态机`的算法,将源代码的字符系列分割成一系列的记号(Token)。 72 | 73 | 记号一般分为这几类:关键字、标识符、字面量(包含数字、字符串)、特殊符号(如加号、等号)。 74 | 75 | 例如,对于如下的代码: 76 | 77 | ```golang 78 | slice[i] = i * (2 + 6) 79 | ``` 80 | 81 | 总共包含 16 个非空字符,经过扫描后, 82 | 83 | |记号|类型| 84 | |---|---| 85 | |slice|标识符| 86 | |[|左方括号| 87 | |i|标识符| 88 | |]|右方括号| 89 | |=|赋值| 90 | |i|标识符| 91 | |*|乘号| 92 | |(|左圆括号| 93 | |2|数字| 94 | |+|加号| 95 | |6|数字| 96 | |)|右圆括号| 97 | 98 | 上面的例子源自《程序员的自我修养》,主要讲解编译、链接相关的内容,很精彩,推荐研读。 99 | 100 | Go 语言(本文的 Go 版本是 1.9.2)扫描器支持的 Token 在源码中的路径: 101 | 102 | ```shell 103 | src/cmd/compile/internal/syntax/token.go 104 | ``` 105 | 106 | 感受一下: 107 | 108 | ```golang 109 | var tokstrings = [...]string{ 110 | // source control 111 | _EOF: "EOF", 112 | 113 | // names and literals 114 | _Name: "name", 115 | _Literal: "literal", 116 | 117 | // operators and operations 118 | _Operator: "op", 119 | _AssignOp: "op=", 120 | _IncOp: "opop", 121 | _Assign: "=", 122 | _Define: ":=", 123 | _Arrow: "<-", 124 | _Star: "*", 125 | 126 | // delimitors 127 | _Lparen: "(", 128 | _Lbrack: "[", 129 | _Lbrace: "{", 130 | _Rparen: ")", 131 | _Rbrack: "]", 132 | _Rbrace: "}", 133 | _Comma: ",", 134 | _Semi: ";", 135 | _Colon: ":", 136 | _Dot: ".", 137 | _DotDotDot: "...", 138 | 139 | // keywords 140 | _Break: "break", 141 | _Case: "case", 142 | _Chan: "chan", 143 | _Const: "const", 144 | _Continue: "continue", 145 | _Default: "default", 146 | _Defer: "defer", 147 | _Else: "else", 148 | _Fallthrough: "fallthrough", 149 | _For: "for", 150 | _Func: "func", 151 | _Go: "go", 152 | _Goto: "goto", 153 | _If: "if", 154 | _Import: "import", 155 | _Interface: "interface", 156 | _Map: "map", 157 | _Package: "package", 158 | _Range: "range", 159 | _Return: "return", 160 | _Select: "select", 161 | _Struct: "struct", 162 | _Switch: "switch", 163 | _Type: "type", 164 | _Var: "var", 165 | } 166 | ``` 167 | 168 | 还是比较熟悉的,包括名称和字面量、操作符、分隔符和关键字。 169 | 170 | 而扫描器的路径是: 171 | 172 | ```shell 173 | src/cmd/compile/internal/syntax/scanner.go 174 | ``` 175 | 176 | 其中最关键的函数就是 next 函数,它不断地读取下一个字符(不是下一个字节,因为 Go 语言支持 Unicode 编码,并不是像我们前面举得 ASCII 码的例子,一个字符只有一个字节),直到这些字符可以构成一个 Token。 177 | 178 | ```golang 179 | func (s *scanner) next() { 180 | // …… 181 | 182 | redo: 183 | // skip white space 184 | c := s.getr() 185 | for c == ' ' || c == '\t' || c == '\n' && !nlsemi || c == '\r' { 186 | c = s.getr() 187 | } 188 | 189 | // token start 190 | s.line, s.col = s.source.line0, s.source.col0 191 | 192 | if isLetter(c) || c >= utf8.RuneSelf && s.isIdentRune(c, true) { 193 | s.ident() 194 | return 195 | } 196 | 197 | switch c { 198 | // …… 199 | 200 | case '\n': 201 | s.lit = "newline" 202 | s.tok = _Semi 203 | 204 | case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 205 | s.number(c) 206 | 207 | // …… 208 | 209 | default: 210 | s.tok = 0 211 | s.error(fmt.Sprintf("invalid character %#U", c)) 212 | goto redo 213 | return 214 | 215 | assignop: 216 | if c == '=' { 217 | s.tok = _AssignOp 218 | return 219 | } 220 | s.ungetr() 221 | s.tok = _Operator 222 | } 223 | ``` 224 | 225 | 代码的主要逻辑就是通过 `c := s.getr()` 获取下一个未被解析的字符,并且会跳过之后的空格、回车、换行、tab 字符,然后进入一个大的 `switch-case` 语句,匹配各种不同的情形,最终可以解析出一个 Token,并且把相关的行、列数字记录下来,这样就完成一次解析过程。 226 | 227 | >当前包中的词法分析器 scanner 也只是为上层提供了 next 方法,词法解析的过程都是惰性的,只有在上层的解析器需要时才会调用 next 获取最新的 Token。 228 | 229 | # 语法分析 230 | 上一步生成的 Token 序列,需要经过进一步处理,生成一棵以`表达式`为结点的`语法树`。 231 | 232 | 比如最开始的那个例子,`slice[i] = i * (2 + 6)`,得到的一棵语法树如下: 233 | 234 | ![语法树](https://user-images.githubusercontent.com/7698088/59962830-3490b600-951d-11e9-8ae6-53d6375f0246.png) 235 | 236 | 整个语句被看作是一个赋值表达式,左子树是一个数组表达式,右子树是一个乘法表达式;数组表达式由 2 个符号表达式组成;乘号表达式则是由一个符号表达式和一个加号表达式组成;加号表达式则是由两个数字组成。符号和数字是最小的表达式,它们不能再被分解,通常作为树的叶子节点。 237 | 238 | 语法分析的过程可以检测一些形式上的错误,例如:括号是否缺少一半,`+` 号表达式缺少一个操作数等。 239 | 240 | >语法分析是根据某种特定的形式文法(Grammar)对 Token 序列构成的输入文本进行分析并确定其语法结构的一种过程。 241 | 242 | # 语义分析 243 | 语法分析完成后,我们并不知道语句的具体意义是什么。像上面的 `*` 号的两棵子树如果是两个指针,这是不合法的,但语法分析检测不出来,语义分析就是干这个事。 244 | 245 | 编译期所能检查的是静态语义,可以认为这是在“代码”阶段,包括变量类型的匹配、转换等。例如,将一个浮点值赋给一个指针变量的时候,明显的类型不匹配,就会报编译错误。而对于运行期间才会出现的错误:不小心除了一个 0 ,语义分析是没办法检测的。 246 | 247 | 语义分析阶段完成之后,会在每个节点上标注上类型: 248 | 249 | ![语义分析完成](https://user-images.githubusercontent.com/7698088/59962838-512cee00-951d-11e9-8581-18e12ffde230.png) 250 | 251 | Go 语言编译器在这一阶段检查常量、类型、函数声明以及变量赋值语句的类型,然后检查哈希中键的类型。实现类型检查的函数通常都是几千行的巨型 switch/case 语句。 252 | 253 | >类型检查是 Go 语言编译的第二个阶段,在词法和语法分析之后我们得到了每个文件对应的抽象语法树,随后的类型检查会遍历抽象语法树中的节点,对每个节点的类型进行检验,找出其中存在的语法错误。 254 | 255 | >在这个过程中也可能会对抽象语法树进行改写,这不仅能够去除一些不会被执行的代码对编译进行优化提高执行效率,而且也会修改 make、new 等关键字对应节点的操作类型。 256 | 257 | 例如比较常用的 make 关键字,用它可以创建各种类型,如 slice,map,channel 等等。到这一步的时候,对于 make 关键字,也就是 OMAKE 节点,会先检查它的参数类型,根据类型的不同,进入相应的分支。如果参数类型是 slice,就会进入 TSLICE case 分支,检查 len 和 cap 是否满足要求,如 len <= cap。最后节点类型会从 OMAKE 改成 OMAKESLICE。 258 | 259 | # 中间代码生成 260 | 我们知道,编译过程一般可以分为前端和后端,前端生成和平台无关的中间代码,后端会针对不同的平台,生成不同的机器码。 261 | 262 | 前面词法分析、语法分析、语义分析等都属于编译器前端,之后的阶段属于编译器后端。 263 | 264 | 编译过程有很多优化的环节,在这个环节是指源代码级别的优化。它将语法树转换成中间代码,它是语法树的顺序表示。 265 | 266 | 中间代码一般和目标机器以及运行时环境无关,它有几种常见的形式:三地址码、P-代码。例如,最基本的`三地址码`是这样的: 267 | 268 | ```shell 269 | x = y op z 270 | ``` 271 | 272 | 表示变量 y 和 变量 z 进行 op 操作后,赋值给 x。op 可以是数学运算,例如加减乘除。 273 | 274 | 前面我们举的例子可以写成如下的形式: 275 | 276 | ```shell 277 | t1 = 2 + 6 278 | t2 = i * t1 279 | slice[i] = t2 280 | ``` 281 | 282 | 这里 2 + 6 是可以直接计算出来的,这样就把 t1 这个临时变量“优化”掉了,而且 t1 变量可以重复利用,因此 t2 也可以“优化”掉。优化之后: 283 | 284 | ```shell 285 | t1 = i * 8 286 | slice[i] = t1 287 | ``` 288 | 289 | Go 语言的中间代码表示形式为 SSA(Static Single-Assignment,静态单赋值),之所以称之为单赋值,是因为每个名字在 SSA 中仅被赋值一次。。 290 | 291 | 这一阶段会根据 CPU 的架构设置相应的用于生成中间代码的变量,例如编译器使用的指针和寄存器的大小、可用寄存器列表等。中间代码生成和机器码生成这两部分会共享相同的设置。 292 | 293 | 在生成中间代码之前,会对抽象语法树中节点的一些元素进行替换。这里引用《面向信仰编程》编译原理相关博客里的一张图: 294 | 295 | ![builtin mapping](https://user-images.githubusercontent.com/7698088/60553849-a364df00-9d67-11e9-832a-450f4d8ee6ba.png) 296 | 297 | 例如对于 map 的操作 m[i],在这里会被转换成 mapacess 或 mapassign。 298 | 299 | >Go 语言的主程序在执行时会调用 runtime 中的函数,也就是说关键字和内置函数的功能其实是由语言的编译器和运行时共同完成的。 300 | 301 | >中间代码的生成过程其实就是从 AST 抽象语法树到 SSA 中间代码的转换过程,在这期间会对语法树中的关键字在进行一次更新,更新后的语法树会经过多轮处理转变最后的 SSA 中间代码。 302 | 303 | # 目标代码生成与优化 304 | 不同机器的机器字长、寄存器等等都不一样,意味着在不同机器上跑的机器码是不一样的。最后一步的目的就是要生成能在不同 CPU 架构上运行的代码。 305 | 306 | 为了榨干机器的每一滴油水,目标代码优化器会对一些指令进行优化,例如使用移位指令代替乘法指令等。 307 | 308 | 这块实在没能力深入,幸好也不需要深入。对于应用层的软件开发工程师来说,了解一下就可以了。 309 | 310 | # 链接过程 311 | 编译过程是针对单个文件进行的,文件与文件之间不可避免地要引用定义在其他模块的全局变量或者函数,这些变量或函数的地址只有在此阶段才能确定。 312 | 313 | 链接过程就是要把编译器生成的一个个目标文件链接成可执行文件。最终得到的文件是分成各种段的,比如数据段、代码段、BSS段等等,运行时会被装载到内存中。各个段具有不同的读写、执行属性,保护了程序的安全运行。 314 | 315 | 这部分内容,推荐看《程序员的自我修养》和《深入理解计算机系统》。 -------------------------------------------------------------------------------- /goroutine 调度器/g0 栈何用户栈如何切换.md: -------------------------------------------------------------------------------- 1 | 上一讲讲完了 main goroutine 的诞生,它不是第一个,算上 g0,它要算第二个了。不过,我们要考虑的就是这个 goroutine,它会真正执行用户代码。 2 | 3 | `g0` 栈用于执行调度器的代码,执行完之后,要跳转到执行用户代码的地方,如何跳转?这中间涉及到栈和寄存器的切换。要知道,函数调用和返回主要靠的也是 CPU 寄存器的切换。`goroutine` 的切换和此类似。 4 | 5 | 继续看 `proc1` 函数的代码。中间有一段调整运行空间的代码,计算出的结果一般为 0,也就是一般不会调整 SP 的位置,忽略好了。 6 | 7 | ```golang 8 | // 确定参数入栈位置 9 | spArg := sp 10 | ``` 11 | 12 | 参数的入参位置也是从 SP 处开始,通过: 13 | 14 | ```golang 15 | // 将参数从执行 newproc 函数的栈拷贝到新 g 的栈 16 | memmove(unsafe.Pointer(spArg), unsafe.Pointer(argp), uintptr(narg)) 17 | ``` 18 | 19 | 将 fn 的参数从 g0 栈上拷贝到 newg 的栈上,memmove 函数需要传入源地址、目的地址、参数大小。由于 main 函数在这里没有参数需要拷贝,因此这里相当于没做什么。 20 | 21 | 接着,初始化 newg 的各种字段,而且涉及到最重要的 pc,sp 等字段: 22 | 23 | ```golang 24 | // 把 newg.sched 结构体成员的所有成员设置为 0 25 | memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched)) 26 | // 设置 newg 的 sched 成员,调度器需要依靠这些字段才能把 goroutine 调度到 CPU 上运行 27 | newg.sched.sp = sp 28 | newg.stktopsp = sp 29 | // newg.sched.pc 表示当 newg 被调度起来运行时从这个地址开始执行指令 30 | newg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function 31 | newg.sched.g = guintptr(unsafe.Pointer(newg)) 32 | gostartcallfn(&newg.sched, fn) 33 | newg.gopc = callerpc 34 | // 设置 newg 的 startpc 为 fn.fn,该成员主要用于函数调用栈的 traceback 和栈收缩 35 | // newg 真正从哪里开始执行并不依赖于这个成员,而是 sched.pc 36 | newg.startpc = fn.fn 37 | if _g_.m.curg != nil { 38 | newg.labels = _g_.m.curg.labels 39 | } 40 | ``` 41 | 42 | 首先,`memclrNoHeapPointers` 将 newg.sched 的内存全部清零。接着,设置 sched 的 sp 字段,当 goroutine 被调度到 m 上运行时,需要通过 sp 字段来指示栈顶的位置,这里设置的就是新栈的栈顶位置。 43 | 44 | 最关键的一行来了: 45 | 46 | ```golang 47 | // newg.sched.pc 表示当 newg 被调度起来运行时从这个地址开始执行指令 48 | newg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function 49 | ``` 50 | 51 | 设置 `pc` 字段为函数 `goexit` 的地址加 1,也说是 `goexit` 函数的第二条指令,`goexit` 函数是 `goroutine` 退出后的一些清理工作。有点奇怪,这是要干嘛?接着往后看。 52 | 53 | ```golang 54 | newg.sched.g = guintptr(unsafe.Pointer(newg)) 55 | ``` 56 | 57 | 设置 `g` 字段为 newg 的地址。插一句,sched 是 g 结构体的一个字段,它本身也是一个结构体,保存调度信息。复习一下: 58 | 59 | ```golang 60 | type gobuf struct { 61 | // 存储 rsp 寄存器的值 62 | sp uintptr 63 | // 存储 rip 寄存器的值 64 | pc uintptr 65 | // 指向 goroutine 66 | g guintptr 67 | ctxt unsafe.Pointer // this has to be a pointer so that gc scans it 68 | // 保存系统调用的返回值 69 | ret sys.Uintreg 70 | lr uintptr 71 | bp uintptr // for GOEXPERIMENT=framepointer 72 | } 73 | ``` 74 | 75 | 接下来的这个函数非常重要,可以解释之前为什么要那样设置 `pc` 字段的值。调用 `gostartcallfn`: 76 | 77 | ```golang 78 | gostartcallfn(&newg.sched, fn) //调整sched成员和newg的栈 79 | ``` 80 | 81 | 传入 newg.sched 和 fn。 82 | 83 | ```golang 84 | func gostartcallfn(gobuf *gobuf, fv *funcval) { 85 | var fn unsafe.Pointer 86 | if fv != nil { 87 | // fn: gorotine 的入口地址,初始化时对应的是 runtime.main 88 | fn = unsafe.Pointer(fv.fn) 89 | } else { 90 | fn = unsafe.Pointer(funcPC(nilfunc)) 91 | } 92 | gostartcall(gobuf, fn, unsafe.Pointer(fv)) 93 | } 94 | 95 | func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) { 96 | // newg 的栈顶,目前 newg 栈上只有 fn 函数的参数,sp 指向的是 fn 的第一参数 97 | sp := buf.sp 98 | 99 | // ………………………… 100 | 101 | // 为返回地址预留空间 102 | sp -= sys.PtrSize 103 | // 这里填的是 newproc1 函数里设置的 goexit 函数的第二条指令 104 | // 伪装 fn 是被 goexit 函数调用的,使得 fn 执行完后返回到 goexit 继续执行,从而完成清理工作 105 | *(*uintptr)(unsafe.Pointer(sp)) = buf.pc 106 | // 重新设置 buf.sp 107 | buf.sp = sp 108 | // 当 goroutine 被调度起来执行时,会从这里的 pc 值开始执行,初始化时就是 runtime.main 109 | buf.pc = uintptr(fn) 110 | buf.ctxt = ctxt 111 | } 112 | ``` 113 | 114 | 函数 `gostartcallfn` 只是拆解出了包含在 funcval 结构体里的函数指针,转过头就调用 `gostartcall`。将 sp 减小了一个指针的位置,这是给返回地址留空间。果然接着就把 buf.pc 填入了栈顶的位置: 115 | 116 | ```golang 117 | *(*uintptr)(unsafe.Pointer(sp)) = buf.pc 118 | ``` 119 | 120 | 原来 buf.pc 只是做了一个搬运工,搞什么啊。重新设置 buf.sp 为送减掉一个指针位置之后的值,设置 buf.pc 为 fn,指向要执行的函数,这里就是指的 runtime.main 函数。 121 | 122 | 对嘛,这才是应有的操作。之后,当调度器“光顾”此 goroutine 时,取出 buf.sp 和 buf.pc,恢复 CPU 相应的寄存器,就可以构造出 goroutine 的运行环境。 123 | 124 | 而 goexit 函数也通过“偷天换日”将自己的地址“强行”放到 newg 的栈顶,达到自己不可告人的目的:每个 goroutine 执行完之后,都要经过我的一些清理工作,才能“放行”。这样一说,goexit 函数还真是无私,默默地做一些“扫尾”的工作。 125 | 126 | 设置完 newg.sched 这后,我们的图又可以前进一步: 127 | 128 | ![设置 newg.sched](https://user-images.githubusercontent.com/7698088/64071278-73738280-cca9-11e9-9a67-2570ceea3724.png) 129 | 130 | 上图中,newg 新增了 sched.pc 指向 `runtime.main` 函数,当它被调度起来执行时,就从这里开始;新增了 sched.sp 指向了 newg 栈顶位置,同时,newg 栈顶位置的内容是一个跳转地址,指向 `runtime.goexit` 的第二条指令,当 goroutine 退出时,这条地址会载入 CPU 的 PC 寄存器,跳转到这里执行“扫尾”工作。 131 | 132 | 之后,将 newg 的状态改为 runnable,设置 goroutine 的 id: 133 | 134 | ```golang 135 | // 设置 g 的状态为 _Grunnable,可以运行了 136 | casgstatus(newg, _Gdead, _Grunnable) 137 | newg.goid = int64(_p_.goidcache) 138 | ``` 139 | 140 | 每个 P 每次会批量(16个)申请 id,每次调用 newproc 函数,新创建一个 goroutine,id 加 1。因此 g0 的 id 是 0,而 main goroutine 的 id 就是 1。 141 | 142 | `newg` 的状态变成可执行后(Runnable),就可以将它加入到 P 的本地运行队列里,等待调度。所以,goroutine 何时被执行,用户代码决定不了。来看源码: 143 | 144 | ```golang 145 | // 将 G 放入 _p_ 的本地待运行队列 146 | runqput(_p_, newg, true) 147 | 148 | // runqput 尝试将 g 放到本地可执行队列里。 149 | // 如果 next 为假,runqput 将 g 添加到可运行队列的尾部 150 | // 如果 next 为真,runqput 将 g 添加到 p.runnext 字段 151 | // 如果 run queue 满了,runnext 将 g 放到全局队列里 152 | // 153 | // runnext 成员中的 goroutine 会被优先调度起来运行 154 | func runqput(_p_ *p, gp *g, next bool) { 155 | // …………………… 156 | 157 | if next { 158 | retryNext: 159 | oldnext := _p_.runnext 160 | if !_p_.runnext.cas(oldnext, guintptr(unsafe.Pointer(gp))) { 161 | // 有其它线程在操作 runnext 成员,需要重试 162 | goto retryNext 163 | } 164 | // 老的 runnext 为 nil,不用管了 165 | if oldnext == 0 { 166 | return 167 | } 168 | // 把之前的 runnext 踢到正常的 runq 中 169 | // 原本存放在 runnext 的 gp 放入 runq 的尾部 170 | gp = oldnext.ptr() 171 | } 172 | 173 | retry: 174 | h := atomic.Load(&_p_.runqhead) // load-acquire, synchronize with consumers 175 | t := _p_.runqtail 176 | // 如果 P 的本地队列没有满,入队 177 | if t-h < uint32(len(_p_.runq)) { 178 | _p_.runq[t%uint32(len(_p_.runq))].set(gp) 179 | // 原子写入 180 | atomic.Store(&_p_.runqtail, t+1) // store-release, makes the item available for consumption 181 | return 182 | } 183 | // 可运行队列已经满了,放入全局队列了 184 | if runqputslow(_p_, gp, h, t) { 185 | return 186 | } 187 | // the queue is not full, now the put above must succeed 188 | // 没有成功放入全局队列,说明本地队列没满,重试一下 189 | goto retry 190 | } 191 | ``` 192 | 193 | `runqput` 函数的主要作用就是将新创建的 goroutine 加入到 P 的可运行队列,如果本地队列满了,则加入到全局可运行队列。前两个参数都好理解,最后一个参数 `next` 的作用是,当它为 true 时,会将 newg 加入到 P 的 runnext 字段,具有最高优先级,将先于普通队列中的 goroutine 得到执行。 194 | 195 | 先将 P 老的 runnext 成员取出,接着用一个原子操作 cas 来试图将 runnext 成员设置成 newg,目的是防止其他线程在同时修改 runnext 字段。 196 | 197 | 设置成功之后,相当于 newg “挤掉” 了原来老的处于 runnext 的 goroutine,还得给人遣散费,安顿好人家嘛,不然和强盗有何区别? 198 | 199 | “安顿”的动作在 retry 代码段中执行。先通过 `head`,`tail`,`len(_p_.runq)` 来判断队列是否已满,如果没满,则直接写到队列尾部,同时修改队列尾部的指针。 200 | 201 | ```golang 202 | // store-release, makes it available for consumption 203 | atomic.Store(&_p_.runqtail, t+1) 204 | ``` 205 | 206 | 这里使用原子操作写入 runtail,防止编译器和 CPU 指令重排,保证上一行代码对 runq 的修改发生在修改 runqtail 之前,并且保证当前线程对队列的修改对其它线程立即可见。 207 | 208 | 如果本地队列满了,那就只能试图将 newg 添加到全局可运行队列中了。调用 `runqputslow(_p_, gp, h, t)` 完成。 209 | 210 | ```golang 211 | // 将 g 和 _p_ 本地队列的一半 goroutine 放入全局队列。 212 | // 因为要获取锁,所以会慢 213 | func runqputslow(_p_ *p, gp *g, h, t uint32) bool { 214 | var batch [len(_p_.runq)/2 + 1]*g 215 | 216 | // First, grab a batch from local queue. 217 | n := t - h 218 | n = n / 2 219 | if n != uint32(len(_p_.runq)/2) { 220 | throw("runqputslow: queue is not full") 221 | } 222 | for i := uint32(0); i < n; i++ { 223 | batch[i] = _p_.runq[(h+i)%uint32(len(_p_.runq))].ptr() 224 | } 225 | // 如果 cas 操作失败,说明本地队列不满了,直接返回 226 | if !atomic.Cas(&_p_.runqhead, h, h+n) { // cas-release, commits consume 227 | return false 228 | } 229 | batch[n] = gp 230 | 231 | // ………………………… 232 | 233 | // Link the goroutines. 234 | // 全局运行队列是一个链表,这里首先把所有需要放入全局运行队列的 g 链接起来, 235 | // 减小锁粒度,从而降低锁冲突,提升性能 236 | for i := uint32(0); i < n; i++ { 237 | batch[i].schedlink.set(batch[i+1]) 238 | } 239 | 240 | // Now put the batch on global queue. 241 | lock(&sched.lock) 242 | globrunqputbatch(batch[0], batch[n], int32(n+1)) 243 | unlock(&sched.lock) 244 | return true 245 | } 246 | ``` 247 | 248 | 先将 P 本地队列里所有的 goroutine 加入到一个数组中,数组长度为 `len(_p_.runq)/2 + 1`,也就是 runq 的一半加上 newg。 249 | 250 | 接着,将从 runq 的头部开始的前一半 goroutine 存入 bacth 数组。然后,使用原子操作尝试修改 P 的队列头,因为出队了一半 goroutine,所以 head 要向后移动 1/2 的长度。如果修改失败,说明 runq 的本地队列被其他线程修改了,因此后面的操作就不进行了,直接返回 false,表示 newg 没被添加进来。 251 | 252 | ```golang 253 | batch[n] = gp 254 | ``` 255 | 256 | 将 newg 本身添加到数组。 257 | 258 | 通过循环将 batch 数组里的所有 g 串成链表: 259 | 260 | ```golang 261 | for i := uint32(0); i < n; i++ { 262 | batch[i].schedlink.set(batch[i+1]) 263 | } 264 | ``` 265 | 266 | ![批量 goroutine 连接成链表](https://user-images.githubusercontent.com/7698088/63630942-09c4fa00-c653-11e9-8919-dc6b8eb957f1.png) 267 | 268 | 最后,将链表添加到全局队列中。由于操作的是全局队列,因此需要获取锁,因为存在竞争,所以代价较高。这也是本地可运行队列存在的原因。调用 `globrunqputbatch(batch[0], batch[n], int32(n+1))`: 269 | 270 | ```golang 271 | // Put a batch of runnable goroutines on the global runnable queue. 272 | // Sched must be locked. 273 | func globrunqputbatch(ghead *g, gtail *g, n int32) { 274 | gtail.schedlink = 0 275 | if sched.runqtail != 0 { 276 | sched.runqtail.ptr().schedlink.set(ghead) 277 | } else { 278 | sched.runqhead.set(ghead) 279 | } 280 | sched.runqtail.set(gtail) 281 | sched.runqsize += n 282 | } 283 | ``` 284 | 285 | 如果全局的队列尾 `sched.runqtail` 不为空,则直接将其和前面生成的链表头相接,否则说明全局的可运行列队为空,那就直接将前面生成的链表头设置到 sched.runqhead。 286 | 287 | 最后,再设置好队列尾,增加 runqsize。 288 | 289 | 设置完成之后: 290 | 291 | ![放到全局可运行队列](https://user-images.githubusercontent.com/7698088/63630946-0f224480-c653-11e9-9f97-ce12db645399.png) 292 | 293 | 再回到 `runqput` 函数,如果将 newg 添加到全局队列失败了,说明本地队列在此过程中发生了变化,又有了位置可以添加 newg,因此重试 retry 代码段。我们也可以发现,P 的本地可运行队列的长度为 256,它是一个循环队列,因此最多只能放下 256 个 goroutine。 294 | 295 | 因为本文还是处于初始化的场景,所以 newg 被成功放入 p0 的本地可运行队列,等待被调度。 296 | 297 | 将我们的图再完善一下: 298 | 299 | ![newg 添加到本地 runq](https://user-images.githubusercontent.com/7698088/64071321-699e4f00-ccaa-11e9-9ef0-b18bafcb7806.png) 300 | 301 | # 参考资料 302 | 【阿波张 Go语言调度器之调度 main 】https://mp.weixin.qq.com/s/8eJm5hjwKXya85VnT4y8Cw -------------------------------------------------------------------------------- /goroutine 调度器/GPM 是什么.md: -------------------------------------------------------------------------------- 1 | G、P、M 是 Go 调度器的三个核心组件,各司其职。在它们精密地配合下,Go 调度器得以高效运转,这也是 Go 天然支持高并发的内在动力。今天这篇文章我们来深入理解 GPM 模型。 2 | 3 | 先看 G,取 goroutine 的首字母,主要保存 goroutine 的一些状态信息以及 CPU 的一些寄存器的值,例如 IP 寄存器,以便在轮到本 goroutine 执行时,CPU 知道要从哪一条指令处开始执行。 4 | 5 | > 当 goroutine 被调离 CPU 时,调度器负责把 CPU 寄存器的值保存在 g 对象的成员变量之中。 6 | 7 | >当 goroutine 被调度起来运行时,调度器又负责把 g 对象的成员变量所保存的寄存器值恢复到 CPU 的寄存器。 8 | 9 | 本系列使用的代码版本是 1.9.2,来看一下 g 的源码: 10 | 11 | ```golang 12 | type g struct { 13 | 14 | // goroutine 使用的栈 15 | stack stack // offset known to runtime/cgo 16 | // 用于栈的扩张和收缩检查,抢占标志 17 | stackguard0 uintptr // offset known to liblink 18 | stackguard1 uintptr // offset known to liblink 19 | 20 | _panic *_panic // innermost panic - offset known to liblink 21 | _defer *_defer // innermost defer 22 | // 当前与 g 绑定的 m 23 | m *m // current m; offset known to arm liblink 24 | // goroutine 的运行现场 25 | sched gobuf 26 | syscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gc 27 | syscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gc 28 | stktopsp uintptr // expected sp at top of stack, to check in traceback 29 | // wakeup 时传入的参数 30 | param unsafe.Pointer // passed parameter on wakeup 31 | atomicstatus uint32 32 | stackLock uint32 // sigprof/scang lock; TODO: fold in to atomicstatus 33 | goid int64 34 | // g 被阻塞之后的近似时间 35 | waitsince int64 // approx time when the g become blocked 36 | // g 被阻塞的原因 37 | waitreason string // if status==Gwaiting 38 | // 指向全局队列里下一个 g 39 | schedlink guintptr 40 | // 抢占调度标志。这个为 true 时,stackguard0 等于 stackpreempt 41 | preempt bool // preemption signal, duplicates stackguard0 = stackpreempt 42 | paniconfault bool // panic (instead of crash) on unexpected fault address 43 | preemptscan bool // preempted g does scan for gc 44 | gcscandone bool // g has scanned stack; protected by _Gscan bit in status 45 | gcscanvalid bool // false at start of gc cycle, true if G has not run since last scan; TODO: remove? 46 | throwsplit bool // must not split stack 47 | raceignore int8 // ignore race detection events 48 | sysblocktraced bool // StartTrace has emitted EvGoInSyscall about this goroutine 49 | // syscall 返回之后的 cputicks,用来做 tracing 50 | sysexitticks int64 // cputicks when syscall has returned (for tracing) 51 | traceseq uint64 // trace event sequencer 52 | tracelastp puintptr // last P emitted an event for this goroutine 53 | // 如果调用了 LockOsThread,那么这个 g 会绑定到某个 m 上 54 | lockedm *m 55 | sig uint32 56 | writebuf []byte 57 | sigcode0 uintptr 58 | sigcode1 uintptr 59 | sigpc uintptr 60 | // 创建该 goroutine 的语句的指令地址 61 | gopc uintptr // pc of go statement that created this goroutine 62 | // goroutine 函数的指令地址 63 | startpc uintptr // pc of goroutine function 64 | racectx uintptr 65 | waiting *sudog // sudog structures this g is waiting on (that have a valid elem ptr); in lock order 66 | cgoCtxt []uintptr // cgo traceback context 67 | labels unsafe.Pointer // profiler labels 68 | // time.Sleep 缓存的定时器 69 | timer *timer // cached timer for time.Sleep 70 | 71 | gcAssistBytes int64 72 | } 73 | ``` 74 | 75 | 源码中,比较重要的字段我已经作了注释,其他未作注释的与调度关系不大或者我暂时也没有理解的。 76 | 77 | `g` 结构体关联了两个比较简单的结构体,stack 表示 goroutine 运行时的栈: 78 | 79 | ```golang 80 | // 描述栈的数据结构,栈的范围:[lo, hi) 81 | type stack struct { 82 | // 栈顶,低地址 83 | lo uintptr 84 | // 栈低,高地址 85 | hi uintptr 86 | } 87 | ``` 88 | 89 | Goroutine 运行时,光有栈还不行,至少还得包括 PC,SP 等寄存器,gobuf 就保存了这些值: 90 | 91 | ```golang 92 | type gobuf struct { 93 | // 存储 rsp 寄存器的值 94 | sp uintptr 95 | // 存储 rip 寄存器的值 96 | pc uintptr 97 | // 指向 goroutine 98 | g guintptr 99 | ctxt unsafe.Pointer // this has to be a pointer so that gc scans it 100 | // 保存系统调用的返回值 101 | ret sys.Uintreg 102 | lr uintptr 103 | bp uintptr // for GOEXPERIMENT=framepointer 104 | } 105 | ``` 106 | 107 | 再来看 M,取 machine 的首字母,它代表一个工作线程,或者说系统线程。G 需要调度到 M 上才能运行,M 是真正工作的人。结构体 m 就是我们常说的 M,它保存了 M 自身使用的栈信息、当前正在 M 上执行的 G 信息、与之绑定的 P 信息…… 108 | 109 | 当 M 没有工作可做的时候,在它休眠前,会“自旋”地来找工作:检查全局队列,查看 network poller,试图执行 gc 任务,或者“偷”工作。 110 | 111 | 结构体 m 的源码如下: 112 | 113 | ```golang 114 | // m 代表工作线程,保存了自身使用的栈信息 115 | type m struct { 116 | // 记录工作线程(也就是内核线程)使用的栈信息。在执行调度代码时需要使用 117 | // 执行用户 goroutine 代码时,使用用户 goroutine 自己的栈,因此调度时会发生栈的切换 118 | g0 *g // goroutine with scheduling stack/ 119 | morebuf gobuf // gobuf arg to morestack 120 | divmod uint32 // div/mod denominator for arm - known to liblink 121 | 122 | // Fields not known to debuggers. 123 | procid uint64 // for debuggers, but offset not hard-coded 124 | gsignal *g // signal-handling g 125 | sigmask sigset // storage for saved signal mask 126 | // 通过 tls 结构体实现 m 与工作线程的绑定 127 | // 这里是线程本地存储 128 | tls [6]uintptr // thread-local storage (for x86 extern register) 129 | mstartfn func() 130 | // 指向正在运行的 gorutine 对象 131 | curg *g // current running goroutine 132 | caughtsig guintptr // goroutine running during fatal signal 133 | // 当前工作线程绑定的 p 134 | p puintptr // attached p for executing go code (nil if not executing go code) 135 | nextp puintptr 136 | id int32 137 | mallocing int32 138 | throwing int32 139 | // 该字段不等于空字符串的话,要保持 curg 始终在这个 m 上运行 140 | preemptoff string // if != "", keep curg running on this m 141 | locks int32 142 | softfloat int32 143 | dying int32 144 | profilehz int32 145 | helpgc int32 146 | // 为 true 时表示当前 m 处于自旋状态,正在从其他线程偷工作 147 | spinning bool // m is out of work and is actively looking for work 148 | // m 正阻塞在 note 上 149 | blocked bool // m is blocked on a note 150 | // m 正在执行 write barrier 151 | inwb bool // m is executing a write barrier 152 | newSigstack bool // minit on C thread called sigaltstack 153 | printlock int8 154 | // 正在执行 cgo 调用 155 | incgo bool // m is executing a cgo call 156 | fastrand uint32 157 | // cgo 调用总计数 158 | ncgocall uint64 // number of cgo calls in total 159 | ncgo int32 // number of cgo calls currently in progress 160 | cgoCallersUse uint32 // if non-zero, cgoCallers in use temporarily 161 | cgoCallers *cgoCallers // cgo traceback if crashing in cgo call 162 | // 没有 goroutine 需要运行时,工作线程睡眠在这个 park 成员上, 163 | // 其它线程通过这个 park 唤醒该工作线程 164 | park note 165 | // 记录所有工作线程的链表 166 | alllink *m // on allm 167 | schedlink muintptr 168 | mcache *mcache 169 | lockedg *g 170 | createstack [32]uintptr // stack that created this thread. 171 | freglo [16]uint32 // d[i] lsb and f[i] 172 | freghi [16]uint32 // d[i] msb and f[i+16] 173 | fflag uint32 // floating point compare flags 174 | locked uint32 // tracking for lockosthread 175 | // 正在等待锁的下一个 m 176 | nextwaitm uintptr // next m waiting for lock 177 | needextram bool 178 | traceback uint8 179 | waitunlockf unsafe.Pointer // todo go func(*g, unsafe.pointer) bool 180 | waitlock unsafe.Pointer 181 | waittraceev byte 182 | waittraceskip int 183 | startingtrace bool 184 | syscalltick uint32 185 | // 工作线程 id 186 | thread uintptr // thread handle 187 | 188 | // these are here because they are too large to be on the stack 189 | // of low-level NOSPLIT functions. 190 | libcall libcall 191 | libcallpc uintptr // for cpu profiler 192 | libcallsp uintptr 193 | libcallg guintptr 194 | syscall libcall // stores syscall parameters on windows 195 | 196 | mOS 197 | } 198 | ``` 199 | 200 | 再来看 P,取 processor 的首字母,为 M 的执行提供“上下文”,保存 M 执行 G 时的一些资源,例如本地可运行 G 队列,memeory cache 等。 201 | 202 | 一个 M 只有绑定 P 才能执行 goroutine,当 M 被阻塞时,整个 P 会被传递给其他 M ,或者说整个 P 被接管。 203 | 204 | ```golang 205 | // p 保存 go 运行时所必须的资源 206 | type p struct { 207 | lock mutex 208 | 209 | // 在 allp 中的索引 210 | id int32 211 | status uint32 // one of pidle/prunning/... 212 | link puintptr 213 | // 每次调用 schedule 时会加一 214 | schedtick uint32 215 | // 每次系统调用时加一 216 | syscalltick uint32 217 | // 用于 sysmon 线程记录被监控 p 的系统调用时间和运行时间 218 | sysmontick sysmontick // last tick observed by sysmon 219 | // 指向绑定的 m,如果 p 是 idle 的话,那这个指针是 nil 220 | m muintptr // back-link to associated m (nil if idle) 221 | mcache *mcache 222 | racectx uintptr 223 | 224 | deferpool [5][]*_defer // pool of available defer structs of different sizes (see panic.go) 225 | deferpoolbuf [5][32]*_defer 226 | 227 | // Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen. 228 | goidcache uint64 229 | goidcacheend uint64 230 | 231 | // Queue of runnable goroutines. Accessed without lock. 232 | // 本地可运行的队列,不用通过锁即可访问 233 | runqhead uint32 // 队列头 234 | runqtail uint32 // 队列尾 235 | // 使用数组实现的循环队列 236 | runq [256]guintptr 237 | 238 | // runnext 非空时,代表的是一个 runnable 状态的 G, 239 | // 这个 G 被 当前 G 修改为 ready 状态,相比 runq 中的 G 有更高的优先级。 240 | // 如果当前 G 还有剩余的可用时间,那么就应该运行这个 G 241 | // 运行之后,该 G 会继承当前 G 的剩余时间 242 | runnext guintptr 243 | 244 | // Available G's (status == Gdead) 245 | // 空闲的 g 246 | gfree *g 247 | gfreecnt int32 248 | 249 | sudogcache []*sudog 250 | sudogbuf [128]*sudog 251 | 252 | tracebuf traceBufPtr 253 | traceSwept, traceReclaimed uintptr 254 | 255 | palloc persistentAlloc // per-P to avoid mutex 256 | 257 | // Per-P GC state 258 | gcAssistTime int64 // Nanoseconds in assistAlloc 259 | gcBgMarkWorker guintptr 260 | gcMarkWorkerMode gcMarkWorkerMode 261 | runSafePointFn uint32 // if 1, run sched.safePointFn at next safe point 262 | 263 | pad [sys.CacheLineSize]byte 264 | } 265 | ``` 266 | 267 | GPM 三足鼎力,共同成就 Go scheduler。G 需要在 M 上才能运行,M 依赖 P 提供的资源,P 则持有待运行的 G。你中有我,我中有你。 268 | 269 | 描述三者的关系: 270 | 271 | ![曹大 golang notes GPM 三者关系](https://user-images.githubusercontent.com/7698088/63308368-eb928d80-c324-11e9-989d-71af7d03bece.png) 272 | 273 | M 会从与它绑定的 P 的本地队列获取可运行的 G,也会从 network poller 里获取可运行的 G,还会从其他 P 偷 G。 274 | 275 | 最后我们从宏观上总结一下 GPM,这篇文章尝试从它们的状态流转角度总结。 276 | 277 | 首先是 G 的状态流转: 278 | 279 | ![G 的状态流转图](https://user-images.githubusercontent.com/7698088/64057782-d98dd600-cbd3-11e9-918d-8320fd9609c0.png) 280 | 281 | 说明一下,上图省略了一些垃圾回收的状态。 282 | 283 | 接着是 P 的状态流转: 284 | 285 | ![P 的状态流转图](https://user-images.githubusercontent.com/7698088/64058164-93d40c00-cbd9-11e9-9095-7bc7248a0fb9.png) 286 | 287 | > 通常情况下(在程序运行时不调整 P 的个数),P 只会在上图中的四种状态下进行切换。 当程序刚开始运行进行初始化时,所有的 P 都处于 `_Pgcstop` 状态, 随着 P 的初始化(`runtime.procresize`),会被置于 `_Pidle`。 288 | 289 | > 当 M 需要运行时,会 `runtime.acquirep` 来使 P 变成 `Prunning` 状态,并通过 `runtime.releasep` 来释放。 290 | 291 | > 当 G 执行时需要进入系统调用,P 会被设置为 `_Psyscall`, 如果这个时候被系统监控抢夺(`runtime.retake`),则 P 会被重新修改为 `_Pidle`。 292 | 293 | > 如果在程序运行中发生 `GC`,则 P 会被设置为 `_Pgcstop`, 并在 `runtime.startTheWorld` 时重新调整为 `_Prunning`。 294 | 295 | 最后,我们来看 M 的状态变化: 296 | 297 | ![M 的状态流转图](https://user-images.githubusercontent.com/7698088/64058333-09d97280-cbdc-11e9-8a4d-1843d5be88d0.png) 298 | 299 | M 只有自旋和非自旋两种状态。自旋的时候,会努力找工作;找不到的时候会进入非自旋状态,之后会休眠,直到有工作需要处理时,被其他工作线程唤醒,又进入自旋状态。 --------------------------------------------------------------------------------