├── .gitignore ├── README.md └── articles ├── appendix ├── 1-source.md └── source │ ├── source-0.png │ └── source-1.png ├── compiler └── inline.md ├── concurrent ├── channel.md ├── mutex.md ├── syncMap.md ├── syncPool.md └── waitGroup.md ├── container ├── array.md ├── array │ └── mem-array.png ├── map.md ├── slice.md └── slice │ ├── slice-1.png │ ├── slice-2.png │ ├── slice-3.png │ ├── slice-4.png │ ├── slice-5.png │ ├── slice-6.png │ ├── slice-7.png │ ├── slice-8.png │ └── slice-9.png ├── go-vet └── copylock.md └── struct.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .idea/ 3 | /test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Go剖析系列 2 | === 3 | ## 简介 4 | 从编译器/runtime/程序员等多个角度了解Go语言内部原理。 5 | 6 | ## 目录 7 | ### 编译器 8 | - [内联优化](./articles/compiler/inline.md) 9 | ### 数据结构 10 | - [切片剖析](./articles/container/slice.md) 11 | - [数组剖析](./articles/container/array.md) 12 | - [map剖析(写作中)](./articles/container/map.md) 13 | 14 | ### 并发 15 | - [channel剖析(写作中)](./articles/concurrent/channel.md) 16 | 17 | 18 | 19 | ### 静态分析 20 | 21 | ### 附录 22 | - [附录1: 如何寻找源码位置](./articles/appendix/1-source.md) 23 | - [静态分析: 不要复制我的结构体!](./articles/go-vet/copylock.md) 24 | ## 推荐阅读 25 | **排名不分先后** 26 | 27 | - [Go语言原本](https://golang.design/under-the-hood/) 28 | - [Go语言问题集](https://golang.design/go-questions/) 29 | - [Go语言高性能编程](https://geektutu.com/post/high-performance-go.html) 30 | - [Golang编译器代码浅析](https://gocompiler.shizhz.me/) 31 | - [Go语言设计与实现](https://draveness.me/golang/) 32 | - [Go语言高级编程](https://github.com/chai2010/advanced-go-programming-book) 33 | - [Go语言规范](https://go.dev/ref/spec#Slice_types) 34 | - [Go语法树入门](https://github.com/chai2010/go-ast-book) 35 | 36 | ## 关于 37 | 本系列文章首发于微信公众号:**比特要塞**,由于微信公众号无法对文章进行更新,所以如果我在学习的过程中对过去的章节有什么新的收获,会通过本仓库进行更新 38 | -------------------------------------------------------------------------------- /articles/appendix/1-source.md: -------------------------------------------------------------------------------- 1 | 如何寻找内建函数源码位置 2 | === 3 | 你有没有遇到过下面这些问题: 4 | - 想查看Go内建函数的实现,IDE跟踪进去后却发现只有函数声明,没有实现代码 5 | - 想了解不同的写法Go编译器是如何进行处理的 6 | - 内建函数被展开之前和之后都做了些什么工作 7 | - 看程序的汇编代码时,同样都是`make(map)`,怎么有时候会被翻译为`runtime.makemap`,有时候却什么都找不到 8 | 9 | 本篇文章可以帮助你解决这些问题。 10 | 11 | **为了帮助基础薄弱的同学,每个小节都会加上实际的代码进行分析。** 12 | 13 | 注:本文写作时的Go版本为: 14 | `go version go1.17.5 linux/amd64`。 15 | # 查看Go内建函数的实现 16 | 初学Go时,你是否好奇过make函数的实现 17 | ```go 18 | // make slice 19 | slice := make([]int,20) 20 | // make channel 21 | channel := make(chan int,3) 22 | ``` 23 | 这个神奇的函数能用来创建好多东西,但当你在IDE里点进去后,却发现它只有一行函数声明: 24 | ```go 25 | func make(t Type, size ...IntegerType) Type 26 | ``` 27 | 于是你的探索就止步于此了... 28 | 29 | 现在让我们看看,有哪些寻找内建函数的源码: 30 | ### 方法1:使用汇编 31 | **场景代码** 32 | 33 | ```go 34 | // foo.go 35 | func foo()[]int { 36 | slice := make([]int, 3) 37 | return slice 38 | } 39 | ``` 40 | 41 | **查看方式** 42 | 使用以下方式查看`foo.go`的汇编指令 43 | ```shell 44 | $ go tool compile -S foo.go 45 | ``` 46 | 下方是该命令生成的结果(省略了部分) 47 | ```c 48 | ... 49 | 0x0023 00035 (./foo.go:4) PCDATA $1, $0 50 | 0x0023 00035 (./foo.go:4) CALL runtime.makeslice(SB) 51 | 0x0028 00040 (./foo.go:5) MOVL $3, BX 52 | ... 53 | ``` 54 | 我们要找的答案就在`CALL runtime.makeslice`这一行,说明我们的`make([]int,3)`最后调用的是`runtime.makeslice`函数(当然,除了这个函数外,编译器也做了一些别的工作,我们后面会介绍。) 55 | 56 | 或者使用反汇编将编译好的文件重新转换为汇编指令: 57 | ```shell 58 | $ go tool objdump -s "foo" foo.o 59 | ``` 60 | 标志`-s`支持正则表达式,动手试一下寻找上文的`runtime.makeslice`吧! 61 | 62 | 这里你可能会疑惑,`foo.o`这个文件是怎么出现的? 63 | 64 | `foo.o`其实是上一步`go tool compile`的产物,在实际使用中,你可以用`go build xx.go`后的`xx.exe`来代替`foo.o`。(这里可能有点绕口,意思就是你可以直接`go build main.go`,然后会生成一个`main.exe`,再使用`go tool objdump -s "main\.main" main.exe`就可以查看main函数对应的汇编指令了)。 65 | 66 | 经常会有人问,`go tool compile -s`和`go tool objdump`产生的汇编有什么区别?仔细观察两个命令的输出可以发现,`go tool compile`生成的是还未链接的汇编指令,只有偏移量,并未赋予实际的内存地址。而`go tool objdump`由于是从编译好的二进制文件反汇编而成的,所以是有这些东西的。 67 | 68 | ### 方法2:查看SSA 69 | **场景代码** 70 | 71 | ```go 72 | // main.go 73 | func foo() (string, bool) { 74 | // 大小尽量大一些,否则如果map分配到栈上,后面的内容可能对不上 75 | bootun := make(map[string]string,10) 76 | bootun["pet"] = "大黄" 77 | dream, ok := bootun["dream"] 78 | return dream, ok 79 | } 80 | ``` 81 | 82 | **查看方式** 83 | ```shell 84 | $ GOSSAFUNC=foo go build main.go 85 | ``` 86 | 该命令在目录下生成一个`ssa.html`,里面记录着数十个SSA的优化过程以及最终的SSA: 87 | 88 | ![](source/source-0.png) 89 | 90 | 我们可以点击源码进行颜色标记,其他阶段对应的代码位置也会标记上相应的颜色。如下图所示: 91 | 92 | ![](source/source-1.png) 93 | 我将几个关键的指令用红框圈了出来。可以看到,`make([]int, 10)`变成了`runtime.makemap`,对map的赋值底层调用了`runtime.mapassign`函数,两个返回值的map访问底层调用了`runtime.mapaccess2`函数,该函数有**两个**返回值(对,你猜没错,单返回值调用的是`runtime.mapaccess1`,该函数只有一个返回值,访问map的两种方式其实对应了不同的函数,这只是Go提供给我们的语法糖罢了)。 94 | 95 | ### 方法3:跟踪Go编译器源码 96 | **场景代码** 97 | ```go 98 | func foo() ([]int,[]int) { 99 | s1 := make([]int,10) 100 | s2 := []int{1,2,3,4,5} 101 | return s1,s2 102 | } 103 | ``` 104 | **查看方式** 105 | 这种方法需要你对**编译原理**和**Go编译器**有一些了解,不过别担心,跟着我一步一步走,我会带你了解内置函数翻译过程中的几个**主要节点**。 106 | 107 | Go编译器源码的位置在`$GOROOT/src/cmd/compile/internal/gc`目录下(此处的gc指Go compiler),入口函数是`/cmd/compile/internal/gc/main.go`中的`Main`函数,篇幅原因,这里我们只介绍上述代码所经过的几个关键节点,如果想详细了解Go编译过程,可以参考[《Go语言设计与实现》](https://draveness.me/golang/ "《Go语言设计与实现》")一书。 108 | 109 | 语法分析和类型检查的入口在上述`Main`函数的这一行: 110 | ```go 111 | // Parse and typecheck input. 112 | noder.LoadPackage(flag.Args()) 113 | ``` 114 | 这个函数内可以看到语法分析和类型检查的不同阶段: 115 | ```go 116 | // Phase 1: const, type, and names and types of funcs. 117 | // This will gather all the information about types 118 | // and methods but doesn't depend on any of it. 119 | // 120 | // We also defer type alias declarations until phase 2 121 | // to avoid cycles like #18640. 122 | // TODO(gri) Remove this again once we have a fix for #25838. 123 | 124 | // Don't use range--typecheck can add closures to Target.Decls. 125 | base.Timer.Start("fe", "typecheck", "top1") 126 | for i := 0; i < len(typecheck.Target.Decls); i++ { 127 | n := typecheck.Target.Decls[i] 128 | if op := n.Op(); op != ir.ODCL && op != ir.OAS && op != ir.OAS2 && (op != ir.ODCLTYPE || !n.(*ir.Decl).X.Alias()) { 129 | typecheck.Target.Decls[i] = typecheck.Stmt(n) 130 | } 131 | } 132 | ``` 133 | 无需阅读源码,通过注释我们就能看出来第一阶段处理的内容,因为代码相似,这里只展示第一阶段的代码。 134 | 135 | 此处我们只需要关注 136 | `typecheck.Target.Decls[i] = typecheck.Stmt(n)`这行代码,这里是进入类型检查的入口,该函数其实是`cmd/compile/internal/typecheck/typecheck.go`下`typecheck`函数的包装函数,`typecheck`函数又会调用`typecheck1`函数,我们的要找的第一部分内容就在这里啦。 137 | 138 | `typecheck1`函数下有这样一处地方: 139 | ```go 140 | case ir.OMAKE: 141 | n := n.(*ir.CallExpr) 142 | return tcMake(n) 143 | ``` 144 | 表示当前处理的节点是`OMAKE`时的情景,让我们跟踪`tcMake`函数,看看里面都做了些什么: 145 | ```go 146 | // tcMake typechecks an OMAKE node. 147 | func tcMake(n *ir.CallExpr) ir.Node { 148 | ... 149 | switch t.Kind() { 150 | default: 151 | base.Errorf("cannot make type %v", t) 152 | case types.TSLICE: 153 | ... 154 | nn = ir.NewMakeExpr(n.Pos(), ir.OMAKESLICE, l, r) 155 | case case types.TMAP: 156 | ... 157 | case types.TCHAN: 158 | ... 159 | ... 160 | } 161 | ``` 162 | 这里我们可以看到,`make`能够创建的类型在这里都有处理,我们只需要关注`slice`的逻辑,`tcMake`函数将当前节点的op更改为了`OMAKESLICE`以便于之后进一步的处理。 163 | 164 | 后面追踪起起来函数太多了,这里只放最后两个,第一个是处理op为`OMAKESLICE`时的场景: 165 | 166 | ```go 167 | func walkExpr1(n ir.Node, init *ir.Nodes) ir.Node { 168 | ... 169 | switch n.Op() { 170 | ... 171 | case ir.OMAKESLICE: 172 | n := n.(*ir.MakeExpr) 173 | return walkMakeSlice(n, init) 174 | ... 175 | } 176 | ``` 177 | 178 | 第二个就是上面函数调用的`walkMakeSlice`函数: 179 | ```go 最终结果 180 | // walkMakeSlice walks an OMAKESLICE node. 181 | func walkMakeSlice(n *ir.MakeExpr, init *ir.Nodes) ir.Node { 182 | ... 183 | fnname = "makeslice" 184 | ... 185 | } 186 | ``` 187 | PS:这个函数下面就是copy的walk函数,当时在[切片剖析](../2-slice.md)一节我们提到了copy函数底层调用了`memmove`,在这里也能看到。 188 | 189 | 此时我们终于看到,`make`在这里展开变为了`makeslice`,这个函数还有展开前后的处理,这里省略掉了。使用这个方法同样可以查看`map`或`channel`等类型的展开过程。 190 | 191 | 192 | 有了上述的这些方法,我们就可以愉快的查阅源码啦。 193 | 194 | ## 课后练习 195 | 196 | ### 1.奇怪的翻译机制 197 | 有时候你会发现,同样是`map/channel/slice`,同样是`make`函数,但是不同条件最终展开的代码可能并不一样。 198 | 比如map有时会被展开为`runtime.makemap_small`,有时则是`runtime.makemap`,甚至有时候什么都没有(比如被分配到栈上),查阅相关函数,看看究竟是哪些因素影响了这一结果。 199 | ### 2.slice字面值展开过程(较难) 200 | **方法3**中我们只分析了`make(slice)`的展开过程,对于使用字面值初始化的方式我们没有解决,请试着寻找编译器对应的处理方法 201 | 关键词:`OSLICELIT` 202 | -------------------------------------------------------------------------------- /articles/appendix/source/source-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootun/go-analysis/a7273e9f45aa8026dc660d9044b0612569d4bc87/articles/appendix/source/source-0.png -------------------------------------------------------------------------------- /articles/appendix/source/source-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootun/go-analysis/a7273e9f45aa8026dc660d9044b0612569d4bc87/articles/appendix/source/source-1.png -------------------------------------------------------------------------------- /articles/compiler/inline.md: -------------------------------------------------------------------------------- 1 | 内联优化 2 | === 3 | 内联优化是一种常见的编译器优化策略,通俗来讲,就是把函数在它被调用的地方展开,这样可以减少函数调用所带来的开销(栈的创建、参数的拷贝等)。 4 | 5 | 当函数/方法被内联时,具体是什么样的表现呢? 6 | 7 | ## 观察内联 8 | 举个例子,现在有以下代码 9 | ```go 10 | // ValidateName 验证给定的用户名是否合法 11 | // 12 | //go:noinline 13 | func ValidateName(name string) bool { // AX: 字符串指针 BX: 字符串长度 14 | if len(name) < 1 { 15 | return false 16 | } else if len(name) > 12 { 17 | return false 18 | } 19 | return true 20 | } 21 | 22 | //go:noinline 23 | func (s *Server) CreateUser(name string, password string) error { 24 | if !ValidateName(name) { 25 | return errors.New("invalid name") 26 | } 27 | // ... 28 | return nil 29 | } 30 | 31 | type Server struct{} 32 | ``` 33 | 为了便于理解,我为函数和方法增加了`//go:noinline`注释。Go编译器在遇到该注释时,不会将函数/方法进行内联处理。我们先看一下禁止内联时,该段代码生成的汇编指令: 34 | ```go 35 | // ... 36 | 37 | // ValidateName函数 38 | // 此时: 39 | // AX寄存器: 指向name字符串数组的指针 40 | // BX寄存器: name字符串的长度 41 | TEXT github.com/bootun/example/user.ValidateName(SB) github.com/bootun/example/user/user.go 42 | user.go:9 0x4602c0 MOVQ AX, 0x8(SP) // 保存name字符串的指针到栈上(后面没有用到) 43 | user.go:10 0x4602c5 TESTQ BX, BX // BX & BX, 用来检测BX是否为0, 等价于:CMPQ 0, BX 44 | user.go:10 0x4602c8 JE 0x4602d9 // 如果为0则跳转到0x4602d9 45 | user.go:12 0x4602ca CMPQ $0xc, BX // 比较常数12和name的长度 46 | user.go:12 0x4602ce JLE 0x4602d3 // 小于等于12则跳转到0x4602d3 47 | user.go:13 0x4602d0 XORL AX, AX // return false 48 | user.go:13 0x4602d2 RET 49 | user.go:15 0x4602d3 MOVL $0x1, AX // return true 50 | user.go:15 0x4602d8 RET 51 | user.go:11 0x4602d9 XORL AX, AX // return false 52 | user.go:11 0x4602db RET 53 | 54 | // CreateUser方法 55 | TEXT github.com/bootun/example/user.(*Server).CreateUser(SB) /github.com/bootun/example/user/user.go 56 | // 省略了一些函数调用前的准备工作(寄存器赋值等操作) 57 | user.go:20 0x460300 CALL user.ValidateName(SB) 58 | user.go:20 0x460305 TESTL AL, AL 59 | user.go:20 0x460307 JE 0x460317 60 | user.go:24 0x460309 XORL AX, AX 61 | user.go:24 0x46030b XORL BX, BX 62 | user.go:24 0x46030d MOVQ 0x10(SP), BP 63 | user.go:24 0x460312 ADDQ $0x18, SP 64 | user.go:24 0x460316 RET 65 | errors.go:62 0x460317 LEAQ 0x9302(IP), AX 66 | errors.go:62 0x46031e NOPW 67 | errors.go:62 0x460320 CALL runtime.newobject(SB) 68 | // ... 69 | ``` 70 | 上面的汇编里只截取了最关键的两段: `ValidateName`函数和`CreateUser`方法。 71 | 72 | 看不懂汇编的同学也没关系,注意看`CreateUser`方法内有一行`user.go:20` `CALL user.ValidateName`, 说明在`CreateUser`方法内调用了`ValidateName`函数,刚好和我们的代码能够对应的上。 73 | 74 | 现在让我们去掉源代码`ValidateName`函数上的`//go:noinline`再次编译后查看生成的汇编指令: 75 | 76 | > 如果你想使用文章里的代码进行尝试,请不要删除`CreateUser`方法上的`//go:noinline`,因为例子中的`CreateUser`太简短了,编译器会把它也内联优化掉,不方便我们进行试验和观察 77 | 78 | ```go 79 | // CreateUser函数 80 | // 此时: 81 | // AX寄存器: 方法Recever,即Server结构体 82 | // BX寄存器: name字符串的指针 83 | // CX寄存器: name字符串的长度 84 | TEXT github.com/bootun/example/user.(*Server).CreateUser(SB) /github.com/bootun/example/user/user.go 85 | 86 | // ... 87 | user.go:18 0x4602d4 MOVQ BX, 0x28(SP) // 保存name字符串的指针到栈上 88 | user.go:19 0x4602d9 TESTQ CX, CX // 验证name的长度是否为0 89 | user.go:9 0x4602dc JE 0x4602e6 // 为0则跳转到0x4602e6 90 | user.go:9 0x4602de NOPW 91 | user.go:11 0x4602e0 CMPQ $0xc, CX // 比较常数12和字符串的长度 92 | user.go:11 0x4602e4 JLE 0x460318 // 小于等于则跳转到0x460318继续执行(name合法) 93 | 94 | errors.go:62 0x4602e6 LEAQ 0x9333(IP), AX // 构造错误返回 95 | errors.go:62 0x4602ed CALL runtime.newobject(SB) 96 | errors.go:62 0x4602f2 MOVQ $0xc, 0x8(AX) 97 | // ... 98 | user.go:23 0x460318 XORL AX, AX // AX = 0 99 | user.go:23 0x46031a XORL BX, BX // BX = 0 100 | user.go:23 0x46031c MOVQ 0x10(SP), BP // 恢复BP寄存器 101 | user.go:23 0x460321 ADDQ $0x18, SP // 增加栈指针, 减小栈空间 102 | user.go:23 0x460325 RET // return 103 | // ... 104 | ``` 105 | 观察这一次的代码可以发现,`ValidateName`函数的逻辑直接被内嵌到了`CreateUser`方法里展开了。我们在生成的汇编代码里也搜索不到`ValidateName`相关的符号了。 106 | 现在的代码等价于: 107 | ```go 108 | func (s *Server) CreateUser(name string, password string) error { 109 | if len(name) < 1 { 110 | return errors.New("invalid name") 111 | } else if len(name) > 12 { 112 | return errors.New("invalid name") 113 | } 114 | return nil 115 | } 116 | ``` 117 | 118 | ## 什么样的函数会被内联? 119 | 内联相关的代码在`cmd/compile/internal/inline/inl.go`里,属于编译器的一部分。在该文件的最上面有这样一段注释, 里面很好的概括了内联的控制和规则: 120 | ```go 121 | // The Debug.l flag controls the aggressiveness. Note that main() swaps level 0 and 1, 122 | // making 1 the default and -l disable. Additional levels (beyond -l) may be buggy and 123 | // are not supported. 124 | // 0: disabled 125 | // 1: 80-nodes leaf functions, oneliners, panic, lazy typechecking (default) 126 | // 2: (unassigned) 127 | // 3: (unassigned) 128 | // 4: allow non-leaf functions 129 | // 130 | // At some point this may get another default and become switch-offable with -N. 131 | // 132 | // The -d typcheckinl flag enables early typechecking of all imported bodies, 133 | // which is useful to flush out bugs. 134 | // 135 | // The Debug.m flag enables diagnostic output. a single -m is useful for verifying 136 | ``` 137 | 总结一下上面这段话中的核心部分: 138 | - **80个节点的叶子函数,oneliners,panic,懒惰的类型检查** 会被内联 139 | - 使用`-N -l`来告诉编译器禁止内联 140 | - 使用`-m`启用诊断输出 141 | 142 | 也就是说,**只要我们的函数/方法足够小,就可能会被内联。** 因此,很多人会使用许多小的函数组合来代替大段代码提升性能。比如我们经常使用的互斥锁(标准库中`sync`包里的`Mutex`)就利用了这一点, 我们平时使用的`Lock`方法一共就只有这几行: 143 | ```go 144 | func (m *Mutex) Lock() { 145 | // Fast path: grab unlocked mutex. 146 | if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { 147 | if race.Enabled { 148 | race.Acquire(unsafe.Pointer(m)) 149 | } 150 | return 151 | } 152 | // Slow path (outlined so that the fast path can be inlined) 153 | m.lockSlow() 154 | } 155 | ``` 156 | 注意倒数第三行的注释:`outlined so that the fast path can be inlined`,利用这一特性,`Lock`里的FastPath就能被内联到我们的程序里而不需要额外的函数调用,从而提升代码的性能。 157 | 158 | > 函数内联部分的入口是函数[inline.InlinePackage](https://github.com/golang/go/blob/7ad92e95b56019083824492fbec5bb07926d8ebd/src/cmd/compile/internal/gc/main.go#LL282 "内联入口"), 想要深入了解的小伙伴可以去看一看。 159 | 160 | ## 内联能为我的程序带来多少性能上的提升? 161 | 前面介绍了这么多内联,连标准库都刻意使用内联来提升Go程序的性能,那么内联究竟能为我们带来多少性能上的提升呢? 162 | 163 | 我们来扩充一下文章开篇提到的例子: 164 | ```go 165 | package user 166 | 167 | import ( 168 | "errors" 169 | ) 170 | 171 | func ValidateName(name string) bool { 172 | if len(name) < 1 { 173 | return false 174 | } else if len(name) > 12 { 175 | return false 176 | } 177 | return true 178 | } 179 | 180 | //go:noinline 181 | func ValidateNameNoInline(name string) bool { 182 | if len(name) < 1 { 183 | return false 184 | } else if len(name) > 12 { 185 | return false 186 | } 187 | return true 188 | } 189 | 190 | func (s *Server) CreateUser(name string, password string) error { 191 | if !ValidateName(name) { 192 | return errors.New("invalid name") 193 | } 194 | return nil 195 | } 196 | 197 | // CreateUserNoInline 使用的是禁止内联版本的 ValidateName 198 | func (s *Server) CreateUserNoInline(name string, password string) error { 199 | if !ValidateNameNoInline(name) { 200 | return errors.New("invalid name") 201 | } 202 | return nil 203 | } 204 | 205 | type Server struct{} 206 | ``` 207 | 我们复制了以`ValidateName`函数,在上面标注上`//go:noinline`来禁止编译器对其进行内联优化,并将其并将其更名为`ValidateNameNoInline`。同时我们也复制了`CreateUser`方法,新的方法内部使用`ValidateNameNoInline`来验证`name`参数,除此之外所有的地方都和原方法相同。 208 | 209 | 我们来写两个Benchmark测试一下: 210 | ```go 211 | package user 212 | 213 | import "testing" 214 | 215 | // BenchmarkCreateUser 测试内联过的函数的性能 216 | func BenchmarkCreateUser(b *testing.B) { 217 | srv := Server{} 218 | for i := 0; i < b.N; i++ { 219 | if err := srv.CreateUser("bootun", "123456"); err != nil { 220 | b.Logf("err: %v", err) 221 | } 222 | } 223 | } 224 | 225 | // BenchmarkValidateNameNoInline 测试函数禁止内联后的性能 226 | func BenchmarkValidateNameNoInline(b *testing.B) { 227 | srv := Server{} 228 | for i := 0; i < b.N; i++ { 229 | if err := srv.CreateUserNoInline("bootun", "123456"); err != nil { 230 | b.Logf("err: %v", err) 231 | } 232 | } 233 | } 234 | ``` 235 | 测试结果如下: 236 | ```sh 237 | # 内联版本的基准测试结果(BenchmarkCreateUser) 238 | goos: windows 239 | goarch: amd64 240 | pkg: github.com/bootun/example/user 241 | cpu: AMD Ryzen 7 6800H with Radeon Graphics 242 | BenchmarkCreateUser 243 | BenchmarkCreateUser-16 1000000000 0.2279 ns/op 244 | PASS 245 | 246 | 247 | # 禁止内联版本的基准测试结果(BenchmarkValidateNameNoInline) 248 | goos: windows 249 | goarch: amd64 250 | pkg: github.com/bootun/example/user 251 | cpu: AMD Ryzen 7 6800H with Radeon Graphics 252 | BenchmarkValidateNameNoInline 253 | BenchmarkValidateNameNoInline-16 733243102 1.635 ns/op 254 | PASS 255 | ``` 256 | 可以看到,禁止内联后每次操作耗费1.6纳秒,而内联后只需要0.22纳秒(因机器而异)。从比例上看,内联优化带来的收益还是很可观的。 257 | 258 | ## 我需要做什么来启用内联优化吗? 259 | 当然不需要,在Go编译器中,内联优化是默认启用的,如果你的函数符合文中提到的内联优化的策略(比如函数很小),并且没有显式的禁用内联,就可能会被编译器执行内联优化。 260 | 261 | 在某些场景下,我们可能不希望函数进行内联(比如使用`dlv`进行DEBUG时,或者查看程序生成的汇编代码时),可以使用`go build -gcflags='-N -l' xxx.go`来禁用内联优化。 262 | 263 | > 编译器默认优化出来的代码可能比较难以阅读和理解,不方便我们进行调试和学习。 264 | 265 | > `-gcflags`是传递给go编译器`gc`的命令行标志, `go build` 背后做了很多事,也不止用到了`gc`一个程序。使用`go build -x main.go`可以查看编译过程中的详细步骤。 -------------------------------------------------------------------------------- /articles/concurrent/channel.md: -------------------------------------------------------------------------------- 1 | Channel剖析 2 | === 3 | > TODO: 该章节内大量细节等待裁剪 4 | 5 | ## channel的结构体表示 6 | channel的运行时结构由`runtime.hchan`来表示: 7 | 8 | ```go 9 | type hchan struct { 10 | qcount uint 11 | dataqsiz uint 12 | buf unsafe.Pointer 13 | elemsize uint16 14 | closed uint32 15 | elemtype *_type // element type 16 | sendx uint // send index 17 | recvx uint // receive index 18 | recvq waitq // list of recv waiters 19 | sendq waitq // list of send waiters 20 | 21 | lock mutex 22 | } 23 | 24 | type waitq struct { 25 | first *sudog 26 | last *sudog 27 | } 28 | 29 | type sudog struct { 30 | // ... 31 | g *g 32 | next *sudog 33 | prev *sudog 34 | // ... 35 | } 36 | ``` 37 | 我们来看一下各个字段的意义: 38 | - `qcount`字段是当前channel队列里的数据量,也就是channel里目前存了多少个数据。 39 | - `dataqsiz`指channel的总容量,在创建channel时传入。 40 | - `buf`是一个指向数组的指针,它的总大小为- `dataqsize*elemsize`,用来存储发给channel的数据。 41 | - `elemsize`是channel的元素大小。 42 | - `closed`用来表示当前channel是否关闭。 43 | - `elemtype`顾名思义就是元素的类型了。 44 | - `sendx`和`recvx`分别表示发送者和接受者的数据位置(你可以想象成两个指针/下标)。 45 | - `recvq`和`sendq`分别表示接受者和发送者的等待队列,它是一个(双向)链表数据结构,`waitq`类型里保存了链表的头和尾。而`sudog`里则保存了对应goroutine的`runtime.g`结构体以及链表前后节点的指针。 46 | 47 | ## 创建channel 48 | `runtime/chan.go`里定义了创建channel的函数: 49 | ```go 50 | func makechan(t *chantype, size int) *hchan { 51 | elem := t.Elem 52 | ... 53 | 54 | // 计算创建channel所需内存大小 55 | mem, overflow := math.MulUintptr(elem.Size_, uintptr(size)) 56 | if overflow || mem > maxAlloc-hchanSize || size < 0 { 57 | panic(plainError("makechan: size out of range")) 58 | } 59 | 60 | var c *hchan 61 | switch { 62 | case mem == 0: 63 | // Queue or element size is zero. 64 | c = (*hchan)(mallocgc(hchanSize, nil, true)) 65 | // Race detector uses this location for synchronization. 66 | c.buf = c.raceaddr() 67 | case !elem.Pointers(): 68 | // Elements do not contain pointers. 69 | // Allocate hchan and buf in one call. 70 | c = (*hchan)(mallocgc(hchanSize+mem, nil, true)) 71 | c.buf = add(unsafe.Pointer(c), hchanSize) 72 | default: 73 | // Elements contain pointers. 74 | c = new(hchan) 75 | c.buf = mallocgc(mem, elem, true) 76 | } 77 | 78 | c.elemsize = uint16(elem.Size_) 79 | c.elemtype = elem 80 | c.dataqsiz = uint(size) 81 | lockInit(&c.lock, lockRankHchan) 82 | 83 | return c 84 | } 85 | ``` 86 | channel的初始化非常简单,主要逻辑就是根据元素类型和元素数量来计算出channel的总内存大小,然后根据内存大小来分配内存并初始化channel的一些字段。 87 | 88 | ## 向channel中发送数据 89 | `xx <-`语法会被编译器翻译为`runtime.chansend1`, 它底层对应着`runtime.chansend`函数,主要逻辑如下: 90 | 91 | ```go 92 | func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { 93 | if c == nil { 94 | // 如果向一个nil channel发送数据 95 | if !block { 96 | // 如果是非阻塞的发送,直接返回false 97 | // 注意,只要是通过 xxx <- 这种方式进来的,都是block的 98 | // 通过select进来的才可能是false 99 | return false 100 | } 101 | // 向一个nil channel 写数据会永远的挂起(阻塞) 102 | gopark(nil, nil, waitReasonChanSendNilChan, traceBlockForever, 2) 103 | throw("unreachable") 104 | } 105 | 106 | // Fast path: check for failed non-blocking operation without acquiring the lock. 107 | // 108 | // After observing that the channel is not closed, we observe that the channel is 109 | // not ready for sending. Each of these observations is a single word-sized read 110 | // (first c.closed and second full()). 111 | // Because a closed channel cannot transition from 'ready for sending' to 112 | // 'not ready for sending', even if the channel is closed between the two observations, 113 | // they imply a moment between the two when the channel was both not yet closed 114 | // and not ready for sending. We behave as if we observed the channel at that moment, 115 | // and report that the send cannot proceed. 116 | // 117 | // It is okay if the reads are reordered here: if we observe that the channel is not 118 | // ready for sending and then observe that it is not closed, that implies that the 119 | // channel wasn't closed during the first observation. However, nothing here 120 | // guarantees forward progress. We rely on the side effects of lock release in 121 | // chanrecv() and closechan() to update this thread's view of c.closed and full(). 122 | if !block && c.closed == 0 && full(c) { 123 | return false 124 | } 125 | 126 | var t0 int64 127 | if blockprofilerate > 0 { 128 | t0 = cputicks() 129 | } 130 | 131 | lock(&c.lock) 132 | 133 | if c.closed != 0 { 134 | unlock(&c.lock) 135 | panic(plainError("send on closed channel")) 136 | } 137 | 138 | if sg := c.recvq.dequeue(); sg != nil { 139 | // Found a waiting receiver. We pass the value we want to send 140 | // directly to the receiver, bypassing the channel buffer (if any). 141 | // send底层会调用sendDirect,直接把数据通过memmove直接写入到buf中,然后调用goready唤醒接收者。 142 | send(c, sg, ep, func() { unlock(&c.lock) }, 3) 143 | return true 144 | } 145 | 146 | if c.qcount < c.dataqsiz { 147 | // Space is available in the channel buffer. Enqueue the element to send. 148 | qp := chanbuf(c, c.sendx) 149 | if raceenabled { 150 | racenotify(c, c.sendx, nil) 151 | } 152 | typedmemmove(c.elemtype, qp, ep) 153 | c.sendx++ 154 | if c.sendx == c.dataqsiz { 155 | c.sendx = 0 156 | } 157 | c.qcount++ 158 | unlock(&c.lock) 159 | return true 160 | } 161 | 162 | if !block { 163 | unlock(&c.lock) 164 | return false 165 | } 166 | 167 | // Block on the channel. Some receiver will complete our operation for us. 168 | gp := getg() 169 | mysg := acquireSudog() 170 | mysg.releasetime = 0 171 | if t0 != 0 { 172 | mysg.releasetime = -1 173 | } 174 | // No stack splits between assigning elem and enqueuing mysg 175 | // on gp.waiting where copystack can find it. 176 | mysg.elem = ep 177 | mysg.waitlink = nil 178 | mysg.g = gp 179 | mysg.isSelect = false 180 | mysg.c = c 181 | gp.waiting = mysg 182 | gp.param = nil 183 | c.sendq.enqueue(mysg) 184 | // Signal to anyone trying to shrink our stack that we're about 185 | // to park on a channel. The window between when this G's status 186 | // changes and when we set gp.activeStackChans is not safe for 187 | // stack shrinking. 188 | gp.parkingOnChan.Store(true) 189 | gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceBlockChanSend, 2) 190 | // Ensure the value being sent is kept alive until the 191 | // receiver copies it out. The sudog has a pointer to the 192 | // stack object, but sudogs aren't considered as roots of the 193 | // stack tracer. 194 | KeepAlive(ep) 195 | 196 | // someone woke us up. 197 | if mysg != gp.waiting { 198 | throw("G waiting list is corrupted") 199 | } 200 | gp.waiting = nil 201 | gp.activeStackChans = false 202 | closed := !mysg.success 203 | gp.param = nil 204 | if mysg.releasetime > 0 { 205 | blockevent(mysg.releasetime-t0, 2) 206 | } 207 | mysg.c = nil 208 | releaseSudog(mysg) 209 | if closed { 210 | if c.closed == 0 { 211 | throw("chansend: spurious wakeup") 212 | } 213 | panic(plainError("send on closed channel")) 214 | } 215 | return true 216 | } 217 | ``` 218 | 219 | ## 从channel中读取数据 220 | `<- xx`这种语法会被翻译为`runtime.chanrecv1`,它是`runtime.chanrecv`的包装函数,`chanrecv`的源码如下: 221 | ```go 222 | // chanrecv receives on channel c and writes the received data to ep. 223 | // ep may be nil, in which case received data is ignored. 224 | // If block == false and no elements are available, returns (false, false). 225 | // Otherwise, if c is closed, zeros *ep and returns (true, false). 226 | // Otherwise, fills in *ep with an element and returns (true, true). 227 | // A non-nil ep must point to the heap or the caller's stack. 228 | func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) { 229 | // channel == nil(尝试读一个空的channel) 230 | if c == nil { 231 | // none blocking(非阻塞情况,比如在select里有default分支,就直接返回) 232 | if !block { 233 | return 234 | } 235 | // 挂起当前goroutine(阻塞) 236 | gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2) 237 | throw("unreachable") 238 | } 239 | 240 | // 如果读channel非阻塞(select内有default)并且channel此时为空(没数据可以读) 241 | if !block && empty(c) { 242 | // 检查channel是否关闭 243 | if atomic.Load(&c.closed) == 0 { 244 | return false, false 245 | } 246 | // 再次检查channel是否有数据到达 247 | if empty(c) { 248 | // ... 249 | return true, false 250 | } 251 | } 252 | // 加锁 253 | lock(&c.lock) 254 | // 如果channel没关闭但里面没数据可以读了 255 | if c.closed != 0 && c.qcount == 0 { 256 | if raceenabled { 257 | raceacquire(c.raceaddr()) 258 | } 259 | // 释放锁并将ep置为零值 260 | unlock(&c.lock) 261 | if ep != nil { 262 | typedmemclr(c.elemtype, ep) 263 | } 264 | return true, false 265 | } 266 | 267 | // 获取发送队列的第一个sudog结构体 268 | if sg := c.sendq.dequeue(); sg != nil { 269 | // 如果缓冲区大小为0,则直接从发送方里接收值,否则 270 | // 从channel队列的头部接收值,并将发送者的值添加到 271 | // 队列的末尾。这部分逻辑在recv里。 272 | recv(c, sg, ep, func() { unlock(&c.lock) }, 3) 273 | return true, true 274 | } 275 | 276 | // 如果缓冲区里有值 277 | if c.qcount > 0 { 278 | // 直接从队列缓冲区里取值 279 | qp := chanbuf(c, c.recvx) 280 | // ... 281 | // 更新channel的字段 282 | c.recvx++ 283 | if c.recvx == c.dataqsiz { 284 | c.recvx = 0 285 | } 286 | c.qcount-- 287 | // 释放锁 288 | unlock(&c.lock) 289 | return true, true 290 | } 291 | 292 | // 走到这里说明: 293 | // 发送队列为空 294 | // 缓冲区没东西 295 | // channel不为nil 296 | 297 | // 非阻塞接收 298 | if !block { 299 | unlock(&c.lock) 300 | return false, false 301 | } 302 | 303 | // 后面就是阻塞的逻辑,把当前G(goroutine)打包为一个sudog结构体,挂到channel的接收队列中,然后调用gopark挂起当前goroutine 304 | // ... 305 | return true, success 306 | } 307 | ``` 308 | 309 | # 关闭channel 310 | ```go 311 | func closechan(c *hchan) { 312 | if c == nil { 313 | panic(plainError("close of nil channel")) 314 | } 315 | 316 | lock(&c.lock) 317 | if c.closed != 0 { 318 | unlock(&c.lock) 319 | panic(plainError("close of closed channel")) 320 | } 321 | 322 | if raceenabled { 323 | callerpc := sys.GetCallerPC() 324 | racewritepc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(closechan)) 325 | racerelease(c.raceaddr()) 326 | } 327 | 328 | c.closed = 1 329 | 330 | var glist gList 331 | 332 | // release all readers 333 | for { 334 | sg := c.recvq.dequeue() 335 | if sg == nil { 336 | break 337 | } 338 | if sg.elem != nil { 339 | typedmemclr(c.elemtype, sg.elem) 340 | sg.elem = nil 341 | } 342 | if sg.releasetime != 0 { 343 | sg.releasetime = cputicks() 344 | } 345 | gp := sg.g 346 | gp.param = unsafe.Pointer(sg) 347 | sg.success = false 348 | if raceenabled { 349 | raceacquireg(gp, c.raceaddr()) 350 | } 351 | glist.push(gp) 352 | } 353 | 354 | // release all writers (they will panic) 355 | for { 356 | sg := c.sendq.dequeue() 357 | if sg == nil { 358 | break 359 | } 360 | sg.elem = nil 361 | if sg.releasetime != 0 { 362 | sg.releasetime = cputicks() 363 | } 364 | gp := sg.g 365 | gp.param = unsafe.Pointer(sg) 366 | sg.success = false 367 | if raceenabled { 368 | raceacquireg(gp, c.raceaddr()) 369 | } 370 | glist.push(gp) 371 | } 372 | unlock(&c.lock) 373 | 374 | // Ready all Gs now that we've dropped the channel lock. 375 | for !glist.empty() { 376 | gp := glist.pop() 377 | gp.schedlink = 0 378 | goready(gp, 3) 379 | } 380 | } 381 | ``` 382 | 383 | 注释基本已经概括了这个函数的作用:`chanrecv`函数从c中接收数据并写入ep中,ep可能为`nil`,在这种情况下,接收到的数据将被忽略(ignored)。如果`block == false`并且没有元素可用,则返回`false, false`。否则,如果这个channel已经被close了,则将*ep置零,返回`true, false`。否则用一个元素填充ep并返回`true, true`。一个非`nil`的ep一定指向**堆**或者调用者的栈。 384 | 385 | block一般都是true的,也就是阻塞的,只有用作`select`的条件且有`default`分支时,才会传入`false`。 386 | 387 | ## 读写channel时的结果 388 | | Operation | Channel state | Result | 389 | | --- | --- | --- | 390 | | Read | nil | Block | 391 | | | Open and Not Empty | Value | 392 | | | Open and Empty | Block | 393 | | | Closed | <default value>, false | 394 | | | Write Only | Compilation Error | 395 | | --- | --- | --- | 396 | | Write | nil | Block | 397 | | | Open and Full | Block | 398 | | | Open and Not Full | Write Value | 399 | | | Closed | panic | 400 | | | Received Only | Compilation Error | 401 | | --- | --- | --- | 402 | | Close | nil | | 403 | | | Open and Not Empty | Closes Channel; read succeed until channel is drained, then read produce default value | 404 | | | Open and Empty | Close Channel; read produce default value | 405 | | | Closed | panic | 406 | | | Received Only | Compilation Error | 407 | 408 | 以上内容选自《Concurrency in Go》,描述了在read, write, close各种不同状态的channel时的结果,可以对照一下源码,查看是否正确。 -------------------------------------------------------------------------------- /articles/concurrent/mutex.md: -------------------------------------------------------------------------------- 1 | # Mutex fairness 2 | ```go 3 | // Mutex fairness 4 | // 5 | // Mutex can be in 2 modes of operations: normal and starvation. 6 | // In normal mode waiters are queued in FIFO order, but a woken up waiter 7 | // does not own the mutex and competes with new arriving goroutines over 8 | // the ownership. New arriving goroutines have an advantage -- they are 9 | // already running on CPU and there can be lots of them, so a woken up 10 | // waiter has good chances of losing. In such case it is queued at front 11 | // of the wait queue. If a waiter fails to acquire the mutex for more than 1ms, 12 | // it switches mutex to the starvation mode. 13 | // 14 | // In starvation mode ownership of the mutex is directly handed off from 15 | // the unlocking goroutine to the waiter at the front of the queue. 16 | // New arriving goroutines don't try to acquire the mutex even if it appears 17 | // to be unlocked, and don't try to spin. Instead they queue themselves at 18 | // the tail of the wait queue. 19 | // 20 | // If a waiter receives ownership of the mutex and sees that either 21 | // (1) it is the last waiter in the queue, or (2) it waited for less than 1 ms, 22 | // it switches mutex back to normal operation mode. 23 | // 24 | // Normal mode has considerably better performance as a goroutine can acquire 25 | // a mutex several times in a row even if there are blocked waiters. 26 | // Starvation mode is important to prevent pathological cases of tail latency. 27 | ``` 28 | 29 | ## 基本流程 30 | 互斥锁的本质就是: 一堆人同时调用`Lock`方法, 只有一个人能顺利return,继续执行代码,也就是所谓的"拿到锁"。其他人会被阻塞在`Lock`方法里,直到有别人将它们唤醒。 31 | 32 | 在了解了大概的流程之后,我们再来看看它具体的实现: 33 | TODO: 互斥锁很推荐《Go 并发编程实战课》的相关章节 -------------------------------------------------------------------------------- /articles/concurrent/syncMap.md: -------------------------------------------------------------------------------- 1 | sync.Map 2 | === 3 | -------------------------------------------------------------------------------- /articles/concurrent/syncPool.md: -------------------------------------------------------------------------------- 1 | # sync.Pool 2 | 3 | ## 什么时候用? 4 | 5 | 在pprof里看 6 | - 进程中的inuse_objects数过多,**gc mark消耗大量CPU** 7 | - 进程中的inuse_objects数过多,**进程RSS占用过高** 8 | 9 | 请求生命周期开始时,pool.Get。请求结束时pool.Put。在fasthttp中有大量应用 -------------------------------------------------------------------------------- /articles/concurrent/waitGroup.md: -------------------------------------------------------------------------------- 1 | sync.WaitGroup 2 | === 3 | ## 使用方式 4 | WaitGroup的注释已经很好的描述了它的用法: 5 | 6 | > `WaitGroup`用来等待goroutine集合完成。主goroutine调用`Add`来设置等待的数量。然后每一个goroutine在运行完成时调用`Done`。`Wait`在所有goroutine都完成之前会一直阻塞。 7 | 8 | 通俗点说,就是你有**一组任务**要并发的执行,你需要等这些任务都完成,才能够继续做接下来的事情,这时候你就可以考虑使用WaitGroup来等待所有任务的完成。 9 | ```go 10 | func main() { 11 | var wg sync.WaitGroup 12 | for i:=0;i<100;i++{ 13 | wg.Add(1) // 或直接在循环外wg.Add(100) 14 | go func(wg *wg.WaitGroup) { 15 | defer wg.Done() 16 | // do something 17 | }(&wg) 18 | } 19 | wg.Wait() 20 | } 21 | ``` 22 | 23 | ## 内部实现 24 | WaitGroup的实现比较简单,整个`waitgroup.go`带上注释也只有100多行。 25 | WaitGroup结构体的定义如下: 26 | ```go 27 | type WaitGroup struct { 28 | noCopy noCopy 29 | 30 | state atomic.Uint64 // high 32 bits are counter, low 32 bits are waiter count. 31 | sema uint32 32 | } 33 | ``` 34 | `noCopy`是一个空结构体,实现了`Locker`接口,用来避免WaitGroup被复制,感兴趣的读者可以查看我之前[分析noCopy的文章](/articles/go-vet/copylock.md),这里就不做解释了。 35 | 36 | 剩余两个字段分别是WaitGroup的状态`state`和用作唤醒的信号量`sema`。 37 | 38 | `state`字段是`atomic`包下的Uint64类型,这意味着所有对该字段的操作(包括更新、读取等)都是原子性的。 -------------------------------------------------------------------------------- /articles/container/array.md: -------------------------------------------------------------------------------- 1 | 数组剖析 2 | ==== 3 | 4 | ## 数组的概念 5 | [数组(array)](https://go.dev/ref/spec#Array_types "Go Specification-Array types")是**单一类型**元素的编号序列,元素的个数称为数组的长度,长度不能为负数。 6 | 7 | ## 数组的初始化方式 8 | ```go 9 | // 显式指定数组大小,长度为3 10 | var a = [3]int{1,2,3} 11 | 12 | // 让编译器自动推导数组大小,长度为3 13 | var b = [...]int{4,5,6} 14 | ``` 15 | 创建数组时可以不显式初始化数组元素,比如`array := [10]int{}`,如果不显示指定数组的内容,则数组的默认内容为该类型的零值。在本例中,该数组的长度为10,数组的内容为10个0。 16 | 17 | ab两种初始化方式在运行期间是等价的,只不过a的类型在编译进行到类型检查阶段就确定了。而b则是在后续阶段由编译器推导数组的大小。最终使用NewArray函数生成数组。有关数组创建过程的更多细节,可以参考[《Draveness - 数组的实现原理》](https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-array "数组的实现原理") 18 | 19 | ## 数组的类型 20 | 数组的长度也是类型的一部分,也就是说,下面两个数组不是同一种类型 21 | ```go 22 | a := [3]int{} 23 | b := [4]int{} 24 | a = b // 编译错误:cannot use b (type [4]int) as type [3]int in assignment 25 | ``` 26 | 尽管a和b两个数组的元素类型都为`int`,但它们的长度不同,所以不属于同一种类型,不能相互比较和赋值。 27 | 28 | ## 数组的内存分配 29 | 数组在内存中是由一块连续的内存组成的。 30 | 31 | ```go 32 | arr := [3]int{1, 2, 3} 33 | fmt.Println(&arr[0]) // 0xc00001a240 34 | fmt.Println(&arr[1]) // 0xc00001a248 35 | fmt.Println(&arr[2]) // 0xc00001a250 36 | // 十六进制表示,在我的64位机器上,int默认占用64位,即8字节,所以每次+8 37 | ``` 38 | 图示如下: 39 | 40 | ![数组的内存布局](array/mem-array.png) 41 | 使用`go tool compile -S main.go`查看汇编代码: 42 | ```ASM 43 | LEAQ type.[3]int(SB), AX 44 | ... 45 | MOVQ $1, (AX) 46 | MOVQ $2, 8(AX) 47 | MOVQ $3, 16(AX) 48 | ``` 49 | 上面的汇编结果省去了部分内容,MOV指令的后缀是Q,而三行MOV指令的偏移量为8,从上面的汇编我们可以得到以下结论:**数组的长度为3,元素所占空间为8字节,中间没有内存空洞或者其他内容,是连续的**。 50 | 51 | 52 | 53 | ## 数组在内存中的位置 54 | 若不考虑逃逸分析,数组的长度**小于等于4**时数组会被分配至栈上,否则则会分配到静态区并在运行时取出,源码可以在`cmd/compile/internal/gc.anylit`中找到。 55 | 56 | 结合逃逸分析的情况下,若数组没有被外部引用,则情况同上。若数组被外部引用,则分配至堆上。例如:函数返回数组指针,或数组作为slice底层的引用数组,且slice被外部引用,则slice底层引用的数组也会逃逸到堆上进行分配。 57 | 58 | ## 数组的访问 59 | 我们知道,数组是有长度的,那么如果我们试图访问超出长度的下标,会发生什么事呢? 60 | 这里分为两种情况 61 | - **编译期能够确定访问位置的情况** 62 | - **编译期间不能确定访问位置的情况** 63 | 64 | 假设有以下代码 65 | ```go 66 | func foo() int{ 67 | arr := [3]int{1,2,3} 68 | return arr[2] 69 | } 70 | 71 | // 调用时传入2,即foo(2) 72 | func bar(i int) int{ 73 | arr := [3]int{1,2,3} 74 | return arr[i] 75 | } 76 | ``` 77 | 这两段代码都是可以通过编译的,但编译器对两段代码的处理是不同的。`foo`函数在编译期就可以确定是否越界访问,如果把`return arr[2]`改为`return arr[4]`,你将得到一个编译错误: 78 | >invalid array index 4 (out of bounds for 3-element array) 79 | 80 | 但bar函数在编译时无法确定是否越界,因此需要运行时(runtime)的介入,使用`GOSSAFUNC=bar go build main.go`来查看bar函数start阶段的SSA指令。 81 | ```SSA 82 | ... 83 | v24 (23) = LocalAddr <*[3]int> {arr} v2 v23 84 | v25 (23) = IsInBounds v6 v14 85 | If v25 → b2 b3 (likely) (23) 86 | b2: ← b1- 87 | v28 (23) = PtrIndex <*int> v24 v6 88 | v29 (23) = Copy v23 89 | v30 (23) = Load v28 v29 90 | v31 (23) = MakeResult v30 v29 91 | Ret v31 (+23) 92 | b3: ← b1- 93 | v26 (23) = Copy v23 94 | v27 (23) = PanicBounds [0] v6 v14 v26 95 | Exit v27 (23) 96 | ... 97 | ``` 98 | **注意看v25这一行**,编译器插入了一个`IsInBounds`指令用来判断是否越界访问,如果越界则执行b3里的内容,触发panic。 99 | 100 | ## 数组的比较 101 | 如果数组的元素类型可以相互比较,那么数组也可以。比如两个**长度相等**的int数组可以进行比较,但两个长度相等的map数组就不可以比较,因为map之间不可以互相比较。 102 | ```go 103 | var a, b [3]int 104 | fmt.Println(a == b) // true 105 | 106 | var c,d map[string]int 107 | fmt.Println(c == d) // invalid operation: c == d (map can only be compared to nil) 108 | ``` 109 | ## 数组的传递 110 | Go中所有的内容都是值传递[](https://go.dev/doc/faq#pass_by_value "Go中的值传递"),因此赋值/传参/返回数组等操作都会将整个数组进行复制。更好的方式是使用slice,这样就能避免复制大对象所带来的开销。 111 | ```go 112 | func getArray() [3]int { 113 | arr := [3]int{1,2,3} 114 | fmt.Printf("%p\n",&arr) // 0xc00001a258 115 | return arr 116 | } 117 | 118 | func main() { 119 | arr := getArray() 120 | fmt.Printf("%p\n",&arr) // 0xc00001a240 121 | } 122 | ``` 123 | 可以发现,arr在返回前和返回后的地址是不相同的。 124 | 125 | 126 | ## 编译时的数组 127 | // TODO: 整理此部分内容 128 | 129 | `arr := [3]int{1,2,3}`在第三阶段类型检查前的语法树为: 130 |
131 | 132 | ```json 133 | { 134 | "op":"ir.OAS", 135 | "X":{ 136 | "op":"ir.ONAME", 137 | "sym":{"Name":"arr"}, 138 | "Ntype":nil 139 | }, 140 | "Y":{ 141 | // Type{List} (composite literal, not yet lowered to specific form) 142 | "op":"ir.OCOMPLIT", 143 | // ir.ArrayType struct 144 | "Ntype":{ 145 | // An ArrayType represents a [Len]Elem type syntax. If Len is nil, the type is a [...]Elem in an array literal. 146 | 147 | // [8]int or [...]int 148 | "op":"ir.OTARRAY", 149 | "Len":{ 150 | "op":"ir.OLITERAL", 151 | "typ":{ 152 | // 153 | "kind":"types.TIDEAL", 154 | } 155 | }, 156 | "Elem":{ 157 | "op":"ir.ONONAME", 158 | "sym":{"Name":"int"} 159 | } 160 | }, 161 | "List":[ 162 | {"op":"ir.OLITERAL","val":1}, 163 | {"op":"ir.OLITERAL","val":2}, 164 | {"op":"ir.OLITERAL","val":3} 165 | ] 166 | } 167 | } 168 | ``` 169 |
170 | 经过第三阶段检查后变为: 171 |
172 | 173 | ```json 174 | { 175 | "op":"ir.OAS", 176 | "X":{ 177 | "op":"ir.ONAME", 178 | "sym":{"Name":"arr"}, 179 | "Ntype":nil 180 | }, 181 | "Y":{ 182 | // Type{List} (composite literal, Type is array) 183 | "op":"ir.OARRAYLIT", 184 | // type.Type struct 185 | "typ":{ 186 | // types.Array struct 187 | "Extra":{ 188 | // type.Type struct 189 | "Elem":{ 190 | "Width":8, 191 | "kind":"types.TINT", 192 | "Align":8, 193 | }, 194 | "Bound":3 195 | }, 196 | "Width":24, 197 | "kind":"types.TARRAY", 198 | "Align":8, 199 | }, 200 | "Ntype":nil, 201 | "List":[ 202 | {"op":"OLITERAL","val":1,"Width":8}, 203 | {"op":"OLITERAL","val":2,"Width":8}, 204 | {"op":"OLITERAL","val":3,"Width":8}, 205 | ], 206 | "Len":0 207 | } 208 | } 209 | ``` 210 |
211 | 212 | `arr := [...]int{1, 2, 3}`在第3阶段类型检查前的语法树 213 | 用JSON形式表示为: 214 |
215 | 216 | ```json 217 | { 218 | "op":"ir.OAS", 219 | "X":{ 220 | "op":"ir.ONAME", 221 | "sym":{"Name":"arr"}, 222 | "Ntype":nil 223 | }, 224 | "Y":{ 225 | // Type{List} (composite literal, not yet lowered to specific form) 226 | "op":"ir.OCOMPLIT", 227 | // ir.ArrayType struct 228 | "Ntype":{ 229 | // An ArrayType represents a [Len]Elem type syntax. If Len is nil, the type is a [...]Elem in an array literal. 230 | 231 | // [8]int or [...]int 232 | "op":"ir.OTARRAY", 233 | "Len":nil, 234 | "Elem":{ 235 | "op":"ir.ONONAME", 236 | "sym":{"Name":"int"} 237 | } 238 | }, 239 | "List":[ 240 | {"op":"ir.OLITERAL","val":1}, 241 | {"op":"ir.OLITERAL","val":2}, 242 | {"op":"ir.OLITERAL","val":3} 243 | ] 244 | } 245 | } 246 | ``` 247 |
248 | 249 | 经过类型检查第三阶段后,语法数变为: 250 |
251 | 252 | ```json 253 | { 254 | "op":"ir.OAS", 255 | "X":{ 256 | "op":"ir.ONAME", 257 | "sym":{"Name":"arr"}, 258 | "Ntype":nil 259 | }, 260 | "Y":{ 261 | // Type{List} (composite literal, Type is array) 262 | "op":"ir.OARRAYLIT", 263 | // type.Type struct 264 | "typ":{ // tcArray 265 | // types.Array struct 266 | "Extra":{ 267 | // type.Type struct 268 | "Elem":{ 269 | "Width":8, 270 | "kind":"types.TINT", 271 | "Align":8, 272 | }, 273 | "Bound":3 274 | }, 275 | "Width":24, 276 | "kind":"types.TARRAY", 277 | "Align":8, 278 | }, 279 | "Ntype":nil, 280 | "List":[ 281 | {"op":"OLITERAL","val":1,"Width":8}, 282 | {"op":"OLITERAL","val":2,"Width":8}, 283 | {"op":"OLITERAL","val":3,"Width":8}, 284 | ], 285 | "Len":0 286 | } 287 | } 288 | ``` 289 |
290 | 291 | `[...]int{}`类型的处理逻辑在`typecheck.tcCompLit`函数中。 292 | 数组在编译时的节点表示为`ir.OTARRAY`,我们可以在类型检查阶段找到对该节点的处理: 293 | 294 | ```go 295 | // typecheck1 should ONLY be called from typecheck. 296 | func typecheck1(n ir.Node, top int) ir.Node { 297 | ... 298 | switch n.Op() { 299 | ... 300 | case ir.OTARRAY: 301 | n := n.(*ir.ArrayType) 302 | return tcArrayType(n) 303 | ... 304 | } 305 | } 306 | ``` 307 | 我们将`tcArrayType`的关键节点放在下面: 308 | ```go 309 | func tcArrayType(n *ir.ArrayType) ir.Node { 310 | if n.Len == nil { // [...]T的形式 311 | // 如果长度是...会直接返回,等到下一阶段进行处理 312 | return n 313 | } 314 | 315 | // 检查数组长度是否合法(大小/长度/是否为负数等) 316 | // ... 317 | 318 | // 确定数组类型 319 | bound, _ := constant.Int64Val(v) 320 | t := types.NewArray(n.Elem.Type(), bound) 321 | n.SetOTYPE(t) 322 | types.CheckSize(t) 323 | return n 324 | } 325 | ``` 326 | 如果直接使用常数作为数组的长度,那么数组的类型在这里就确定好了。 327 | 如果使用`[...]T`+字面量这种形式,则会在`typecheck.tcCompLit`函数中确认元素的数量,并将其op更改为`ir.OARRAYLIT`以便于之后阶段使用 328 | 329 | ```go 330 | func tcCompLit(n *ir.CompLitExpr) (res ir.Node) { 331 | ... 332 | // Need to handle [...]T arrays specially. 333 | if array, ok := n.Ntype.(*ir.ArrayType); ok && array.Elem != nil && array.Len == nil { 334 | array.Elem = typecheckNtype(array.Elem) 335 | elemType := array.Elem.Type() 336 | if elemType == nil { 337 | n.SetType(nil) 338 | return n 339 | } 340 | length := typecheckarraylit(elemType, -1, n.List, "array literal") 341 | n.SetOp(ir.OARRAYLIT) 342 | n.SetType(types.NewArray(elemType, length)) 343 | n.Ntype = nil 344 | return n 345 | } 346 | ... 347 | } 348 | ``` 349 | TODO: 350 | -------------------------------------------------------------------------------- /articles/container/array/mem-array.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootun/go-analysis/a7273e9f45aa8026dc660d9044b0612569d4bc87/articles/container/array/mem-array.png -------------------------------------------------------------------------------- /articles/container/map.md: -------------------------------------------------------------------------------- 1 | Map剖析 2 | === 3 | ## 初始化 4 | // TODO: 5 | 6 | ## 编译时的map 7 | 使用字面值初始化map时,map编译时节点的op类型为`ir.OMAPLIT` 8 | ```go 9 | func maplit(n *ir.CompLitExpr, m ir.Node, init *ir.Nodes) { 10 | 11 | // make the map var 12 | // 虽然这里传入的op类型为ir.OMAKE,但最终生成代码时并 13 | // 不一定会有makemap,比如当map被分配至栈上,详细请看 14 | // walk.walkMakeMap 15 | a := ir.NewCallExpr(base.Pos, ir.OMAKE, nil, nil) 16 | ... 17 | // 如果要初始化的条目大于25,则将其放入数组循环赋值 18 | if len(entries) > 25 { 19 | 20 | // for i = 0; i < len(vstatk); i++ { 21 | // map[vstatk[i]] = vstate[i] 22 | // } 23 | return 24 | } 25 | // 对于很少数量的条目,直接赋值 26 | 27 | // Build list of var[c] = expr. 28 | } 29 | ``` 30 | 31 | ## 运行时的map 32 | 33 | ### 数据结构 34 | ### 访问 35 | ### 赋值 36 | ### 删除 37 | ### 扩容 -------------------------------------------------------------------------------- /articles/container/slice.md: -------------------------------------------------------------------------------- 1 | 切片剖析 2 | === 3 | [切片(slice)](https://go.dev/ref/spec#Slice_types "Go Specification Slice types")类似数组,都是一段相同类型元素在内存中的连续集合。和数组不同的是,切片的长度是可以动态改变的,而数组一旦确定了长度之后就无法再改变了。 4 | 5 | ## 切片的创建和使用 6 | #### 创建 7 | 要创建一个切片,我们可以使用以下几种方式 8 | - 使用关键字make()创建切片 9 | - 使用[切片表达式](https://go.dev/ref/spec#Slice_expressions "Slice expressions")从切片或数组构造切片 10 | - 使用字面值显式初始化切片 11 | ```go 12 | a := make([]int,3,5) // 使用make关键字 13 | b := a[1:3] // 使用切片表达式 14 | c := []int{1,2,3} // 使用字面值显式初始化 15 | ``` 16 | 17 | #### 使用 18 | 切片的使用方式类似数组,都是通过下标访问对应的元素: 19 | ```go 20 | // 使用字面值初始化切片 21 | foo := []int{5,6,7,8,9} 22 | f1 := foo[0] // f1 = 5 23 | f2 := foo[1] // f2 = 6 24 | f3 := foo[4] // f3 = 9 #注意这里的值# 25 | 26 | // 使用切片表达式从切片foo上创建新切片bar 27 | bar := foo[2:4] 28 | b1 := bar[0] // b1 = 7 29 | b2 := bar[1] // b2 = 8 30 | // b3 := bar[2] 出错,panic: runtime error: index out of range [2] with length 2 31 | 32 | // 对切片进行操作 33 | bar = append(bar,10) // 向slice末尾内追加元素 34 | b3 := bar[2] // b3 = 10 # 注意这里的值 35 | f3 = foo[4] // f3 = 10 #和上面的f3比较一下# 36 | foo[4] = 9 // 更改foo[4]的值 37 | b3 = bar[2] // b3 = 9 #和上面的b3比较一下# 38 | ``` 39 | 我们并没有对`foo`进行修改,我们只是在`bar`后面添加了一个元素10,为什么`foo`的内容也发生改变了呢?后面我们修改了`foo[4]`,为什么`bar`刚刚追加的10变成了9?是不是它们用的是同一块地址空间? 40 | 41 | 在回答这些问题之前,让我们先看看切片的结构。 42 | ## 切片的运行时结构 43 | 切片在运行时的表示为`reflect.SliceHeader`,其结构如下 44 | ```go 45 | type SliceHeader struct { 46 | Data uintptr // 底层数组的指针 47 | Len int // 切片的长度 48 | Cap int // 切片的容量 49 | } 50 | ``` 51 | 我们可以发现,切片本身并没有“存储数据”的能力,它"肚子里“有一个指针,这个指针指向的才是真正存放数据的数组。当我们使用`make([]int,3,5)`来创建切片时,编译器会在底层创建一个长度为5的数组来作为这个切片存储数据的容器,并将切片的Data字段设为指向数组的指针。 52 | 53 | 我们可以使用如下方法来将一个切片转换为`reflect.SliceHeader`结构: 54 | ```go 55 | // 初始化一个切片 56 | slice := make([]int, 3, 5) 57 | // 将切片的指针转化为unsafe.Pointer类型,再进一步将其转化为reflect.SliceHeader 58 | sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice)) 59 | // 打印切片的属性 60 | fmt.Printf("Data:%p\n",unsafe.Pointer(sliceHeader.Data)) // Data:0xc0000181e0 61 | fmt.Printf("Len:%v\n",sliceHeader.Len) // Len:3 62 | fmt.Printf("Cap:%v\n",sliceHeader.Cap) // Cap:5 63 | ``` 64 | 65 | 通过这种方法,我们可以很容易的观察使用切片表达式创建切片和原数组的关系: 66 | ```go 67 | // 初始化一个数组 68 | arr := [3]int{1, 2, 3} 69 | // 使用切片表达式在数组arr上创建一个切片 70 | slice := arr[0:2] 71 | sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice)) 72 | fmt.Printf("Data:%p\n", unsafe.Pointer(sliceHeader.Data)) // Data:0xc0000b8000 73 | fmt.Printf("Arr:%p\n",&arr) // Arr:0xc0000b8000 74 | fmt.Printf("Len:%v\n", sliceHeader.Len) // Len:2 75 | fmt.Printf("Cap:%v\n", sliceHeader.Cap) // Cap:3 76 | ``` 77 | 我们可以发现,切片的Data字段和数组的指针是相同的,说明切片和数组共用的是同一块内存空间。 78 | 79 | ## 切片的内存结构 80 | 说了这么多,切片究竟长什么样子呢? 81 | 82 | 我们拿下面这段代码来做个示范: 83 | ```go 84 | // 初始化一个长度为5的数组 85 | arr := [5]int{5, 6, 7, 8, 9} 86 | // 使用切片表达式在arr上构建一个切片 87 | slice := arr[2:4] 88 | ``` 89 | 数组arr和切片slice的结构如下图所示: 90 | 91 | ![](slice/slice-1.png) 92 | 我们结合代码来解读一下这张图, 93 | - `arr`是一个由**5**个元素组成的**数组**,它们在内存中是**连续**的。 94 | - `slice`是使用切片表达式`arr[2:4]`来构建的 95 | 96 | 所以`slice`的内容是数组`arr[2]`到`arr[4]`中间的这部分(从2开始但并不包括4)。 97 | 98 | 也就是说,`slice`的`Data`字段指针指向的是`arr[2]`的位置,**当前slice里有两个元素**(4 - 2 = 2),`slice[0]`的值是7,`slice[1]`的值是8。 99 | ```go 100 | fmt.Println(slice[0]) // 7 101 | fmt.Println(slice[1]) // 8 102 | ``` 103 | 那`Cap`为什么是3呢? 104 | 105 | 因为`slice`的底层数组,也就是`arr`依然有额外的空间供`slice`使用,目前`slice`只包括了`7`和`8`两个元素,但如果需要添加新的元素进`slice`里,那么9这个元素的空间就可以供`slice`使用。 106 | 107 | 我们可以查看汇编验证一下是否正确 108 | ```go 109 | ... 110 | // 初始化部分 111 | LEAQ type.[5]int(SB), AX 112 | CALL runtime.newobject(SB) // 在堆上分配内存 113 | MOVQ $5, (AX) // arr[0] = 5 114 | MOVQ $6, 8(AX) // arr[1] = 6 115 | MOVQ $7, 16(AX) // arr[2] = 7 116 | MOVQ $8, 24(AX) // arr[3] = 8 117 | MOVQ $9, 32(AX) // arr[4] = 9 118 | // Slice相关逻辑 119 | ADDQ $16, AX // Data = &arr[2],AX寄存器指向的是arr[0],将其加16字节,也就是向后移动两个元素 120 | MOVL $2, BX // Len = 2 121 | MOVL $3, CX // Cap = 3 122 | ... 123 | ``` 124 | 总结一下,`Data`指针指向`slice`截取的第一个数据,`Len`代表`slice`当前有多少个元素,`Cap`表示`slice`最多可以容纳的元素数量,超过`Cap`之后就需要扩容了。 125 | 126 | 所以现在我们可以解决开始的问题了:如果使用**切片表达式**在别的切片或数组上构建切片,那么**它们共享的是同一块内存空间**,只不过`Data`,`Cap`,`Len`的数据不一样,**如果修改其中某一个元素,其他切片或数组也会受到影响**。 127 | ## 切片的扩容 128 | 切片本身并不是一个动态数组,那么它是如何实现动态扩容的呢? 129 | 130 | 我们依然拿上面的例子讲解 131 | ```go 132 | arr := [5]int{5, 6, 7, 8, 9} 133 | slice := arr[2:4] 134 | // 当前slice属性 Len:2,Cap:3 135 | // 向slice内追加元素 136 | slice = append(slice,5) // 此时slice为[7,8,5] 137 | // 当前slice属性 Len:3,Cap:3 138 | ``` 139 | 在我们使用`append`向`slice`内追加1个元素后,底层的`arr`数组其实已经没有额外的容量能够让我们再一次追加数据了,此时的`slice`如图所示: 140 | 141 | ![](slice/slice-2.png) 142 | 143 | 现在我们再次`append`一个元素看看会发生什么事情 144 | ```go 145 | // 接前面 146 | // 在底层数组没有空间的情况下再次追加元素 147 | slice = append(slice,6) 148 | 149 | // arr 此时的值 150 | fmt.Printf("%#v\n",arr) // [5]int{5, 6, 7, 8, 5} 151 | // slice 此时的值 152 | fmt.Printf("%#v\n",slice) // []int{7, 8, 5, 6} 153 | 154 | // arr的地址 155 | fmt.Printf("%p\n",&arr) // 0x081e0 156 | // slice Data的值以及Len和Cap 157 | sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice)) 158 | fmt.Printf("Data:%p\n", unsafe.Pointer(sliceHeader.Data)) // 0x08210 159 | fmt.Printf("Len:%v\n", sliceHeader.Len) // Len:4 160 | fmt.Printf("Cap:%v\n", sliceHeader.Cap) // Cap:6 161 | ``` 162 | 观察输出你会发现,`slice`的`Data`字段已经和`arr`的值**不一样**了,说明**此时`slice`的底层数组已经不是`arr`了**,我们使用以下命令禁用优化和内联进行编译,并查看对应的汇编代码,再次验证一下: 163 | ```SH 164 | GOSSAFUNC=main.grow go build -gcflags "-N -l" slice.go 165 | ``` 166 | **源代码**: 167 | 168 | ![](slice/slice-3.png) 169 | 170 | **最终生成的汇编代码**: 171 | 172 | ![](slice/slice-4.png) 173 | 174 | 仔细观察我们发现,源代码第7行所对应的汇编指令中,有一行`CALL runtime.growslice`,这个函数定义在`runtime/slice.go`文件中,函数签名如下 175 | ```go 176 | func growslice(et *_type, old slice, cap int) slice 177 | ``` 178 | 该函数传入**旧的slice**和**期望的新容量**,返回**新的`slice`**,让我们看看函数的具体实现: 179 | ```go 180 | func growslice(et *_type, old slice, cap int) slice { 181 | // 省略了部分代码 182 | newcap := old.cap 183 | // doublecap为旧切片容量的2倍 184 | doublecap := newcap + newcap 185 | // 如果期望容量 > doublecap,则直接将期望容量作为新的容量 186 | if cap > doublecap { 187 | newcap = cap 188 | } else { 189 | // 判断旧切片的容量,如果小于1024,则将旧切片的容量翻倍 190 | if old.cap < 1024 { 191 | newcap = doublecap 192 | } else { 193 | // 每次增长1/4,直到容量大于期望容量 194 | for 0 < newcap && newcap < cap { 195 | newcap += newcap / 4 196 | } 197 | // 如果旧切片容量小于等于0,则直接将期望容量作为新容量 198 | if newcap <= 0 { 199 | newcap = cap 200 | } 201 | } 202 | } 203 | // 省略了部分代码 204 | } 205 | ``` 206 | 在本例中,我们向`growslice`函数传入的`cap`为`4`(因为旧的`slice`本身有3个元素,再次`append`1个元素,所以期望容量是`4`,为了印证这一点,我们可以观察对应的汇编代码,在`Go 1.17`之后的版本里,x86平台下函数调用使用寄存器来传递函数的参数,寄存器`AX`传递第一个参数,参数二的`runtime.slice`是个结构体,有三个字段,占用3个寄存器,所以`BX`,`CX`,`DI`寄存器是第二个参数,`SI`寄存器为`cap`,仔细看调用前的准备工作`MOVL $4, SI`,印证了我们之前说传入的`cap`为4的说法),`cap`传入4之后我们向下走,`4 < 2 x 3`,不满足 `cap > doublecap`,继续向下走,到`old.cap < 1024`时条件满足,所以新的容量就等于旧的容量翻倍,也就是`2 x 3 = 6`。 207 | 208 | 但**上述部分得到的结果并不是真正的新切片容量**,为了提高内存分配效率,减少内存碎片,会在这个newcap的基础上**向上取整**,取整的参考是一个数组,位置在`runtime/sizeclasses.go`中,数组内容如下: 209 | ```go 210 | var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 24, 32, 48, 64, ..., 32768} 211 | ``` 212 | 上面我们计算的`newcap`为6,每个`int`占用8字节,共计`6 x 8 = 48`字节,正好在这个数组中,不需要再向上取整,如果我们刚刚计算的`newcap`为5,`5 x 8 = 40`字节,需要向上对齐到`48`字节,于是最终的新切片大小就为`6`个元素而不是`5`个。 213 | 214 | 到这里我们已经计算出我们新切片的所有数据了,`Len:4`,`Cap:6`。怎么样?是不是和上面代码的输出一样呢?现在我们再来看看新切片的内存结构图: 215 | 216 | 217 | ![](slice/slice-5.png) 218 | 219 | 220 | `slice`现在引用了一个新的数组,现在的`slice`和`arr`已经**没有关系**了。 221 | 222 | 值得注意的是,切片扩容后灰色的部分暂时未使用,这部分是留给下次`append`时使用的,避免频繁扩容带来的复制和内存分配的开销。 223 | 224 | ## 切片的传递 225 | 我们之前提到过,Go中的参数传递都是**值传递**[](https://go.dev/doc/faq#pass_by_value "pass by value")。但如果你有过一些Go语言的经验,可能会遇到下面这种情况: 226 | ```go 227 | // foo 尝试把数组的第一个元素修改为9 228 | func foo(arr [3]int) { 229 | arr[0] = 9 230 | } 231 | 232 | // bar 尝试把切片的第一个元素修改为9 233 | func bar(slice []int) { 234 | slice[0] = 9 235 | } 236 | 237 | func main() { 238 | arr := [3]int{1, 2, 3} 239 | slice := []int{1, 2, 3} 240 | foo(arr) // 尝试修改数组 241 | bar(slice) // 尝试修改切片 242 | fmt.Println(arr) // [1 2 3] 243 | fmt.Println(slice) // [9 2 3] 244 | } 245 | ``` 246 | 我们对`arr`的修改在`main函数`中是**不可见**的,这符合我们刚刚提到的:一切参数的传递都是值传递,我们在`foo`中修改的只是`arr`的一个**副本**,真正的`arr`并没有被修改,除非我们将`foo`函数的参数修改为指针类型。但`slice`的修改却影响到了`main函数`中的`slice`,这是为什么呢?难道切片是引用传递而非值传递吗? 247 | 248 | 其实不是的,我们上面说了,**一切参数传递都是值传递**,`slice`当然也不例外 ,结合我们刚刚了解的`slice`的结构,我们在向`bar`中传递参数时,是**将`slice`的结构体拷贝了一份而并非拷贝了底层数组本身**,因为`slice`的结构体内持有底层数组的指针,所以在`bar`内修改`slice`的数据会将底层数组的数据修改,从而影响到`main函数`中的`slice`。 249 | 250 | 下面我用两幅图向你展示参数传递时复制的内容: 251 | 252 | ![](slice/slice-6.png) 253 | 254 | 上面这幅是我们调用`foo`函数时的情景,我们复制了一个**数组**后传入了`foo`函数,`foo`函数内的`arr`和main函数内的`arr`除了里面的元素相等之外**没有任何关系**,它们不是同一块内存空间,所以我们在`foo`函数内修改数组`arr`并不会影响main函数的`arr`。 255 | 256 | 接下来我们再看一下当我们调用`bar`时的情景: 257 | ![](slice/slice-7.png) 258 | 259 | 当我们调用`bar`函数时,复制的是**切片的结构体**,也就是`Data`,`Len`,`Cap`这三个属性。此时`bar`函数内的`slice`和main函数内的`slice`持有的是同一个数组的指针,所以我们在bar函数内对`slice`所做的修改会影响到main函数中的`slice`。 260 | 261 | 那是不是可以认为,我们在任意场景下都可以传递`slice`到别的函数?反正里面有指针,做的修改都能在函数外面“看到”呀? 262 | 263 | 那我们稍微修改一下上面代码中的`bar`函数: 264 | ```go 265 | // bar 尝试在slice后追加元素 266 | func bar(slice []int) { 267 | slice = append(slice,4) 268 | } 269 | 270 | func main() { 271 | slice := []int{1, 2, 3} 272 | bar(slice) // 追加元素 273 | fmt.Println(slice) // [1 2 3] 274 | } 275 | ``` 276 | 277 | 为什么我们在`bar`内追加了元素,但main内没发生变化? 278 | 279 | ![](slice/slice-8.png) 280 | 281 | 我们知道,`bar.slice`是`main.slice`的**复制**,起初`bar.slice`和`main.slice`的`Data`是**相同**的,它们指向同一块内存地址,但我们在`bar`函数内向`bar.slice`追加了元素,由于`slice`底层的数组只能容纳3个元素,所以`append`操作会**触发扩容**,**导致`bar.slice`指向新的数组**,而`main.slice`指向的依然是旧的数组,所以我们在`bar`内的`append`操作不会影响main函数内的`slice`。 282 | 283 | **思考** 284 | - 如果上述例子中`main.slice`的`Len`为3,`Cap`为4,那么调用`bar`函数进行`append`操作,`main.slice`会受到什么影响吗? 285 | 286 | ## 切片的复制 287 | 288 | 切片使用内建函数`copy`进行复制,`copy`将元素从源切片(`src`)复制到目标切片(`dst`),返回复制的元素数。 289 | ```go 290 | // copy 的函数签名 291 | func copy(dst, src []Type) int 292 | 293 | // copy的使用 294 | foo := []int{5, 4, 3, 2, 1} 295 | bar := []int{0, 0, 0, 0} 296 | 297 | // 将bar复制到foo,从foo[2]开始 298 | i := copy(foo[2:], bar) 299 | fmt.Printf("%v\n",foo) // [5 4 0 0 0] 300 | fmt.Printf("%d\n",i) // 3 301 | ``` 302 | 上面代码的行为如下图所示: 303 | 304 | ![](slice/slice-9.png) 305 | 306 | 我们`copy`的`dst`是从`foo[2]`开始的,所以`copy`只会将`foo[2]`,`foo[3]`,`foo[4]`使用`bar`进行替换,由于只复制了3个,所以`copy`返回3 307 | 308 | `copy`会在编译时进行展开,底层调用`runtime.memmove`函数将整块内存进行拷贝。相较于平时我们使用for循环进行逐个复制,copy能够提供更好的性能。 -------------------------------------------------------------------------------- /articles/container/slice/slice-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootun/go-analysis/a7273e9f45aa8026dc660d9044b0612569d4bc87/articles/container/slice/slice-1.png -------------------------------------------------------------------------------- /articles/container/slice/slice-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootun/go-analysis/a7273e9f45aa8026dc660d9044b0612569d4bc87/articles/container/slice/slice-2.png -------------------------------------------------------------------------------- /articles/container/slice/slice-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootun/go-analysis/a7273e9f45aa8026dc660d9044b0612569d4bc87/articles/container/slice/slice-3.png -------------------------------------------------------------------------------- /articles/container/slice/slice-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootun/go-analysis/a7273e9f45aa8026dc660d9044b0612569d4bc87/articles/container/slice/slice-4.png -------------------------------------------------------------------------------- /articles/container/slice/slice-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootun/go-analysis/a7273e9f45aa8026dc660d9044b0612569d4bc87/articles/container/slice/slice-5.png -------------------------------------------------------------------------------- /articles/container/slice/slice-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootun/go-analysis/a7273e9f45aa8026dc660d9044b0612569d4bc87/articles/container/slice/slice-6.png -------------------------------------------------------------------------------- /articles/container/slice/slice-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootun/go-analysis/a7273e9f45aa8026dc660d9044b0612569d4bc87/articles/container/slice/slice-7.png -------------------------------------------------------------------------------- /articles/container/slice/slice-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootun/go-analysis/a7273e9f45aa8026dc660d9044b0612569d4bc87/articles/container/slice/slice-8.png -------------------------------------------------------------------------------- /articles/container/slice/slice-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootun/go-analysis/a7273e9f45aa8026dc660d9044b0612569d4bc87/articles/container/slice/slice-9.png -------------------------------------------------------------------------------- /articles/go-vet/copylock.md: -------------------------------------------------------------------------------- 1 | 不要复制我的结构体! 2 | === 3 | 4 | ## 不允许复制的结构体 5 | sync包中的许多结构都是不允许拷贝的,比如`sync.Cond`,`sync.WaitGroup`,`sync.Pool`, 以及sync包中的各种锁,因为它们自身存储了一些状态(比如等待者的数量),如果你尝试复制这些结构体: 6 | ```go 7 | var wg1 sync.WaitGroup 8 | wg2 := wg1 // 将 wg1 复制一份,命名为 wg2 9 | wg2.Wait() 10 | ``` 11 | 12 | 那么你将在你的 IDE 中看到一个醒目的警告: 13 | 14 | ```go 15 | assignment copies lock value to wg2: sync.WaitGroup contains sync.noCopy 16 | ``` 17 | 18 | IDE是如何实现这一点的呢?我们自己又能否利用这一机制来告诉别人,不要拷贝某个结构体呢? 19 | 20 | ## 实现原理 21 | 22 | 大部分编辑器/IDE都会在你的代码上运行`go vet`,vet是Go官方提供的**静态分析工具**,我们刚刚得到的提示信息就是vet分析代码后告诉我们的。vet的实现在Go源码的`cmd/vet`中,里面注册了很多不同类型的分析器,其中`copylock`这个分析器会检查实现了`Lock`和`Unlock`方法的结构体是否被复制。 23 | 24 | `copylock Analyser`在`cmd/vet`中注册,具体实现代码在`golang.org/x/tools/go/analysis/passes/copylock/copylock.go`中, 这里只摘抄部分核心代码进行解释: 25 | ```go 26 | var lockerType *types.Interface 27 | 28 | func init() { 29 | //... 30 | methods := []*types.Func{ 31 | types.NewFunc(token.NoPos, nil, "Lock", nullary), 32 | types.NewFunc(token.NoPos, nil, "Unlock", nullary), 33 | } 34 | // Locker 结构包括了 Lock 和 Unlock 两个方法 35 | lockerType = types.NewInterface(methods, nil).Complete() 36 | } 37 | ``` 38 | 39 | `init`函数中把包级别的全局变量`lockerType`进行了初始化,`lockerType`内包含了两个方法: `Lock`和`Unlock`, 只有实现了这两个方法的结构体才是`copylock Analyzer`要处理的对象。 40 | ```go 41 | // lockPath 省略了参数部分,只保留了最核心的逻辑, 42 | // 用来检测某个类型是否实现了Locker接口(Lock和Unlock方法) 43 | func lockPath(...) typePath { 44 | // ... 45 | // 如果传进来的指针类型实现了Locker接口, 就返回这个类型的信息 46 | if types.Implements(types.NewPointer(typ), lockerType) && !types.Implements(typ, lockerType) { 47 | return []string{typ.String()} 48 | } 49 | // ... 50 | } 51 | 52 | // checkCopyLocksAssign 检查赋值操作是否复制了一个锁 53 | func checkCopyLocksAssign(pass *analysis.Pass, as *ast.AssignStmt) { 54 | for i, x := range as.Rhs { 55 | // 如果等号右边的结构体里有字段实现了Lock/Unlock的话,就输出警告信息 56 | if path := lockPathRhs(pass, x); path != nil { 57 | pass.ReportRangef(x, "assignment copies lock value to %v: %v", analysisutil.Format(pass.Fset, as.Lhs[i]), path) 58 | } 59 | } 60 | } 61 | ``` 62 | 在`copylock.go`中不仅有赋值检查的逻辑,其他可能会导致结构体被复制的方式它也进行了处理,例如 函数传参、函数调用等,这里就不一一解释了,感兴趣的同学可以自行查看源码。 63 | 64 | ## 结论 65 | 只要你的IDE会帮你运行`go vet`(目前主流的VSCode和GoLand都会自动帮你运行),你就能通过这个机制来尽量避免复制结构体。 66 | 67 | 如果你的结构体也因为某些原因,不希望使用者复制,你也可以使用该机制来警告使用者: 68 | 69 | 定义一个实现了`Lock`和`Unlock`的结构体 70 | ```go 71 | type noCopy struct{} 72 | 73 | func (*noCopy) Lock() {} 74 | func (*noCopy) Unlock() {} 75 | ``` 76 | 将其嵌入你的结构体中: 77 | ```go 78 | // Foo 代表你不希望别人复制的结构体 79 | type Foo struct { 80 | noCopy 81 | // ... 82 | } 83 | ``` 84 | 或直接让你的结构体实现`Lock`和`Unlock`方法: 85 | ```go 86 | type Foo struct { 87 | // ... 88 | } 89 | 90 | func (*Foo) Lock() {} 91 | func (*Foo) Unlock() {} 92 | ``` 93 | 94 | 这样别人在复制`Foo`的时候,就会得到IDE的警告信息了。 -------------------------------------------------------------------------------- /articles/struct.md: -------------------------------------------------------------------------------- 1 | 2 | //TODO: 3 | 4 | struct字段的offset会在类型检查阶段确定 --------------------------------------------------------------------------------