├── .github └── FUNDING.yml ├── 1、数据定义.md ├── 1、最常用的调试golang的bug以及性能问题的实践方法?.md ├── 1、流?I-O操作?阻塞?epoll.md ├── 2、Golang的协程调度器原理及GMP设计思想?.md ├── 2、分布式从ACID、CAP、BASE的理论推进.md ├── 2、数组和切片.md ├── 3、Golang中逃逸现象,变量“何时栈何时堆”.md ├── 3、Map.md ├── 3、对于操作系统而言进程、线程以及Goroutine协程的区别.md ├── 4、Golang中make与new有何区别?.md ├── 4、Go是否可以无限go?如何限定数量?.md ├── 4、interface.md ├── 5、Golang三色标记+混合写屏障GC模式全分析.md ├── 5、channel.md ├── 5、单点Server的N种并发模型汇总.md ├── 6、TCP中TIME_WAIT状态意义详解.md ├── 6、WaitGroup.md ├── 6、面向对象的编程思维理解interface.md ├── 7、Golang中的Defer必掌握的7知识点.md ├── 7、一种实时动态保活的Worker工作池设计机制.md ├── 8、精通Golang项目依赖Gomodules.md ├── LICENSE ├── README.md ├── SUMMARY.md ├── book.json ├── cover.jpg ├── default.md └── images ├── 0-作者公众号刘丹冰Aceld.jpg ├── 10-N-1关系.png ├── 100-epoll.png ├── 101-epoll3.png ├── 102-epoll4.png ├── 103-epoll水平触发1.png ├── 104-epoll水平触发2.png ├── 105-epoll边缘触发1.png ├── 105-epoll边缘触发2.png ├── 107-内存4区.jpeg ├── 108-数据变量.png ├── 109-foreach.jpeg ├── 11-1-1.png ├── 110-foreach2.jpeg ├── 111-defer1.png ├── 112-defer2.jpeg ├── 113-defer3.jpeg ├── 114-defer4.jpeg ├── 115-interface1.jpeg ├── 116-interface2.jpeg ├── 117-interface3.jpeg ├── 118-interface4.jpeg ├── 119-interface5.jpeg ├── 12-m-n.png ├── 120-interface6.jpeg ├── 121-interface7.jpeg ├── 126-分布式5.jpeg ├── 127-分布式6.jpeg ├── 128-分布式7.jpeg ├── 129-CAP1.jpeg ├── 13-gm.png ├── 130-CAP2.jpeg ├── 131-CAP3.jpeg ├── 132-CAP4.jpeg ├── 133-CAP5.jpeg ├── 134-CAP6.jpeg ├── 135-CAP7.jpeg ├── 136-CAP8.jpeg ├── 137-CAP9.jpeg ├── 138-CAP10.jpeg ├── 139-CAP11.jpeg ├── 14-old调度器.png ├── 140-CAP12.jpeg ├── 141-CAP13.jpeg ├── 143-Base2.jpeg ├── 144-Base2.jpeg ├── 145-Base3.jpeg ├── 146-Cap0.jpeg ├── 149-goroutines3.jpeg ├── 15-gmp.png ├── 151-goroutines5.jpeg ├── 152-进程线程1.jpeg ├── 153-进程线程2.jpeg ├── 154-进程线程3.jpeg ├── 155-进程线程4.jpeg ├── 156-进程线程5.jpeg ├── 157-进程线程6.jpeg ├── 158-并发模型1.jpeg ├── 159-并发模型2.jpeg ├── 16-GMP-调度.png ├── 160-并发模型3.jpeg ├── 161-并发模型4.jpeg ├── 162-并发模型5.jpeg ├── 163-并发模型6.jpeg ├── 164-并发模型7.jpeg ├── 165-timewait1.png ├── 166-timewait2.png ├── 167-timewait3.png ├── 168-timewait4.png ├── 169-channel异常情况总结.png ├── 17-pic-go调度器生命周期.png ├── 18-go-func调度周期.jpeg ├── 19-go-trace1.png ├── 20-go-trace2.png ├── 20-go-trace3.png ├── 22-go-trace4.png ├── 23-go-trace5.png ├── 24-go-trace6.png ├── 25-go-trace7.png ├── 26-gmp场景1.png ├── 27-gmp场景2.png ├── 28-gmp场景3.png ├── 29-gmp场景4.png ├── 30-gmp场景5.png ├── 31-gmp场景6.png ├── 32-gmp场景7.001.jpeg ├── 33-gmp场景8.png ├── 34-gmp场景9.png ├── 35-gmp场景10.png ├── 36-gmp场景11.png ├── 37-godebug1.png ├── 38-pprof-profile-web.png ├── 39-pprof001.png ├── 40-平铺设计.png ├── 41-开闭设计.png ├── 42-GC2.png ├── 42-混乱的依赖关系.png ├── 43-依赖倒转设计.png ├── 44-GC1.png ├── 45-GC3.png ├── 46-GC4.png ├── 47-GC5.jpeg ├── 48-GC6.jpeg ├── 49-GC7.jpeg ├── 5-单进程操作系统.png ├── 50-GC8.jpeg ├── 51-GC9.jpeg ├── 52-GC10.jpeg ├── 53-STW1.png ├── 54-STW2.png ├── 55-三色标记问题1.jpeg ├── 56-三色标记问题2.jpeg ├── 57-三色标记问题3.jpeg ├── 58-三色标记问题4.jpeg ├── 59-三色标记问题5.jpeg ├── 6-多进程操作系统.png ├── 60-三色标记问题6.jpeg ├── 61-三色标记问题7.jpeg ├── 62-三色标记插入写屏障1.jpeg ├── 63-三色标记插入写屏障2.jpeg ├── 64-三色标记插入写屏障3.jpeg ├── 65-三色标记插入写屏障4.jpeg ├── 66-三色标记插入写屏障5.jpeg ├── 67-三色标记插入写屏障6.jpeg ├── 68-三色标记插入写屏障7.jpeg ├── 69-三色标记插入写屏障9.jpeg ├── 7-cpu切换浪费成本.png ├── 70-三色标记插入写屏障10.jpeg ├── 71-三色标记插入写屏障11.jpeg ├── 72-三色标记删除写屏障1.jpeg ├── 73-三色标记删除写屏障2.jpeg ├── 74-三色标记删除写屏障3.jpeg ├── 75-三色标记删除写屏障4.jpeg ├── 76-三色标记删除写屏障5.jpeg ├── 77-三色标记删除写屏障6.jpeg ├── 78-三色标记删除写屏障7.jpeg ├── 79-三色标记混合写屏障1.jpeg ├── 8-线程的内核和用户态.png ├── 80-三色标记混合写屏障2.jpeg ├── 81-三色标记混合写屏障3.jpeg ├── 82-三色标记混合写屏障4.jpeg ├── 83-三色标记混合写屏障5.jpeg ├── 84-三色标记混合写屏障6.jpeg ├── 85-三色标记混合写屏障7.jpeg ├── 86-三色标记混合写屏障8.jpeg ├── 87-三色标记混合写屏障9.jpeg ├── 88-三色标记混合写屏障10.jpeg ├── 89-三色标记混合写屏障11.jpeg ├── 9-协程和线程.png ├── 90-三色标记混合写屏障12.jpeg ├── 91-三色标记混合写屏障13.jpeg ├── 92-io-阻塞1.png ├── 93-io-阻塞2.png ├── 94-io-阻塞3.png ├── 95-io-阻塞4.png ├── 96-io-阻塞5.png ├── 97-非阻塞忙轮询.png ├── 98-select.png ├── 99-epoll.png ├── GPM封面.png ├── Golang修养之路封面.jpg ├── 标题.jpeg └── 深入理解Go语言.jpg /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [aceld] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | #custom: https://user-images.githubusercontent.com/7778936/179700249-a89a82e1-d851-4b3d-a43e-c5a91b7d128d.png # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /1、数据定义.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 1、数据定义 4 | 5 | 6 | 7 | ### (1).函数返回值问题 8 | 9 | > 下面代码是否可以编译通过? 10 | 11 | > test1.go 12 | 13 | ```go 14 | package main 15 | 16 | /* 17 | 下面代码是否编译通过? 18 | */ 19 | func myFunc(x,y int)(sum int,error){ 20 | return x+y,nil 21 | } 22 | 23 | func main() { 24 | num, err := myFunc(1, 2) 25 | fmt.Println("num = ", num) 26 | } 27 | 28 | ``` 29 | 30 | 31 | 32 | 答案: 33 | 34 | 编译报错理由: 35 | 36 | ```bash 37 | # command-line-arguments 38 | ./test1.go:6:21: syntax error: mixed named and unnamed function parameters 39 | ``` 40 | 41 | 42 | 43 | > 考点:函数返回值命名 44 | 45 | > 结果:编译出错。 46 | 47 | > 在函数有多个返回值时,只要有一个返回值有指定命名,其他的也必须有命名。 如果返回值有有多个返回值必须加上括号; 如果只有一个返回值并且有命名也需要加上括号; 此处函数第一个返回值有sum名称,第二个未命名,所以错误。 48 | 49 | 50 | 51 | 52 | 53 | ### (2).结构体比较问题 54 | 55 | > 下面代码是否可以编译通过?为什么? 56 | 57 | > test2.go 58 | 59 | ```go 60 | package main 61 | 62 | import "fmt" 63 | 64 | func main() { 65 | 66 | sn1 := struct { 67 | age int 68 | name string 69 | }{age: 11, name: "qq"} 70 | 71 | sn2 := struct { 72 | age int 73 | name string 74 | }{age: 11, name: "qq"} 75 | 76 | if sn1 == sn2 { 77 | fmt.Println("sn1 == sn2") 78 | } 79 | 80 | sm1 := struct { 81 | age int 82 | m map[string]string 83 | }{age: 11, m: map[string]string{"a": "1"}} 84 | 85 | sm2 := struct { 86 | age int 87 | m map[string]string 88 | }{age: 11, m: map[string]string{"a": "1"}} 89 | 90 | if sm1 == sm2 { 91 | fmt.Println("sm1 == sm2") 92 | } 93 | } 94 | 95 | ``` 96 | 97 | 结果 98 | 99 | 编译不通过 100 | 101 | ```bash 102 | ./test2.go:31:9: invalid operation: sm1 == sm2 (struct containing map[string]string cannot be compared) 103 | 104 | ``` 105 | 106 | 考点:**结构体比较** 107 | 108 | > **结构体比较规则注意1**:只有相同类型的结构体才可以比较,结构体是否相同不但与属性类型个数有关,还与属性顺序相关. 109 | 110 | 比如: 111 | 112 | ```go 113 | sn1 := struct { 114 | age int 115 | name string 116 | }{age: 11, name: "qq"} 117 | 118 | sn3:= struct { 119 | name string 120 | age int 121 | }{age:11, name:"qq"} 122 | ``` 123 | 124 | `sn3`与`sn1`就不是相同的结构体了,不能比较。 125 | 126 | 127 | 128 | > **结构体比较规则注意2**:结构体是相同的,但是结构体属性中有不可以比较的类型,如`map`,`slice`,则结构体不能用`==`比较。 129 | 130 | 131 | 132 | 可以使用reflect.DeepEqual进行比较 133 | 134 | ```go 135 | if reflect.DeepEqual(sm1, sm2) { 136 | fmt.Println("sm1 == sm2") 137 | } else { 138 | fmt.Println("sm1 != sm2") 139 | } 140 | ``` 141 | 142 | 143 | 144 | 145 | 146 | ### (3).string与nil类型 147 | 148 | > 下面代码是否能够编译通过?为什么? 149 | 150 | > test3.go 151 | 152 | ```go 153 | package main 154 | 155 | import ( 156 | "fmt" 157 | ) 158 | 159 | func GetValue(m map[int]string, id int) (string, bool) { 160 | if _, exist := m[id]; exist { 161 | return "存在数据", true 162 | } 163 | return nil, false 164 | } 165 | 166 | func main() { 167 | intmap:=map[int]string{ 168 | 1:"a", 169 | 2:"bb", 170 | 3:"ccc", 171 | } 172 | 173 | v,err:=GetValue(intmap,3) 174 | fmt.Println(v,err) 175 | } 176 | 177 | ``` 178 | 179 | 考点:**函数返回值类型** 180 | 181 | 答案:编译不会通过。 182 | 183 | 分析: 184 | 185 | nil 可以用作 interface、function、pointer、map、slice 和 channel 的“空值”。但是如果不特别指定的话,Go 语言不能识别类型,所以会报错。通常编译的时候不会报错,但是运行是时候会报:`cannot use nil as type string in return argument`. 186 | 187 | 所以将`GetValue`函数改成如下形式就可以了 188 | 189 | ```go 190 | func GetValue(m map[int]string, id int) (string, bool) { 191 | if _, exist := m[id]; exist { 192 | return "存在数据", true 193 | } 194 | return "不存在数据", false 195 | } 196 | ``` 197 | 198 | ### (4) 常量 199 | 200 | > 下面函数有什么问题? 201 | 202 | > test4.go 203 | 204 | ```go 205 | package main 206 | 207 | const cl = 100 208 | 209 | var bl = 123 210 | 211 | func main() { 212 | println(&bl,bl) 213 | println(&cl,cl) 214 | } 215 | ``` 216 | 217 | 解析 218 | 219 | 考点:**常量** 220 | 常量不同于变量的在运行期分配内存,常量通常会被编译器在预处理阶段直接展开,作为指令数据使用, 221 | 222 | ``` 223 | cannot take the address of cl 224 | ``` 225 | 226 | 227 | 228 | 内存四区概念: 229 | 230 | #### A.数据类型本质: 231 | 232 | ​ 固定内存大小的别名 233 | 234 | #### B. 数据类型的作用: 235 | 236 | ​ 编译器预算对象(变量)分配的内存空间大小。 237 | 238 | ![](images/108-数据变量.png) 239 | 240 | 241 | 242 | 243 | 244 | #### C. 内存四区 245 | 246 | ![](images/107-内存4区.jpeg) 247 | 248 | 流程说明 249 | 250 | 1、操作系统把物理硬盘代码load到内存 251 | 252 | 2、操作系统把c代码分成四个区 253 | 254 | 3、操作系统找到main函数入口执行 255 | 256 | 257 | 258 | ##### 栈区(Stack): 259 | 260 | ​ 空间较小,要求数据读写性能高,数据存放时间较短暂。由编译器自动分配和释放,存放函数的参数值、函数的调用流程方法地址、局部变量等(局部变量如果产生逃逸现象,可能会挂在在堆区) 261 | 262 | 263 | 264 | ##### 堆区(heap): 265 | 266 | ​ 空间充裕,数据存放时间较久。一般由开发者分配及释放(但是Golang中会根据变量的逃逸现象来选择是否分配到栈上或堆上),启动Golang的GC由GC清除机制自动回收。 267 | 268 | 269 | 270 | ##### 全局区-静态全局变量区: 271 | 272 | ​ 全局变量的开辟是在程序在`main`之前就已经放在内存中。而且对外完全可见。即作用域在全部代码中,任何同包代码均可随时使用,在变量会搞混淆,而且在局部函数中如果同名称变量使用`:=`赋值会出现编译错误。 273 | 274 | ​ 全局变量最终在进程退出时,由操作系统回收。 275 | 276 | > 我么在开发的时候,尽量减少使用全局变量的设计 277 | 278 | 279 | 280 | ###### 全局区-常量区: 281 | 282 | ​ 常量区也归属于全局区,常量为存放数值字面值单位,即不可修改。或者说的有的常量是直接挂钩字面值的。 283 | 284 | 比如: 285 | 286 | ```go 287 | const cl = 10 288 | ``` 289 | 290 | cl是字面量10的对等符号。 291 | 292 | 所以在golang中,常量是无法取出地址的,因为字面量符号并没有地址而言。 293 | 294 | --- 295 | 296 | 297 | -------------------------------------------------------------------------------- /1、最常用的调试golang的bug以及性能问题的实践方法?.md: -------------------------------------------------------------------------------- 1 | 2 | [TOC] 3 | 4 | # 1、最常用的调试 golang 的 bug 以及性能问题的实践方法? 5 | > 本节为**重点**章节 6 | 7 | 8 | ## 场景1: 如何分析程序的运行时间与CPU利用率情况? 9 | 10 | ### (1) shell内置time指令 11 | 12 | 这个方法不算新颖,但是确很实用。 `time`是Unix/Linux内置多命令,使用时一般不用传过多参数,直接跟上需要调试多程序即可。 13 | 14 | 15 | 16 | ```bash 17 | $ time go run test2.go 18 | &{{0 0} 张三 0} 19 | 20 | real 0m0.843s 21 | user 0m0.216s 22 | sys 0m0.389s 23 | ``` 24 | 25 | 26 | 27 | 上面是使用time对 `go run test2.go`对执行程序坐了性能分析,得到3个指标。 28 | 29 | - `real`:从程序开始到结束,实际度过的时间; 30 | - `user`:程序在**用户态**度过的时间; 31 | - `sys`:程序在**内核态**度过的时间。 32 | 33 | 一般情况下 `real` **>=** `user` + `sys`,因为系统还有其它进程(切换其他进程中间对于本进程会有空白期)。 34 | 35 | 36 | 37 | ### (2) /usr/bin/time指令 38 | 39 | 这个指令比内置的time更加详细一些,使用的时候需要用绝对路径,而且要加上参数`-v ` 40 | 41 | ```bash 42 | $ /usr/bin/time -v go run test2.go 43 | 44 | Command being timed: "go run test2.go" 45 | User time (seconds): 0.12 46 | System time (seconds): 0.06 47 | Percent of CPU this job got: 115% 48 | Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.16 49 | Average shared text size (kbytes): 0 50 | Average unshared data size (kbytes): 0 51 | Average stack size (kbytes): 0 52 | Average total size (kbytes): 0 53 | Maximum resident set size (kbytes): 41172 54 | Average resident set size (kbytes): 0 55 | Major (requiring I/O) page faults: 1 56 | Minor (reclaiming a frame) page faults: 15880 57 | Voluntary context switches: 897 58 | Involuntary context switches: 183 59 | Swaps: 0 60 | File system inputs: 256 61 | File system outputs: 2664 62 | Socket messages sent: 0 63 | Socket messages received: 0 64 | Signals delivered: 0 65 | Page size (bytes): 4096 66 | Exit status: 0 67 | 68 | ``` 69 | 70 | 可以看到这里的功能要强大多了,除了之前的信息外,还包括了: 71 | 72 | - CPU占用率; 73 | - 内存使用情况; 74 | - Page Fault 情况; 75 | - 进程切换情况; 76 | - 文件系统IO; 77 | - Socket 使用情况; 78 | - …… 79 | 80 | 81 | 82 | ## 场景2: 如何分析golang程序的内存使用情况? 83 | 84 | 85 | 86 | ### (1) 内存占用情况查看 87 | 88 | 我们先写一段demo例子代码 89 | 90 | ```go 91 | package main 92 | 93 | import ( 94 | "log" 95 | "runtime" 96 | "time" 97 | ) 98 | 99 | func test() { 100 | //slice 会动态扩容,用slice来做堆内存申请 101 | container := make([]int, 8) 102 | 103 | log.Println(" ===> loop begin.") 104 | for i := 0; i < 32*1000*1000; i++ { 105 | container = append(container, i) 106 | } 107 | log.Println(" ===> loop end.") 108 | } 109 | 110 | func main() { 111 | log.Println("Start.") 112 | 113 | test() 114 | 115 | log.Println("force gc.") 116 | runtime.GC() //强制调用gc回收 117 | 118 | log.Println("Done.") 119 | 120 | time.Sleep(3600 * time.Second) //睡眠,保持程序不退出 121 | } 122 | 123 | ``` 124 | 125 | 126 | 127 | 编译 128 | 129 | ```bash 130 | $go build -o snippet_mem && ./snippet_mem 131 | ``` 132 | 133 | 134 | 135 | 然后在./snippet_mem进程没有执行完,我们再开一个窗口,通过`top`命令查看进程的内存占用情况 136 | 137 | ```bash 138 | $top -p $(pidof snippet_mem) 139 | ``` 140 | 141 | 得到结果如下: 142 | 143 | ![](images/37-godebug1.png) 144 | 我们看出来,没有退出的snippet_mem进程有约830m的内存被占用。 145 | 146 | 直观上来说,这个程序在`test()`函数执行完后,切片`contaner`的内存应该被释放,不应该占用830M那么大。 147 | 148 | 下面让我们使用GODEBUG来分析程序的内存使用情况。 149 | 150 | --- 151 | 152 | 153 | 154 | ### (2) GODEBUG与gctrace 155 | 156 | **用法** 157 | 158 | 执行`snippet_mem`程序之前添加环境变量`GODEBUG='gctrace=1'`来跟踪打印垃圾回收器信息 159 | 160 | ```bash 161 | $ GODEBUG='gctrace=1' ./snippet_mem 162 | ``` 163 | 164 | 设置`gctrace=1`会使得垃圾回收器在每次回收时汇总所回收内存的大小以及耗时, 165 | 并将这些内容汇总成单行内容打印到标准错误输出中。 166 | 167 | **格式** 168 | 169 | ``` 170 | gc # @#s #%: #+#+# ms clock, #+#/#/#+# ms cpu, #->#-># MB, # MB goal, # P 171 | ``` 172 | 173 | 含义 174 | 175 | ``` 176 | gc # GC次数的编号,每次GC时递增 177 | @#s 距离程序开始执行时的时间 178 | #% GC占用的执行时间百分比 179 | #+...+# GC使用的时间 180 | #->#-># MB GC开始,结束,以及当前活跃堆内存的大小,单位M 181 | # MB goal 全局堆内存大小 182 | # P 使用processor的数量 183 | ``` 184 | 185 | 如果每条信息最后,以`(forced)`结尾,那么该信息是由`runtime.GC()`调用触发 186 | 187 | 我们来选择其中一行来解释一下: 188 | 189 | ```bash 190 | gc 17 @0.149s 1%: 0.004+36+0.003 ms clock, 0.009+0/0.051/36+0.006 ms cpu, 181->181->101 MB, 182 MB goal, 2 P 191 | ``` 192 | 193 | 该条信息含义如下: 194 | 195 | * `gc 17`: Gc 调试编号为17 196 | * `@0.149s`:此时程序已经执行了0.149s 197 | * `1%`: 0.149s中其中gc模块占用了1%的时间 198 | * `0.004+36+0.003 ms clock`: 垃圾回收的时间,分别为STW(stop-the-world)清扫的时间+并发标记和扫描的时间+STW标记的时间 199 | * `0.009+0/0.051/36+0.006 ms cpu`: 垃圾回收占用cpu时间 200 | * `181->181->101 MB`: GC开始前堆内存181M, GC结束后堆内存181M,当前活跃的堆内存101M 201 | * `182 MB goal`: 全局堆内存大小 202 | * `2 P`: 本次GC使用了2个P(调度器中的Processor) 203 | 204 | --- 205 | 206 | 207 | 208 | 了解了GC的调试信息读法后,接下来我们来分析一下本次GC的结果。 209 | 210 | 我们还是执行GODEBUG调试 211 | 212 | ```bash 213 | $ GODEBUG='gctrace=1' ./snippet_mem 214 | ``` 215 | 216 | 结果如下 217 | 218 | ```bash 219 | 2020/03/02 11:22:37 Start. 220 | 2020/03/02 11:22:37 ===> loop begin. 221 | gc 1 @0.002s 5%: 0.14+0.45+0.002 ms clock, 0.29+0/0.042/0.33+0.005 ms cpu, 4->4->0 MB, 5 MB goal, 2 P 222 | gc 2 @0.003s 4%: 0.13+3.7+0.019 ms clock, 0.27+0/0.037/2.8+0.038 ms cpu, 4->4->2 MB, 5 MB goal, 2 P 223 | gc 3 @0.008s 3%: 0.002+1.1+0.001 ms clock, 0.005+0/0.083/1.0+0.003 ms cpu, 6->6->2 MB, 7 MB goal, 2 P 224 | gc 4 @0.010s 3%: 0.003+0.99+0.002 ms clock, 0.006+0/0.041/0.82+0.004 ms cpu, 5->5->2 MB, 6 MB goal, 2 P 225 | gc 5 @0.011s 4%: 0.079+0.80+0.003 ms clock, 0.15+0/0.046/0.51+0.006 ms cpu, 6->6->3 MB, 7 MB goal, 2 P 226 | gc 6 @0.013s 4%: 0.15+3.7+0.002 ms clock, 0.31+0/0.061/3.3+0.005 ms cpu, 8->8->8 MB, 9 MB goal, 2 P 227 | gc 7 @0.019s 3%: 0.004+2.5+0.005 ms clock, 0.008+0/0.051/2.1+0.010 ms cpu, 20->20->6 MB, 21 MB goal, 2 P 228 | gc 8 @0.023s 5%: 0.014+3.7+0.002 ms clock, 0.029+0.040/1.2/0+0.005 ms cpu, 15->15->8 MB, 16 MB goal, 2 P 229 | gc 9 @0.031s 4%: 0.003+1.6+0.001 ms clock, 0.007+0.094/0/0+0.003 ms cpu, 19->19->10 MB, 20 MB goal, 2 P 230 | gc 10 @0.034s 3%: 0.006+5.2+0.004 ms clock, 0.013+0/0.045/5.0+0.008 ms cpu, 24->24->13 MB, 25 MB goal, 2 P 231 | gc 11 @0.040s 3%: 0.12+2.6+0.002 ms clock, 0.24+0/0.043/2.5+0.004 ms cpu, 30->30->16 MB, 31 MB goal, 2 P 232 | gc 12 @0.043s 3%: 0.11+4.4+0.002 ms clock, 0.23+0/0.044/4.1+0.005 ms cpu, 38->38->21 MB, 39 MB goal, 2 P 233 | gc 13 @0.049s 3%: 0.008+10+0.040 ms clock, 0.017+0/0.045/10+0.080 ms cpu, 47->47->47 MB, 48 MB goal, 2 P 234 | gc 14 @0.070s 2%: 0.004+12+0.002 ms clock, 0.008+0/0.062/12+0.005 ms cpu, 122->122->41 MB, 123 MB goal, 2 P 235 | gc 15 @0.084s 2%: 0.11+11+0.038 ms clock, 0.22+0/0.064/3.9+0.076 ms cpu, 93->93->93 MB, 94 MB goal, 2 P 236 | gc 16 @0.122s 1%: 0.005+25+0.010 ms clock, 0.011+0/0.12/24+0.021 ms cpu, 238->238->80 MB, 239 MB goal, 2 P 237 | gc 17 @0.149s 1%: 0.004+36+0.003 ms clock, 0.009+0/0.051/36+0.006 ms cpu, 181->181->101 MB, 182 MB goal, 2 P 238 | gc 18 @0.187s 1%: 0.12+19+0.004 ms clock, 0.25+0/0.049/19+0.008 ms cpu, 227->227->126 MB, 228 MB goal, 2 P 239 | gc 19 @0.207s 1%: 0.096+27+0.004 ms clock, 0.19+0/0.077/0.73+0.009 ms cpu, 284->284->284 MB, 285 MB goal, 2 P 240 | gc 20 @0.287s 0%: 0.005+944+0.040 ms clock, 0.011+0/0.048/1.3+0.081 ms cpu, 728->728->444 MB, 729 MB goal, 2 P 241 | 2020/03/02 11:22:38 ===> loop end. 242 | 2020/03/02 11:22:38 force gc. 243 | gc 21 @1.236s 0%: 0.004+0.099+0.001 ms clock, 0.008+0/0.018/0.071+0.003 ms cpu, 444->444->0 MB, 888 MB goal, 2 P (forced) 244 | 2020/03/02 11:22:38 Done. 245 | GC forced 246 | gc 22 @122.455s 0%: 0.010+0.15+0.003 ms clock, 0.021+0/0.025/0.093+0.007 ms cpu, 0->0->0 MB, 4 MB goal, 2 P 247 | GC forced 248 | gc 23 @242.543s 0%: 0.007+0.075+0.002 ms clock, 0.014+0/0.022/0.085+0.004 ms cpu, 0->0->0 MB, 4 MB goal, 2 P 249 | GC forced 250 | gc 24 @362.545s 0%: 0.018+0.19+0.006 ms clock, 0.037+0/0.055/0.15+0.013 ms cpu, 0->0->0 MB, 4 MB goal, 2 P 251 | GC forced 252 | gc 25 @482.548s 0%: 0.012+0.25+0.005 ms clock, 0.025+0/0.025/0.11+0.010 ms cpu, 0->0->0 MB, 4 MB goal, 2 P 253 | GC forced 254 | gc 26 @602.551s 0%: 0.009+0.10+0.003 ms clock, 0.018+0/0.021/0.075+0.006 ms cpu, 0->0->0 MB, 4 MB goal, 2 P 255 | GC forced 256 | gc 27 @722.554s 0%: 0.012+0.30+0.005 ms clock, 0.025+0/0.15/0.22+0.011 ms cpu, 0->0->0 MB, 4 MB goal, 2 P 257 | GC forced 258 | gc 28 @842.556s 0%: 0.027+0.18+0.003 ms clock, 0.054+0/0.11/0.14+0.006 ms cpu, 0->0->0 MB, 4 MB goal, 2 P 259 | ... 260 | ``` 261 | 262 | **分析** 263 | 264 | ​ 先看在`test()`函数执行完后立即打印的`gc 21`那行的信息。`444->444->0 MB, 888 MB goal`表示垃圾回收器已经把444M的内存标记为非活跃的内存。 265 | 266 | 再看下一个记录`gc 22`。`0->0->0 MB, 4 MB goal`表示垃圾回收器中的全局堆内存大小由`888M`下降为`4M`。 267 | 268 | **结论** 269 | 270 | **1、在test()函数执行完后,demo程序中的切片容器所申请的堆空间都被垃圾回收器回收了。** 271 | 272 | 2、如果此时在`top`指令查询内存的时候,如果依然是800+MB,说明**垃圾回收器回收了应用层的内存后,(可能)并不会立即将内存归还给系统。** 273 | 274 | 275 | 276 | ### (3)runtime.ReadMemStats 277 | 278 | 接下来我么换另一种方式查看内存的方式 利用 runtime库里的`ReadMemStats()`方法 279 | 280 | > demo2.go 281 | 282 | ```go 283 | package main 284 | 285 | import ( 286 | "log" 287 | "runtime" 288 | "time" 289 | ) 290 | 291 | func readMemStats() { 292 | 293 | var ms runtime.MemStats 294 | 295 | runtime.ReadMemStats(&ms) 296 | 297 | log.Printf(" ===> Alloc:%d(bytes) HeapIdle:%d(bytes) HeapReleased:%d(bytes)", ms.Alloc, ms.HeapIdle, ms.HeapReleased) 298 | } 299 | 300 | func test() { 301 | //slice 会动态扩容,用slice来做堆内存申请 302 | container := make([]int, 8) 303 | 304 | log.Println(" ===> loop begin.") 305 | for i := 0; i < 32*1000*1000; i++ { 306 | container = append(container, i) 307 | if ( i == 16*1000*1000) { 308 | readMemStats() 309 | } 310 | } 311 | 312 | log.Println(" ===> loop end.") 313 | } 314 | 315 | func main() { 316 | log.Println(" ===> [Start].") 317 | 318 | readMemStats() 319 | test() 320 | readMemStats() 321 | 322 | log.Println(" ===> [force gc].") 323 | runtime.GC() //强制调用gc回收 324 | 325 | log.Println(" ===> [Done].") 326 | readMemStats() 327 | 328 | go func() { 329 | for { 330 | readMemStats() 331 | time.Sleep(10 * time.Second) 332 | } 333 | }() 334 | 335 | time.Sleep(3600 * time.Second) //睡眠,保持程序不退出 336 | } 337 | ``` 338 | 339 | 这里我们, 封装了一个函数`readMemStats()`,这里面主要是调用`runtime`中的`ReadMemStats()`方法获得内存信息,然后通过`log`打印出来。 340 | 341 | 我们执行一下代码并运行 342 | 343 | 344 | 345 | ```bash 346 | $ go run demo2.go 347 | 2020/03/02 18:21:17 ===> [Start]. 348 | 2020/03/02 18:21:17 ===> Alloc:71280(bytes) HeapIdle:66633728(bytes) HeapReleased:66600960(bytes) 349 | 2020/03/02 18:21:17 ===> loop begin. 350 | 2020/03/02 18:21:18 ===> Alloc:132535744(bytes) HeapIdle:336756736(bytes) HeapReleased:155721728(bytes) 351 | 2020/03/02 18:21:38 ===> loop end. 352 | 2020/03/02 18:21:38 ===> Alloc:598300600(bytes) HeapIdle:609181696(bytes) HeapReleased:434323456(bytes) 353 | 2020/03/02 18:21:38 ===> [force gc]. 354 | 2020/03/02 18:21:38 ===> [Done]. 355 | 2020/03/02 18:21:38 ===> Alloc:55840(bytes) HeapIdle:1207427072(bytes) HeapReleased:434266112(bytes) 356 | 2020/03/02 18:21:38 ===> Alloc:56656(bytes) HeapIdle:1207394304(bytes) HeapReleased:434266112(bytes) 357 | 2020/03/02 18:21:48 ===> Alloc:56912(bytes) HeapIdle:1207394304(bytes) HeapReleased:1206493184(bytes) 358 | 2020/03/02 18:21:58 ===> Alloc:57488(bytes) HeapIdle:1207394304(bytes) HeapReleased:1206493184(bytes) 359 | 2020/03/02 18:22:08 ===> Alloc:57616(bytes) HeapIdle:1207394304(bytes) HeapReleased:1206493184(bytes) 360 | c2020/03/02 18:22:18 ===> Alloc:57744(bytes) HeapIdle:1207394304(bytes) HeapReleased:1206493184(by 361 | ``` 362 | 363 | ​ 可以看到,打印`[Done].`之后那条trace信息,Alloc已经下降,即内存已被垃圾回收器回收。在`2020/03/02 18:21:38`和`2020/03/02 18:21:48`的两条trace信息中,HeapReleased开始上升,即垃圾回收器把内存归还给系统。 364 | 365 | 另外,MemStats还可以获取其它哪些信息以及字段的含义可以参见官方文档: 366 | 367 | > http://golang.org/pkg/runtime/#MemStats 368 | 369 | 370 | 371 | ### (4)pprof工具 372 | 373 | pprof工具支持网页上查看内存的使用情况,需要在代码中添加一个协程即可。 374 | 375 | ```go 376 | import( 377 | "net/http" 378 | _ "net/http/pprof" 379 | ) 380 | 381 | go func() { 382 | log.Println(http.ListenAndServe("0.0.0.0:10000", nil)) 383 | }() 384 | ``` 385 | 386 | 具体添加的完整代码如下: 387 | 388 | > demo3.go 389 | 390 | ```go 391 | package main 392 | 393 | import ( 394 | "log" 395 | "runtime" 396 | "time" 397 | "net/http" 398 | _ "net/http/pprof" 399 | ) 400 | 401 | func readMemStats() { 402 | 403 | var ms runtime.MemStats 404 | 405 | runtime.ReadMemStats(&ms) 406 | 407 | log.Printf(" ===> Alloc:%d(bytes) HeapIdle:%d(bytes) HeapReleased:%d(bytes)", ms.Alloc, ms.HeapIdle, ms.HeapReleased) 408 | } 409 | 410 | func test() { 411 | //slice 会动态扩容,用slice来做堆内存申请 412 | container := make([]int, 8) 413 | 414 | log.Println(" ===> loop begin.") 415 | for i := 0; i < 32*1000*1000; i++ { 416 | container = append(container, i) 417 | if ( i == 16*1000*1000) { 418 | readMemStats() 419 | } 420 | } 421 | 422 | log.Println(" ===> loop end.") 423 | } 424 | 425 | func main() { 426 | 427 | 428 | //启动pprof 429 | go func() { 430 | log.Println(http.ListenAndServe("0.0.0.0:10000", nil)) 431 | }() 432 | 433 | log.Println(" ===> [Start].") 434 | 435 | readMemStats() 436 | test() 437 | readMemStats() 438 | 439 | log.Println(" ===> [force gc].") 440 | runtime.GC() //强制调用gc回收 441 | 442 | log.Println(" ===> [Done].") 443 | readMemStats() 444 | 445 | go func() { 446 | for { 447 | readMemStats() 448 | time.Sleep(10 * time.Second) 449 | } 450 | }() 451 | 452 | time.Sleep(3600 * time.Second) //睡眠,保持程序不退出 453 | } 454 | 455 | ``` 456 | 457 | 我们正常运行程序,然后同时打开浏览器, 458 | 459 | 输入地址:http://127.0.0.1:10000/debug/pprof/heap?debug=1 460 | 461 | 浏览器的内容其中有一部分如下,记录了目前的内存情况 462 | 463 | ```bash 464 | # ... 465 | 466 | # runtime.MemStats 467 | # Alloc = 228248 468 | # TotalAlloc = 1293696976 469 | # Sys = 834967896 470 | # Lookups = 0 471 | # Mallocs = 2018 472 | # Frees = 671 473 | # HeapAlloc = 228248 474 | # HeapSys = 804913152 475 | # HeapIdle = 804102144 476 | # HeapInuse = 811008 477 | # HeapReleased = 108552192 478 | # HeapObjects = 1347 479 | # Stack = 360448 / 360448 480 | # MSpan = 28288 / 32768 481 | # MCache = 3472 / 16384 482 | # BuckHashSys = 1449617 483 | # GCSys = 27418976 484 | # OtherSys = 776551 485 | # NextGC = 4194304 486 | # LastGC = 1583203571137891390 487 | 488 | # ... 489 | ``` 490 | 491 | 492 | 493 | ## 场景3: 如何分析Golang程序的CPU性能情况? 494 | 495 | 496 | 497 | ### (1)性能分析注意事项 498 | 499 | - 性能分析必须在一个 500 | 501 | 可重复的、稳定的环境中来进行。 502 | 503 | - 机器必须闲置 504 | - 不要在共享硬件上进行性能分析; 505 | - 不要在性能分析期间,在同一个机器上去浏览网页 506 | - 注意省电模式和过热保护,如果突然进入这些模式,会导致分析数据严重不准确 507 | - **不要使用虚拟机、共享的云主机**,太多干扰因素,分析数据会很不一致; 508 | - 不要在 macOS 10.11 及以前的版本运行性能分析,有 bug,之后的版本修复了。 509 | 510 | 如果承受得起,购买专用的性能测试分析的硬件设备,上架。 511 | 512 | - 关闭电源管理、过热管理; 513 | - 绝不要升级,以保证测试的一致性,以及具有可比性。 514 | 515 | 如果没有这样的环境,那就一定要在多个环境中,执行多次,以取得可参考的、具有相对一致性的测试结果。 516 | 517 | 518 | 519 | ### (2) CPU性能分析 520 | 521 | 我们来用下面的代码进行测试 522 | 523 | > demo4.go 524 | 525 | ```go 526 | package main 527 | 528 | import ( 529 | "bytes" 530 | "math/rand" 531 | "time" 532 | "log" 533 | "net/http" 534 | _ "net/http/pprof" 535 | ) 536 | 537 | 538 | func test() { 539 | 540 | log.Println(" ===> loop begin.") 541 | for i := 0; i < 1000; i++ { 542 | log.Println(genSomeBytes()) 543 | } 544 | 545 | log.Println(" ===> loop end.") 546 | } 547 | 548 | //生成一个随机字符串 549 | func genSomeBytes() *bytes.Buffer { 550 | 551 | var buff bytes.Buffer 552 | 553 | for i := 1; i < 20000; i++ { 554 | buff.Write([]byte{'0' + byte(rand.Intn(10))}) 555 | } 556 | 557 | return &buff 558 | } 559 | 560 | func main() { 561 | 562 | go func() { 563 | for { 564 | test() 565 | time.Sleep(time.Second * 1) 566 | } 567 | }() 568 | 569 | //启动pprof 570 | http.ListenAndServe("0.0.0.0:10000", nil) 571 | 572 | } 573 | 574 | ``` 575 | 576 | 这里面还是启动了pprof的监听,有关`pprof`启动的代码如下 577 | 578 | ```go 579 | import ( 580 | "net/http" 581 | _ "net/http/pprof" 582 | ) 583 | 584 | func main() { 585 | //... 586 | //... 587 | 588 | //启动pprof 589 | http.ListenAndServe("0.0.0.0:10000", nil) 590 | } 591 | ``` 592 | 593 | `main()`里的流程很简单,启动一个goroutine去无限循环调用`test()`方法,休眠1s. 594 | 595 | `test()`的流程是生成1000个20000个字符的随机字符串.并且打印. 596 | 597 | 我们将上面的代码编译成可执行的二进制文件 `demo4`(记住这个名字,稍后我们能用到) 598 | 599 | ```bash 600 | $ go build demo4.go 601 | ``` 602 | 603 | 604 | 605 | 接下来我们启动程序,程序会无限循环的打印字符串. 606 | 607 | 接下来我们通过几种方式来查看进程的cpu性能情况. 608 | 609 | 610 | 611 | #### A. Web界面查看 612 | 613 | 浏览器访问http://127.0.0.1:10000/debug/pprof/ 614 | 615 | 我们会看到如下画面 616 | 617 | ![](images/38-pprof-profile-web.png) 618 | 619 | 这里面能够通过pprof查看包括(阻塞信息、cpu信息、内存堆信息、锁信息、goroutine信息等等), 我们这里关心的cpu的性能的`profile`信息. 620 | 621 | 有关`profile`下面的英文解释大致如下: 622 | 623 | > “CPU配置文件。您可以在秒GET参数中指定持续时间。获取概要文件后,请使用go tool pprof命令调查概要文件。” 624 | 625 | 所以我们要是想得到cpu性能,就是要获取到当前进程的`profile`文件,这个文件默认是30s生成一个,所以你的程序要至少运行30s以上(这个参数也可以修改,稍后我们介绍) 626 | 627 | 我们可以直接点击网页的`profile`,浏览器会给我们下载一个`profile`文件. 记住这个文件的路径, 可以拷贝到与`demo4`所在的同一文件夹下. 628 | 629 | 630 | 631 | #### B. 使用pprof工具查看 632 | 633 | pprof 的格式如下 634 | 635 | ```bash 636 | go tool pprof [binary] [profile] 637 | ``` 638 | 639 | binary: 必须指向生成这个性能分析数据的那个二进制可执行文件; 640 | 641 | profile: 必须是该二进制可执行文件所生成的性能分析数据文件。 642 | 643 | **`binary` 和 `profile` 必须严格匹配**。 644 | 645 | 我们来查看一下: 646 | 647 | ```bash 648 | $ go tool pprof ./demo4 profile 649 | 650 | File: demo4 651 | Type: cpu 652 | Time: Mar 3, 2020 at 11:18pm (CST) 653 | Duration: 30.13s, Total samples = 6.27s (20.81%) 654 | Entering interactive mode (type "help" for commands, "o" for options) 655 | (pprof) 656 | ``` 657 | 658 | **help**可以查看一些指令,我么可以通过**top**来查看cpu的性能情况. 659 | 660 | ```bash 661 | (pprof) top 662 | Showing nodes accounting for 5090ms, 81.18% of 6270ms total 663 | Dropped 80 nodes (cum <= 31.35ms) 664 | Showing top 10 nodes out of 60 665 | flat flat% sum% cum cum% 666 | 1060ms 16.91% 16.91% 2170ms 34.61% math/rand.(*lockedSource).Int63 667 | 850ms 13.56% 30.46% 850ms 13.56% sync.(*Mutex).Unlock (inline) 668 | 710ms 11.32% 41.79% 2950ms 47.05% math/rand.(*Rand).Int31n 669 | 570ms 9.09% 50.88% 990ms 15.79% bytes.(*Buffer).Write 670 | 530ms 8.45% 59.33% 540ms 8.61% syscall.Syscall 671 | 370ms 5.90% 65.23% 370ms 5.90% runtime.procyield 672 | 270ms 4.31% 69.54% 4490ms 71.61% main.genSomeBytes 673 | 250ms 3.99% 73.52% 3200ms 51.04% math/rand.(*Rand).Intn 674 | 250ms 3.99% 77.51% 250ms 3.99% runtime.memmove 675 | 230ms 3.67% 81.18% 690ms 11.00% runtime.suspendG 676 | (pprof) 677 | 678 | ``` 679 | 680 | 这里面有几列数据,需要说明一下. 681 | 682 | - flat:当前函数占用CPU的耗时 683 | - flat%::当前函数占用CPU的耗时百分比 684 | - sun%:函数占用CPU的耗时累计百分比 685 | - cum:当前函数加上调用当前函数的函数占用CPU的总耗时 686 | - cum%:当前函数加上调用当前函数的函数占用CPU的总耗时百分比 687 | - 最后一列:函数名称 688 | 689 | 通过结果我们可以看出, 该程序的大部分cpu性能消耗在 `main.getSoneBytes()`方法中,其中math/rand取随机数消耗比较大. 690 | 691 | 692 | 693 | #### C. 通过go tool pprof得到profile文件 694 | 695 | 我们上面的profile文件是通过web浏览器下载的,这个profile的经过时间是30s的,默认值我们在浏览器上修改不了,如果你想得到时间更长的cpu利用率,可以通过`go tool pprof`指令与程序交互来获取到 696 | 697 | 首先,我们先启动程序 698 | 699 | ``` 700 | $ ./demo4 701 | ``` 702 | 703 | 然后再打开一个终端 704 | 705 | ```bash 706 | go tool pprof http://localhost:10000/debug/pprof/profile?seconds=60 707 | ``` 708 | 709 | 这里制定了生成profile文件的时间间隔60s 710 | 711 | 等待60s之后, 终端就会有结果出来,我们继续使用top来查看. 712 | 713 | ```bash 714 | $ go tool pprof http://localhost:10000/debug/pprof/profile?seconds=60 715 | Fetching profile over HTTP from http://localhost:10000/debug/pprof/profile?seconds=60 716 | Saved profile in /home/itheima/pprof/pprof.demo4.samples.cpu.005.pb.gz 717 | File: demo4 718 | Type: cpu 719 | Time: Mar 3, 2020 at 11:59pm (CST) 720 | Duration: 1mins, Total samples = 12.13s (20.22%) 721 | Entering interactive mode (type "help" for commands, "o" for options) 722 | (pprof) top 723 | Showing nodes accounting for 9940ms, 81.95% of 12130ms total 724 | Dropped 110 nodes (cum <= 60.65ms) 725 | Showing top 10 nodes out of 56 726 | flat flat% sum% cum cum% 727 | 2350ms 19.37% 19.37% 4690ms 38.66% math/rand.(*lockedSource).Int63 728 | 1770ms 14.59% 33.97% 1770ms 14.59% sync.(*Mutex).Unlock (inline) 729 | 1290ms 10.63% 44.60% 6040ms 49.79% math/rand.(*Rand).Int31n 730 | 1110ms 9.15% 53.75% 1130ms 9.32% syscall.Syscall 731 | 810ms 6.68% 60.43% 1860ms 15.33% bytes.(*Buffer).Write 732 | 620ms 5.11% 65.54% 6660ms 54.91% math/rand.(*Rand).Intn 733 | 570ms 4.70% 70.24% 570ms 4.70% runtime.procyield 734 | 500ms 4.12% 74.36% 9170ms 75.60% main.genSomeBytes 735 | 480ms 3.96% 78.32% 480ms 3.96% runtime.memmove 736 | 440ms 3.63% 81.95% 440ms 3.63% math/rand.(*rngSource).Uint64 737 | (pprof) 738 | 739 | ``` 740 | 741 | 依然会得到cpu性能的结果, 我们发现这次的结果与上次30s的结果百分比类似. 742 | 743 | 744 | 745 | #### D.可视化查看 746 | 747 | 我们还是通过 748 | 749 | ```bash 750 | $ go tool pprof ./demo4 profile 751 | ``` 752 | 753 | 进入profile文件查看,然后我们输入`web`指令. 754 | 755 | ```bash 756 | $ go tool pprof ./demo4 profileFile: demo4 757 | Type: cpu 758 | Time: Mar 3, 2020 at 11:18pm (CST) 759 | Duration: 30.13s, Total samples = 6.27s (20.81%) 760 | Entering interactive mode (type "help" for commands, "o" for options) 761 | (pprof) web 762 | 763 | ``` 764 | 765 | 这里如果报找不到`graphviz`工具,需要安装一下 766 | 767 | **Ubuntu安装** 768 | 769 | ``` 770 | $sudo apt-get install graphviz 771 | ``` 772 | 773 | **Mac安装** 774 | 775 | ``` 776 | brew install graphviz 777 | ``` 778 | 779 | **windows安装** 780 | 781 | 下载https://graphviz.gitlab.io/_pages/Download/Download_windows.html 782 | 783 | ```bash 784 | 将graphviz安装目录下的bin文件夹添加到Path环境变量中。 在终端输入dot -version查看是否安装成功。 785 | ``` 786 | 787 | 788 | 789 | 然后我们得到一个svg的可视化文件在`/tmp`路径下 790 | 791 | ![](images/39-pprof001.png) 792 | 793 | 这样我们就能比较清晰的看到函数之间的调用关系,方块越大的表示cpu的占用越大. 794 | 795 | 796 | -------------------------------------------------------------------------------- /1、流?I-O操作?阻塞?epoll.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 1、流?I/O操作?阻塞?epoll? 4 | 5 | ### 一、流?I/O操作? 阻塞? 6 | 7 | #### (1) 流 8 | 9 | - 可以进行I/O操作的内核对象 10 | - 文件、管道、套接字…… 11 | - 流的入口:文件描述符(fd) 12 | 13 | #### (2) I/O操作 14 | 15 | 所有对流的读写操作,我们都可以称之为IO操作。 16 | 17 | 18 | 19 | 当一个流中, 在没有数据read的时候,或者说在流中已经写满了数据,再write,我们的IO操作就会出现一种现象,就是阻塞现象,如下图。 20 | 21 | ![](images/92-io-阻塞1.png) 22 | 23 | 24 | ![](images/93-io-阻塞2.png) 25 | 26 | --- 27 | 28 | 29 | 30 | #### (3) 阻塞 31 | ![](images/94-io-阻塞3.png) 32 | 33 | ​ **阻塞场景**: 你有一份快递,家里有个座机,快递到了主动给你打电话,期间你可以休息。 34 | ![](images/95-io-阻塞4.png) 35 | 36 | **非阻塞,忙轮询场景**: 你性子比较急躁, 每分钟就要打电话询问快递小哥一次, 到底有没有到,快递员接你电话要停止运输,这样很耽误快递小哥的运输速度。 37 | 38 | 39 | 40 | * 阻塞等待 41 | 42 | 空出大脑可以安心睡觉, 不影响快递员工作(不占用CPU宝贵的时间片)。 43 | 44 | - 非阻塞,忙轮询 45 | 46 | 浪费时间,浪费电话费,占用快递员时间(占用CPU,系统资源)。 47 | 48 | 49 | 50 | 很明显,阻塞等待这种方式,对于通信上是有明显优势的, 那么它有哪些弊端呢? 51 | 52 | 53 | 54 | ### 二、解决阻塞死等待的办法 55 | 56 | #### 阻塞死等待的缺点 57 | ![](images/96-io-阻塞5.png) 58 | 59 | ​ 也就是同一时刻,你只能被动的处理一个快递员的签收业务,其他快递员打电话打不进来,只能干瞪眼等待。那么解决这个问题,家里多买N个座机, 但是依然是你一个人接,也处理不过来,需要用影分身术创建都个自己来接电话(采用多线程或者多进程)来处理。 60 | 61 | ​ 这种方式就是没有多路IO复用的情况的解决方案, 但是在单线程计算机时代(无法影分身),这简直是灾难。 62 | 63 | 64 | 65 | --- 66 | 67 | 68 | 69 | 那么如果我们不借助影分身的方式(多线程/多进程),该如何解决阻塞死等待的方法呢? 70 | 71 | #### 办法一:非阻塞、忙轮询 72 | 73 | ![](images/97-非阻塞忙轮询.png) 74 | 75 | ```go 76 | while true { 77 | for i in 流[] { 78 | if i has 数据 { 79 | 读 或者 其他处理 80 | } 81 | } 82 | } 83 | ``` 84 | 85 | 非阻塞忙轮询的方式,可以让用户分别与每个快递员取得联系,宏观上来看,是同时可以与多个快递员沟通(并发效果)、 但是快递员在于用户沟通时耽误前进的速度(浪费CPU)。 86 | 87 | --- 88 | 89 | #### 办法二:select 90 | ![](images/98-select.png) 91 | 92 | 我们可以开设一个代收网点,让快递员全部送到代收点。这个网店管理员叫select。这样我们就可以在家休息了,麻烦的事交给select就好了。当有快递的时候,select负责给我们打电话,期间在家休息睡觉就好了。 93 | 94 | 95 | 96 | 但select 代收员比较懒,她记不住快递员的单号,还有快递货物的数量。她只会告诉你快递到了,但是是谁到的,你需要挨个快递员问一遍。 97 | 98 | ```go 99 | while true { 100 | select(流[]); //阻塞 101 | 102 | //有消息抵达 103 | for i in 流[] { 104 | if i has 数据 { 105 | 读 或者 其他处理 106 | } 107 | } 108 | } 109 | ``` 110 | 111 | --- 112 | 113 | #### 办法三:epoll 114 | 115 | ![](images/99-epoll.png) 116 | 117 | epoll的服务态度要比select好很多,在通知我们的时候,不仅告诉我们有几个快递到了,还分别告诉我们是谁谁谁。我们只需要按照epoll给的答复,来询问快递员取快递即可。 118 | 119 | ```go 120 | while true { 121 | 可处理的流[] = epoll_wait(epoll_fd); //阻塞 122 | 123 | //有消息抵达,全部放在 “可处理的流[]”中 124 | for i in 可处理的流[] { 125 | 读 或者 其他处理 126 | } 127 | } 128 | ``` 129 | 130 | --- 131 | 132 | ### 三、epoll? 133 | 134 | - 与select,poll一样,对I/O多路复用的技术 135 | - 只关心“活跃”的链接,无需遍历全部描述符集合 136 | - 能够处理大量的链接请求(系统可以打开的文件数目) 137 | 138 | ### 四、epoll的API 139 | 140 | #### (1) 创建EPOLL 141 | 142 | ```c 143 | /** 144 | * @param size 告诉内核监听的数目 145 | * 146 | * @returns 返回一个epoll句柄(即一个文件描述符) 147 | */ 148 | int epoll_create(int size); 149 | ``` 150 | 151 | 使用 152 | 153 | ```c 154 | int epfd = epoll_create(1000); 155 | ``` 156 | ![](images/100-epoll.png) 157 | 158 | 创建一个epoll句柄,实际上是在内核空间,建立一个root根节点,这个根节点的关系与epfd相对应。 159 | 160 | 161 | 162 | #### (2) 控制EPOLL 163 | 164 | ```c 165 | /** 166 | * @param epfd 用epoll_create所创建的epoll句柄 167 | * @param op 表示对epoll监控描述符控制的动作 168 | * 169 | * EPOLL_CTL_ADD(注册新的fd到epfd) 170 | * EPOLL_CTL_MOD(修改已经注册的fd的监听事件) 171 | * EPOLL_CTL_DEL(epfd删除一个fd) 172 | * 173 | * @param fd 需要监听的文件描述符 174 | * @param event 告诉内核需要监听的事件 175 | * 176 | * @returns 成功返回0,失败返回-1, errno查看错误信息 177 | */ 178 | int epoll_ctl(int epfd, int op, int fd, 179 | struct epoll_event *event); 180 | 181 | 182 | struct epoll_event { 183 | __uint32_t events; /* epoll 事件 */ 184 | epoll_data_t data; /* 用户传递的数据 */ 185 | } 186 | 187 | /* 188 | * events : {EPOLLIN, EPOLLOUT, EPOLLPRI, 189 | EPOLLHUP, EPOLLET, EPOLLONESHOT} 190 | */ 191 | typedef union epoll_data { 192 | void *ptr; 193 | int fd; 194 | uint32_t u32; 195 | uint64_t u64; 196 | } epoll_data_t; 197 | ``` 198 | 199 | 使用 200 | 201 | ```c 202 | struct epoll_event new_event; 203 | 204 | new_event.events = EPOLLIN | EPOLLOUT; 205 | new_event.data.fd = 5; 206 | 207 | epoll_ctl(epfd, EPOLL_CTL_ADD, 5, &new_event); 208 | ``` 209 | 210 | ​ 创建一个用户态的事件,绑定到某个fd上,然后添加到内核中的epoll红黑树中。 211 | 212 | ![](images/101-epoll3.png) 213 | 214 | #### (3) 等待EPOLL 215 | 216 | ```c 217 | /** 218 | * 219 | * @param epfd 用epoll_create所创建的epoll句柄 220 | * @param event 从内核得到的事件集合 221 | * @param maxevents 告知内核这个events有多大, 222 | * 注意: 值 不能大于创建epoll_create()时的size. 223 | * @param timeout 超时时间 224 | * -1: 永久阻塞 225 | * 0: 立即返回,非阻塞 226 | * >0: 指定微秒 227 | * 228 | * @returns 成功: 有多少文件描述符就绪,时间到时返回0 229 | * 失败: -1, errno 查看错误 230 | */ 231 | int epoll_wait(int epfd, struct epoll_event *event, 232 | int maxevents, int timeout); 233 | ``` 234 | 235 | 使用 236 | 237 | ```c 238 | struct epoll_event my_event[1000]; 239 | 240 | int event_cnt = epoll_wait(epfd, my_event, 1000, -1); 241 | ``` 242 | 243 | ​ `epoll_wait`是一个阻塞的状态,如果内核检测到IO的读写响应,会抛给上层的epoll_wait, 返回给用户态一个已经触发的事件队列,同时阻塞返回。开发者可以从队列中取出事件来处理,其中事件里就有绑定的对应fd是哪个(之前添加epoll事件的时候已经绑定)。 244 | 245 | ![](images/102-epoll4.png) 246 | 247 | 248 | #### (4) 使用epoll编程主流程骨架 249 | 250 | ```c 251 | int epfd = epoll_crete(1000); 252 | 253 | //将 listen_fd 添加进 epoll 中 254 | epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd,&listen_event); 255 | 256 | while (1) { 257 | //阻塞等待 epoll 中 的fd 触发 258 | int active_cnt = epoll_wait(epfd, events, 1000, -1); 259 | 260 | for (i = 0 ; i < active_cnt; i++) { 261 | if (evnets[i].data.fd == listen_fd) { 262 | //accept. 并且将新accept 的fd 加进epoll中. 263 | } 264 | else if (events[i].events & EPOLLIN) { 265 | //对此fd 进行读操作 266 | } 267 | else if (events[i].events & EPOLLOUT) { 268 | //对此fd 进行写操作 269 | } 270 | } 271 | } 272 | ``` 273 | 274 | 275 | ### 五、epoll的触发模式 276 | 277 | #### (1) 水平触发 278 | ![](images/103-epoll水平触发1.png) 279 | ![](images/104-epoll水平触发2.png) 280 | 281 | 水平触发的主要特点是,如果用户在监听`epoll`事件,当内核有事件的时候,会拷贝给用户态事件,但是**如果用户只处理了一次,那么剩下没有处理的会在下一次epoll_wait再次返回该事件**。 282 | 283 | 这样如果用户永远不处理这个事件,就导致每次都会有该事件从内核到用户的拷贝,耗费性能,但是水平触发相对安全,最起码事件不会丢掉,除非用户处理完毕。 284 | 285 | 286 | 287 | ##### (2) 边缘触发 288 | ![](images/105-epoll边缘触发1.png) 289 | 290 | ![](images/105-epoll边缘触发2.png) 291 | 292 | 293 | 294 | 边缘触发,相对跟水平触发相反,当内核有事件到达, 只会通知用户一次,至于用户处理还是不处理,以后将不会再通知。这样减少了拷贝过程,增加了性能,但是相对来说,如果用户马虎忘记处理,将会产生事件丢的情况。 295 | 296 | 297 | 298 | ### 六、简单的epoll服务器(C语言) 299 | 300 | #### (1) 服务端 301 | 302 | ```c 303 | #include 304 | #include 305 | #include 306 | #include 307 | 308 | #include 309 | #include 310 | #include 311 | #include 312 | 313 | #include 314 | 315 | #define SERVER_PORT (7778) 316 | #define EPOLL_MAX_NUM (2048) 317 | #define BUFFER_MAX_LEN (4096) 318 | 319 | char buffer[BUFFER_MAX_LEN]; 320 | 321 | void str_toupper(char *str) 322 | { 323 | int i; 324 | for (i = 0; i < strlen(str); i ++) { 325 | str[i] = toupper(str[i]); 326 | } 327 | } 328 | 329 | int main(int argc, char **argv) 330 | { 331 | int listen_fd = 0; 332 | int client_fd = 0; 333 | struct sockaddr_in server_addr; 334 | struct sockaddr_in client_addr; 335 | socklen_t client_len; 336 | 337 | int epfd = 0; 338 | struct epoll_event event, *my_events; 339 | 340 | / socket 341 | listen_fd = socket(AF_INET, SOCK_STREAM, 0); 342 | 343 | // bind 344 | server_addr.sin_family = AF_INET; 345 | server_addr.sin_addr.s_addr = htonl(INADDR_ANY); 346 | server_addr.sin_port = htons(SERVER_PORT); 347 | bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)); 348 | 349 | // listen 350 | listen(listen_fd, 10); 351 | 352 | // epoll create 353 | epfd = epoll_create(EPOLL_MAX_NUM); 354 | if (epfd < 0) { 355 | perror("epoll create"); 356 | goto END; 357 | } 358 | 359 | // listen_fd -> epoll 360 | event.events = EPOLLIN; 361 | event.data.fd = listen_fd; 362 | if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &event) < 0) { 363 | perror("epoll ctl add listen_fd "); 364 | goto END; 365 | } 366 | 367 | my_events = malloc(sizeof(struct epoll_event) * EPOLL_MAX_NUM); 368 | 369 | 370 | while (1) { 371 | // epoll wait 372 | int active_fds_cnt = epoll_wait(epfd, my_events, EPOLL_MAX_NUM, -1); 373 | int i = 0; 374 | for (i = 0; i < active_fds_cnt; i++) { 375 | // if fd == listen_fd 376 | if (my_events[i].data.fd == listen_fd) { 377 | //accept 378 | client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len); 379 | if (client_fd < 0) { 380 | perror("accept"); 381 | continue; 382 | } 383 | 384 | char ip[20]; 385 | printf("new connection[%s:%d]\n", inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip)), ntohs(client_addr.sin_port)); 386 | 387 | event.events = EPOLLIN | EPOLLET; 388 | event.data.fd = client_fd; 389 | epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &event); 390 | } 391 | else if (my_events[i].events & EPOLLIN) { 392 | printf("EPOLLIN\n"); 393 | client_fd = my_events[i].data.fd; 394 | 395 | // do read 396 | 397 | buffer[0] = '\0'; 398 | int n = read(client_fd, buffer, 5); 399 | if (n < 0) { 400 | perror("read"); 401 | continue; 402 | } 403 | else if (n == 0) { 404 | epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &event); 405 | close(client_fd); 406 | } 407 | else { 408 | printf("[read]: %s\n", buffer); 409 | buffer[n] = '\0'; 410 | #if 1 411 | str_toupper(buffer); 412 | write(client_fd, buffer, strlen(buffer)); 413 | printf("[write]: %s\n", buffer); 414 | memset(buffer, 0, BUFFER_MAX_LEN); 415 | #endif 416 | 417 | /* 418 | event.events = EPOLLOUT; 419 | event.data.fd = client_fd; 420 | epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, &event); 421 | */ 422 | } 423 | } 424 | else if (my_events[i].events & EPOLLOUT) { 425 | printf("EPOLLOUT\n"); 426 | /* 427 | client_fd = my_events[i].data.fd; 428 | str_toupper(buffer); 429 | write(client_fd, buffer, strlen(buffer)); 430 | printf("[write]: %s\n", buffer); 431 | memset(buffer, 0, BUFFER_MAX_LEN); 432 | 433 | event.events = EPOLLIN; 434 | event.data.fd = client_fd; 435 | epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, &event); 436 | */ 437 | } 438 | } 439 | } 440 | 441 | END: 442 | close(epfd); 443 | close(listen_fd); 444 | return 0; 445 | } 446 | ``` 447 | 448 | 449 | 450 | 451 | 452 | #### (2) 客户端 453 | 454 | ```c 455 | #include 456 | #include 457 | #include 458 | #include 459 | 460 | #include 461 | #include 462 | #include 463 | #include 464 | #include 465 | 466 | #define MAX_LINE (1024) 467 | #define SERVER_PORT (7778) 468 | 469 | void setnoblocking(int fd) 470 | { 471 | int opts = 0; 472 | opts = fcntl(fd, F_GETFL); 473 | opts = opts | O_NONBLOCK; 474 | fcntl(fd, F_SETFL); 475 | } 476 | 477 | int main(int argc, char **argv) 478 | { 479 | int sockfd; 480 | char recvline[MAX_LINE + 1] = {0}; 481 | 482 | struct sockaddr_in server_addr; 483 | 484 | if (argc != 2) { 485 | fprintf(stderr, "usage ./client \n"); 486 | exit(0); 487 | } 488 | 489 | 490 | // 创建socket 491 | if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { 492 | fprintf(stderr, "socket error"); 493 | exit(0); 494 | } 495 | 496 | 497 | // server addr 赋值 498 | bzero(&server_addr, sizeof(server_addr)); 499 | server_addr.sin_family = AF_INET; 500 | server_addr.sin_port = htons(SERVER_PORT); 501 | 502 | if (inet_pton(AF_INET, argv[1], &server_addr.sin_addr) <= 0) { 503 | fprintf(stderr, "inet_pton error for %s", argv[1]); 504 | exit(0); 505 | } 506 | 507 | 508 | // 链接服务端 509 | if (connect(sockfd, (struct sockaddr*) &server_addr, sizeof(server_addr)) < 0) { 510 | perror("connect"); 511 | fprintf(stderr, "connect error\n"); 512 | exit(0); 513 | } 514 | 515 | setnoblocking(sockfd); 516 | 517 | char input[100]; 518 | int n = 0; 519 | int count = 0; 520 | 521 | 522 | 523 | // 不断的从标准输入字符串 524 | while (fgets(input, 100, stdin) != NULL) 525 | { 526 | printf("[send] %s\n", input); 527 | n = 0; 528 | // 把输入的字符串发送 到 服务器中去 529 | n = send(sockfd, input, strlen(input), 0); 530 | if (n < 0) { 531 | perror("send"); 532 | } 533 | 534 | n = 0; 535 | count = 0; 536 | 537 | 538 | // 读取 服务器返回的数据 539 | while (1) 540 | { 541 | n = read(sockfd, recvline + count, MAX_LINE); 542 | if (n == MAX_LINE) 543 | { 544 | count += n; 545 | continue; 546 | } 547 | else if (n < 0){ 548 | perror("recv"); 549 | break; 550 | } 551 | else { 552 | count += n; 553 | recvline[count] = '\0'; 554 | printf("[recv] %s\n", recvline); 555 | break; 556 | } 557 | } 558 | } 559 | 560 | return 0; 561 | } 562 | ``` 563 | 564 | 565 | -------------------------------------------------------------------------------- /2、Golang的协程调度器原理及GMP设计思想?.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 2、Golang的协程调度器原理及GMP设计思想? 4 | > 本节为**重点**章节 5 | > 本章节含视频版: 6 | 7 | [![](images/GPM封面.png)](https://www.bilibili.com/video/BV19r4y1w7Nx) 8 | 9 | --- 10 | 11 | ## 一、Golang“调度器”的由来? 12 | 13 | ### (1) 单进程时代不需要调度器 14 | 15 | 我们知道,一切的软件都是跑在操作系统上,真正用来干活(计算)的是CPU。早期的操作系统每个程序就是一个进程,知道一个程序运行完,才能进行下一个进程,就是“单进程时代” 16 | 17 | 一切的程序只能串行发生。 18 | 19 | 20 | ![](images/5-单进程操作系统.png) 21 | 22 | 早期的单进程操作系统,面临2个问题: 23 | 24 | 1.单一的执行流程,计算机只能一个任务一个任务处理。 25 | 26 | 2.进程阻塞所带来的CPU时间浪费。 27 | 28 | 29 | 30 | 那么能不能有多个进程来宏观一起来执行多个任务呢? 31 | 32 | 后来操作系统就具有了**最早的并发能力:多进程并发**,当一个进程阻塞的时候,切换到另外等待执行的进程,这样就能尽量把CPU利用起来,CPU就不浪费了。 33 | 34 | 35 | 36 | ### (2)多进程/线程时代有了调度器需求 37 | 38 | ![](images/6-多进程操作系统.png) 39 | 40 | 在多进程/多线程的操作系统中,就解决了阻塞的问题,因为一个进程阻塞cpu可以立刻切换到其他进程中去执行,而且调度cpu的算法可以保证在运行的进程都可以被分配到cpu的运行时间片。这样从宏观来看,似乎多个进程是在同时被运行。 41 | 42 | 但新的问题就又出现了,进程拥有太多的资源,进程的创建、切换、销毁,都会占用很长的时间,CPU虽然利用起来了,但如果进程过多,CPU有很大的一部分都被用来进行进程调度了。 43 | 44 | **怎么才能提高CPU的利用率呢?** 45 | 46 | 但是对于Linux操作系统来讲,cpu对进程的态度和线程的态度是一样的。 47 | 48 | ![](images/7-cpu切换浪费成本.png) 49 | 50 | 很明显,CPU调度切换的是进程和线程。尽管线程看起来很美好,但实际上多线程开发设计会变得更加复杂,要考虑很多同步竞争等问题,如锁、竞争冲突等。 51 | 52 | 53 | 54 | ### (3)协程来提高CPU利用率 55 | 56 | 多进程、多线程已经提高了系统的并发能力,但是在当今互联网高并发场景下,为每个任务都创建一个线程是不现实的,因为会消耗大量的内存(进程虚拟内存会占用4GB[32位操作系统], 而线程也要大约4MB)。 57 | 58 | 大量的进程/线程出现了新的问题 59 | 60 | * 高内存占用 61 | * 调度的高消耗CPU 62 | 63 | 64 | 65 | 好了,然后工程师们就发现,其实一个线程分为“内核态“线程和”用户态“线程。 66 | 67 | 一个“用户态线程”必须要绑定一个“内核态线程”,但是CPU并不知道有“用户态线程”的存在,它只知道它运行的是一个“内核态线程”(Linux的PCB进程控制块)。 68 | 69 | ![](images/8-线程的内核和用户态.png) 70 | 71 | ​ 72 | 73 | ​ 这样,我们再去细化去分类一下,内核线程依然叫“线程(thread)”,用户线程叫“协程(co-routine)". 74 | 75 | ![](images/9-协程和线程.png) 76 | 77 | ​ 看到这里,我们就要开脑洞了,既然一个协程(co-routine)可以绑定一个线程(thread),那么能不能多个协程(co-routine)绑定一个或者多个线程(thread)上呢。 78 | 79 | ​ 之后,我们就看到了有3中协程和线程的映射关系: 80 | 81 | 82 | 83 | > #### N:1关系 84 | 85 | N个协程绑定1个线程,优点就是**协程在用户态线程即完成切换,不会陷入到内核态,这种切换非常的轻量快速**。但也有很大的缺点,1个进程的所有协程都绑定在1个线程上 86 | 87 | 88 | 89 | 缺点: 90 | 91 | * 某个程序用不了硬件的多核加速能力 92 | * 一旦某协程阻塞,造成线程阻塞,本进程的其他协程都无法执行了,根本就没有并发的能力了。 93 | 94 | ![](images/10-N-1关系.png) 95 | 96 | 97 | 98 | 99 | 100 | > #### 1:1 关系 101 | 102 | 1个协程绑定1个线程,这种最容易实现。协程的调度都由CPU完成了,不存在N:1缺点, 103 | 104 | 缺点: 105 | 106 | * 协程的创建、删除和切换的代价都由CPU完成,有点略显昂贵了。 107 | 108 | ![](images/11-1-1.png) 109 | 110 | 111 | > #### M:N关系 112 | 113 | M个协程绑定1个线程,是N:1和1:1类型的结合,克服了以上2种模型的缺点,但实现起来最为复杂。 114 | 115 | ![](images/12-m-n.png) 116 | 117 | ​ 协程跟线程是有区别的,线程由CPU调度是抢占式的,**协程由用户态调度是协作式的**,一个协程让出CPU后,才执行下一个协程。 118 | 119 | ​ 120 | 121 | ### (4)Go语言的协程goroutine 122 | 123 | **Go为了提供更容易使用的并发方法,使用了goroutine和channel**。goroutine来自协程的概念,让一组可复用的函数运行在一组线程之上,即使有协程阻塞,该线程的其他协程也可以被`runtime`调度,转移到其他可运行的线程上。最关键的是,程序员看不到这些底层的细节,这就降低了编程的难度,提供了更容易的并发。 124 | 125 | Go中,协程被称为goroutine,它非常轻量,一个goroutine只占几KB,并且这几KB就足够goroutine运行完,这就能在有限的内存空间内支持大量goroutine,支持了更多的并发。虽然一个goroutine的栈只占几KB,但实际是可伸缩的,如果需要更多内容,`runtime`会自动为goroutine分配。 126 | 127 | 128 | 129 | Goroutine特点: 130 | 131 | * 占用内存更小(几kb) 132 | * 调度更灵活(runtime调度) 133 | 134 | 135 | 136 | ### (5)被废弃的goroutine调度器 137 | 138 | ​ 好了,既然我们知道了协程和线程的关系,那么最关键的一点就是调度协程的调度器的实现了。 139 | 140 | Go目前使用的调度器是2012年重新设计的,因为之前的调度器性能存在问题,所以使用4年就被废弃了,那么我们先来分析一下被废弃的调度器是如何运作的? 141 | 142 | > 大部分文章都是会用G来表示Goroutine,用M来表示线程,那么我们也会用这种表达的对应关系。 143 | 144 | 145 | ![](images/13-gm.png) 146 | 147 | 148 | 149 | ​ 下面我们来看看被废弃的golang调度器是如何实现的? 150 | 151 | ![](images/14-old调度器.png) 152 | 153 | ​ M想要执行、放回G都必须访问全局G队列,并且M有多个,即多线程访问同一资源需要加锁进行保证互斥/同步,所以全局G队列是有互斥锁进行保护的。 154 | 155 | 156 | 157 | 老调度器有几个缺点: 158 | 159 | 1. 创建、销毁、调度G都需要每个M获取锁,这就形成了**激烈的锁竞争**。 160 | 2. M转移G会造成**延迟和额外的系统负载**。比如当G中包含创建新协程的时候,M创建了G’,为了继续执行G,需要把G’交给M’执行,也造成了**很差的局部性**,因为G’和G是相关的,最好放在M上执行,而不是其他M'。 161 | 3. 系统调用(CPU在M之间的切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销。 162 | 163 | 164 | 165 | ## 二、Goroutine调度器的GMP模型的设计思想 166 | 167 | 面对之前调度器的问题,Go设计了新的调度器。 168 | 169 | 在新调度器中,出列M(thread)和G(goroutine),又引进了P(Processor)。 170 | 171 | ![](images/15-gmp.png) 172 | 173 | **Processor,它包含了运行goroutine的资源**,如果线程想运行goroutine,必须先获取P,P中还包含了可运行的G队列。 174 | 175 | 176 | 177 | ### (1)GMP模型 178 | 179 | 在Go中,**线程是运行goroutine的实体,调度器的功能是把可运行的goroutine分配到工作线程上**。 180 | 181 | ![](images/16-GMP-调度.png) 182 | 183 | 1. **全局队列**(Global Queue):存放等待运行的G。 184 | 2. **P的本地队列**:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建G'时,G'优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列。 185 | 3. **P列表**:所有的P都在程序启动时创建,并保存在数组中,最多有`GOMAXPROCS`(可配置)个。 186 | 4. **M**:线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列**拿**一批G放到P的本地队列,或从其他P的本地队列**偷**一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。 187 | 188 | **Goroutine调度器和OS调度器是通过M结合起来的,每个M都代表了1个内核线程,OS调度器负责把内核线程分配到CPU的核上执行**。 189 | 190 | 191 | 192 | > #### 有关P和M的个数问题 193 | 194 | 1、P的数量: 195 | 196 | - 由启动时环境变量`$GOMAXPROCS`或者是由`runtime`的方法`GOMAXPROCS()`决定。这意味着在程序执行的任意时刻都只有`$GOMAXPROCS`个goroutine在同时运行。 197 | 198 | 2、M的数量: 199 | 200 | - go语言本身的限制:go程序启动时,会设置M的最大数量,默认10000.但是内核很难支持这么多的线程数,所以这个限制可以忽略。 201 | - runtime/debug中的SetMaxThreads函数,设置M的最大数量 202 | - 一个M阻塞了,会创建新的M。 203 | 204 | M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能会创建很多个M出来。 205 | 206 | > #### P和M何时会被创建 207 | 208 | 1、P何时创建:在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P。 209 | 210 | 2、M何时创建:没有足够的M来关联P并运行其中的可运行的G。比如所有的M此时都阻塞住了,而P中还有很多就绪任务,就会去寻找空闲的M,而没有空闲的,就会去创建新的M。 211 | 212 | 213 | 214 | ### (2)调度器的设计策略 215 | 216 | **复用线程**:避免频繁的创建、销毁线程,而是对线程的复用。 217 | 218 | 1)work stealing机制 219 | 220 | ​ 当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程。 221 | 222 | 2)hand off机制 223 | 224 | ​ 当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。 225 | 226 | 227 | 228 | **利用并行**:`GOMAXPROCS`设置P的数量,最多有`GOMAXPROCS`个线程分布在多个CPU上同时运行。`GOMAXPROCS`也限制了并发的程度,比如`GOMAXPROCS = 核数/2`,则最多利用了一半的CPU核进行并行。 229 | 230 | 231 | 232 | 233 | 234 | **抢占**:在coroutine中要等待一个协程主动让出CPU才执行下一个协程,在Go中,一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死,这就是goroutine不同于coroutine的一个地方。 235 | 236 | 237 | 238 | **全局G队列**:在新的调度器中依然有全局G队列,但功能已经被弱化了,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G。 239 | 240 | ### (3) go func() 调度流程 241 | 242 | ![](images/18-go-func调度周期.jpeg) 243 | 244 | 从上图我们可以分析出几个结论: 245 | 246 | ​ 1、我们通过 go func()来创建一个goroutine; 247 | 248 | ​ 2、有两个存储G的队列,一个是局部调度器P的本地队列、一个是全局G队列。新创建的G会先保存在P的本地队列中,如果P的本地队列已经满了就会保存在全局的队列中; 249 | 250 | ​ 3、G只能运行在M中,一个M必须持有一个P,M与P是1:1的关系。M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空,就会想其他的MP组合偷取一个可执行的G来执行; 251 | 252 | ​ 4、一个M调度G执行的过程是一个循环机制; 253 | 254 | ​ 5、当M执行某一个G时候如果发生了syscall或则其余阻塞操作,M会阻塞,如果当前有一些G在执行,runtime会把这个线程M从P中摘除(detach),然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P; 255 | 256 | ​ 6、当M系统调用结束时候,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列。如果获取不到P,那么这个线程M变成休眠状态, 加入到空闲线程中,然后这个G会被放入全局队列中。 257 | 258 | 259 | 260 | ### (4)调度器的生命周期 261 | 262 | ![](images/17-pic-go调度器生命周期.png) 263 | 264 | 特殊的M0和G0 265 | 266 | **M0** 267 | 268 | `M0`是启动程序后的编号为0的主线程,这个M对应的实例会在全局变量runtime.m0中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G, 在之后M0就和其他的M一样了。 269 | 270 | **G0** 271 | 272 | `G0`是每次启动一个M都会第一个创建的gourtine,G0仅用于负责调度的G,G0不指向任何可执行的函数, 每个M都会有一个自己的G0。在调度或系统调用时会使用G0的栈空间, 全局变量的G0是M0的G0。 273 | 274 | 275 | 276 | 我们来跟踪一段代码 277 | 278 | ```go 279 | package main 280 | 281 | import "fmt" 282 | 283 | func main() { 284 | fmt.Println("Hello world") 285 | } 286 | ``` 287 | 288 | 接下来我们来针对上面的代码对调度器里面的结构做一个分析。 289 | 290 | 291 | 292 | 也会经历如上图所示的过程: 293 | 294 | 1. runtime创建最初的线程m0和goroutine g0,并把2者关联。 295 | 2. 调度器初始化:初始化m0、栈、垃圾回收,以及创建和初始化由GOMAXPROCS个P构成的P列表。 296 | 3. 示例代码中的main函数是`main.main`,`runtime`中也有1个main函数——`runtime.main`,代码经过编译后,`runtime.main`会调用`main.main`,程序启动时会为`runtime.main`创建goroutine,称它为main goroutine吧,然后把main goroutine加入到P的本地队列。 297 | 4. 启动m0,m0已经绑定了P,会从P的本地队列获取G,获取到main goroutine。 298 | 5. G拥有栈,M根据G中的栈信息和调度信息设置运行环境 299 | 6. M运行G 300 | 7. G退出,再次回到M获取可运行的G,这样重复下去,直到`main.main`退出,`runtime.main`执行Defer和Panic处理,或调用`runtime.exit`退出程序。 301 | 302 | 调度器的生命周期几乎占满了一个Go程序的一生,`runtime.main`的goroutine执行之前都是为调度器做准备工作,`runtime.main`的goroutine运行,才是调度器的真正开始,直到`runtime.main`结束而结束。 303 | 304 | 305 | 306 | ### (5)可视化GMP编程 307 | 308 | 有2种方式可以查看一个程序的GMP的数据。 309 | 310 | **方式1:go tool trace** 311 | 312 | trace记录了运行时的信息,能提供可视化的Web页面。 313 | 314 | 简单测试代码:main函数创建trace,trace会运行在单独的goroutine中,然后main打印"Hello World"退出。 315 | 316 | > trace.go 317 | 318 | ```go 319 | package main 320 | 321 | import ( 322 | "os" 323 | "fmt" 324 | "runtime/trace" 325 | ) 326 | 327 | func main() { 328 | 329 | //创建trace文件 330 | f, err := os.Create("trace.out") 331 | if err != nil { 332 | panic(err) 333 | } 334 | 335 | defer f.Close() 336 | 337 | //启动trace goroutine 338 | err = trace.Start(f) 339 | if err != nil { 340 | panic(err) 341 | } 342 | defer trace.Stop() 343 | 344 | //main 345 | fmt.Println("Hello World") 346 | } 347 | 348 | ``` 349 | 350 | 运行程序 351 | 352 | ```bash 353 | $ go run trace.go 354 | Hello World 355 | ``` 356 | 357 | 会得到一个`trace.out`文件,然后我们可以用一个工具打开,来分析这个文件。 358 | 359 | ```golang 360 | $ go tool trace trace.out 361 | 2020/02/23 10:44:11 Parsing trace... 362 | 2020/02/23 10:44:11 Splitting trace... 363 | 2020/02/23 10:44:11 Opening browser. Trace viewer is listening on http://127.0.0.1:33479 364 | 365 | ``` 366 | 367 | 我们可以通过浏览器打开`http://127.0.0.1:33479`网址,点击`view trace` 能够看见可视化的调度流程。 368 | 369 | ![](images/19-go-trace1.png) 370 | 371 | ![](images/20-go-trace2.png) 372 | 373 | 374 | 375 | **G信息** 376 | 377 | 点击Goroutines那一行可视化的数据条,我们会看到一些详细的信息。 378 | 379 | ![](images/20-go-trace3.png) 380 | 381 | ​ 一共有两个G在程序中,一个是特殊的G0,是每个M必须有的一个初始化的G,这个我们不必讨论。 382 | 383 | 其中G1应该就是main goroutine(执行main函数的协程),在一段时间内处于可运行和运行的状态。 384 | 385 | 386 | 387 | **M信息** 388 | 389 | 点击Threads那一行可视化的数据条,我们会看到一些详细的信息。 390 | 391 | ![](images/22-go-trace4.png) 392 | 393 | ​ 一共有两个M在程序中,一个是特殊的M0,用于初始化使用,这个我们不必讨论。 394 | 395 | 396 | 397 | **P信息** 398 | 399 | ![](images/23-go-trace5.png) 400 | 401 | G1中调用了`main.main`,创建了`trace goroutine g18`。G1运行在P1上,G18运行在P0上。 402 | 403 | 这里有两个P,我们知道,一个P必须绑定一个M才能调度G。 404 | 405 | 我们在来看看上面的M信息。 406 | 407 | 408 | ![](images/24-go-trace6.png) 409 | 410 | 411 | 412 | 我们会发现,确实G18在P0上被运行的时候,确实在Threads行多了一个M的数据,点击查看如下: 413 | ![](images/25-go-trace7.png) 414 | 415 | 多了一个M2应该就是P0为了执行G18而动态创建的M2. 416 | 417 | **方式2:Debug trace** 418 | 419 | 420 | 421 | ```go 422 | package main 423 | 424 | import ( 425 | "fmt" 426 | "time" 427 | ) 428 | 429 | func main() { 430 | for i := 0; i < 5; i++ { 431 | time.Sleep(time.Second) 432 | fmt.Println("Hello World") 433 | } 434 | } 435 | ``` 436 | 437 | 438 | 439 | 编译 440 | 441 | ```bash 442 | $ go build trace2.go 443 | ``` 444 | 445 | 446 | 447 | 通过Debug方式运行 448 | 449 | ```bash 450 | $ GODEBUG=schedtrace=1000 ./trace2 451 | SCHED 0ms: gomaxprocs=2 idleprocs=0 threads=4 spinningthreads=1 idlethreads=1 runqueue=0 [0 0] 452 | Hello World 453 | SCHED 1003ms: gomaxprocs=2 idleprocs=2 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0 0] 454 | Hello World 455 | SCHED 2014ms: gomaxprocs=2 idleprocs=2 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0 0] 456 | Hello World 457 | SCHED 3015ms: gomaxprocs=2 idleprocs=2 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0 0] 458 | Hello World 459 | SCHED 4023ms: gomaxprocs=2 idleprocs=2 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0 0] 460 | Hello World 461 | 462 | ``` 463 | 464 | 465 | 466 | - `SCHED`:调试信息输出标志字符串,代表本行是goroutine调度器的输出; 467 | - `0ms`:即从程序启动到输出这行日志的时间; 468 | - `gomaxprocs`: P的数量,本例有2个P, 因为默认的P的属性是和cpu核心数量默认一致,当然也可以通过GOMAXPROCS来设置; 469 | - `idleprocs`: 处于idle状态的P的数量;通过gomaxprocs和idleprocs的差值,我们就可知道执行go代码的P的数量; 470 | - t`hreads: os threads/M`的数量,包含scheduler使用的m数量,加上runtime自用的类似sysmon这样的thread的数量; 471 | - `spinningthreads`: 处于自旋状态的os thread数量; 472 | - `idlethread`: 处于idle状态的os thread的数量; 473 | - `runqueue=0`: Scheduler全局队列中G的数量; 474 | - `[0 0]`: 分别为2个P的local queue中的G的数量。 475 | 476 | 477 | 478 | 下一篇,我们来继续详细的分析GMP调度原理的一些场景问题。 479 | 480 | 481 | 482 | ## 三、Go调度器调度场景过程全解析 483 | 484 | 485 | 486 | ### (1)场景1 487 | 488 | P拥有G1,M1获取P后开始运行G1,G1使用`go func()`创建了G2,为了局部性G2优先加入到P1的本地队列。 489 | ![](images/26-gmp场景1.png) 490 | 491 | 492 | 493 | --- 494 | 495 | ### (2)场景2 496 | 497 | G1运行完成后(函数:`goexit`),M上运行的goroutine切换为G0,G0负责调度时协程的切换(函数:`schedule`)。从P的本地队列取G2,从G0切换到G2,并开始运行G2(函数:`execute`)。实现了线程M1的复用。 498 | ![](images/27-gmp场景2.png) 499 | 500 | --- 501 | 502 | ### (3)场景3 503 | 504 | 假设每个P的本地队列只能存3个G。G2要创建了6个G,前3个G(G3, G4, G5)已经加入p1的本地队列,p1本地队列满了。 505 | ![](images/28-gmp场景3.png) 506 | 507 | --- 508 | 509 | ### (4)场景4 510 | 511 | G2在创建G7的时候,发现P1的本地队列已满,需要执行**负载均衡**(把P1中本地队列中前一半的G,还有新创建G**转移**到全局队列) 512 | 513 | > (实现中并不一定是新的G,如果G是G2之后就执行的,会被保存在本地队列,利用某个老的G替换新G加入全局队列) 514 | ![](images/29-gmp场景4.png) 515 | 516 | 这些G被转移到全局队列时,会被打乱顺序。所以G3,G4,G7被转移到全局队列。 517 | 518 | --- 519 | 520 | ### (5)场景5 521 | 522 | G2创建G8时,P1的本地队列未满,所以G8会被加入到P1的本地队列。 523 | 524 | ![](images/30-gmp场景5.png) 525 | 526 | ​ G8加入到P1点本地队列的原因还是因为P1此时在与M1绑定,而G2此时是M1在执行。所以G2创建的新的G会优先放置到自己的M绑定的P上。 527 | 528 | --- 529 | 530 | ### (6)场景6 531 | 532 | 规定:**在创建G时,运行的G会尝试唤醒其他空闲的P和M组合去执行**。 533 | 534 | ![](images/31-gmp场景6.png) 535 | 536 | 假定G2唤醒了M2,M2绑定了P2,并运行G0,但P2本地队列没有G,M2此时为自旋线程**(没有G但为运行状态的线程,不断寻找G)**。 537 | 538 | --- 539 | 540 | 541 | 542 | ### (7)场景7 543 | 544 | M2尝试从全局队列(简称“GQ”)取一批G放到P2的本地队列(函数:`findrunnable()`)。M2从全局队列取的G数量符合下面的公式: 545 | 546 | ```go 547 | n = min(len(GQ)/GOMAXPROCS + 1, len(GQ)/2) 548 | ``` 549 | 550 | 至少从全局队列取1个g,但每次不要从全局队列移动太多的g到p本地队列,给其他p留点。这是**从全局队列到P本地队列的负载均衡**。 551 | 552 | ![](images/32-gmp场景7.001.jpeg) 553 | 554 | 555 | ​ 假定我们场景中一共有4个P(GOMAXPROCS设置为4,那么我们允许最多就能用4个P来供M使用)。所以M2只从能从全局队列取1个G(即G3)移动P2本地队列,然后完成从G0到G3的切换,运行G3。 556 | 557 | --- 558 | 559 | ### (8)场景8 560 | 561 | 假设G2一直在M1上运行,经过2轮后,M2已经把G7、G4从全局队列获取到了P2的本地队列并完成运行,全局队列和P2的本地队列都空了,如场景8图的左半部分。 562 | 563 | ![](images/33-gmp场景8.png) 564 | 565 | ​ **全局队列已经没有G,那m就要执行work stealing(偷取):从其他有G的P哪里偷取一半G过来,放到自己的P本地队列**。P2从P1的本地队列尾部取一半的G,本例中一半则只有1个G8,放到P2的本地队列并执行。 566 | 567 | --- 568 | 569 | ### (9)场景9 570 | 571 | G1本地队列G5、G6已经被其他M偷走并运行完成,当前M1和M2分别在运行G2和G8,M3和M4没有goroutine可以运行,M3和M4处于**自旋状态**,它们不断寻找goroutine。 572 | 573 | ![](images/34-gmp场景9.png) 574 | 575 | ​ 为什么要让m3和m4自旋,自旋本质是在运行,线程在运行却没有执行G,就变成了浪费CPU. 为什么不销毁现场,来节约CPU资源。因为创建和销毁CPU也会浪费时间,我们**希望当有新goroutine创建时,立刻能有M运行它**,如果销毁再新建就增加了时延,降低了效率。当然也考虑了过多的自旋线程是浪费CPU,所以系统中最多有`GOMAXPROCS`个自旋的线程(当前例子中的`GOMAXPROCS`=4,所以一共4个P),多余的没事做线程会让他们休眠。 576 | 577 | --- 578 | 579 | ### (10)场景10 580 | 581 | ​ 假定当前除了M3和M4为自旋线程,还有M5和M6为空闲的线程(没有得到P的绑定,注意我们这里最多就只能够存在4个P,所以P的数量应该永远是M>=P, 大部分都是M在抢占需要运行的P),G8创建了G9,G8进行了**阻塞的系统调用**,M2和P2立即解绑,P2会执行以下判断:如果P2本地队列有G、全局队列有G或有空闲的M,P2都会立马唤醒1个M和它绑定,否则P2则会加入到空闲P列表,等待M来获取可用的p。本场景中,P2本地队列有G9,可以和其他空闲的线程M5绑定。 582 | ![](images/35-gmp场景10.png) 583 | 584 | --- 585 | 586 | ### (11)场景11 587 | 588 | G8创建了G9,假如G8进行了**非阻塞系统调用**。 589 | 590 | ![](images/36-gmp场景11.png) 591 | 592 | 593 | ​ M2和P2会解绑,但M2会记住P2,然后G8和M2进入**系统调用**状态。当G8和M2退出系统调用时,会尝试获取P2,如果无法获取,则获取空闲的P,如果依然没有,G8会被记为可运行状态,并加入到全局队列,M2因为没有P的绑定而变成休眠状态(长时间休眠等待GC回收销毁)。 594 | 595 | --- 596 | 597 | ## 四、小结 598 | 599 | 总结,Go调度器很轻量也很简单,足以撑起goroutine的调度工作,并且让Go具有了原生(强大)并发的能力。**Go调度本质是把大量的goroutine分配到少量线程上去执行,并利用多核并行,实现更强大的并发。** 600 | 601 | 602 | ## 五、思维导图笔记 603 | 604 | 605 | 606 | 607 | -------------------------------------------------------------------------------- /2、分布式从ACID、CAP、BASE的理论推进.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 2、分布式从ACID、CAP、BASE的理论推进 4 | 5 | ​ 分布式实际上就是单一的本地一体解决方案,在硬件或者资源上不够业务需求,而采取的一种分散式多节点,可以扩容资源的一种解决思路。它研究如何把一个需要非常巨大的计算能力才能解决的问题分成许多小的部分,然后把这些部分分配给多个计算机进行处理,最后把这些计算结果综合起来得到最终的结果。 6 | 7 | ​ 那么在了解分布式之前,我们应该从一体式的构造开始说明。 8 | 9 | 10 | 11 | ### 一、从本地事务到分布式理论 12 | 13 | 理解分布式之前,需要理解一个问题就是"事务" 14 | 15 | > 事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。 16 | 17 | 简单地说,事务提供一种“ **要么什么都不做,要么做全套(All or Nothing)**”机制。 18 | 19 | ![](images/126-分布式5.jpeg) 20 | 21 | 22 | 23 | ### 二、ACID理论 24 | 25 | ​ 事务是基于数据进行操作,需要保证事务的数据通常存储在数据库中,所以介绍到事务,就不得不介绍数据库事务的`ACID`特性,指数据库事务正确执行的四个基本特性的缩写。包含: 26 | 27 | * **原子性(Atomicity)** 28 | 29 | * **一致性(Consistency)** 30 | 31 | * **隔离性(Isolation)** 32 | 33 | * **持久性(Durability)** 34 | 35 | 36 | 37 | #### (1) **原子性(Atomicity)** 38 | 39 | ​ 整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。 40 | ![](images/127-分布式6.jpeg) 41 | 42 | 例如:银行转账,从A账户转100元至B账户: 43 | 44 | A、从A账户取100元 45 | 46 | B、存入100元至B账户。 这两步要么一起完成,要么一起不完成,如果只完成第一步,第二步失败,钱会莫名其妙少了100元。 47 | 48 | 49 | 50 | #### (2) **一致性(Consistency)** 51 | 52 | 在事务开始之前和事务结束以后,数据库数据的一致性约束没有被破坏。 53 | 54 | ![](images/128-分布式7.jpeg) 55 | 56 | 例如:现有完整性约束A+B=100,如果一个事务改变了A,那么必须得改变B,使得事务结束后依然满足A+B=100,否则事务失败。 57 | 58 | 59 | 60 | #### (3) **隔离性(Isolation)** 61 | 62 | ​ 数据库允许多个并发事务同时对数据进行读写和修改的能力,如果一个事务要访问的数据正在被另外一个事务修改,只要另外一个事务未提交,它所访问的数据就不受未提交事务的影响。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。 63 | 64 | 例如:现有有个交易是从A账户转100元至B账户,在这个交易事务还未完成的情况下,如果此时B查询自己的账户,是看不到新增加的100元的。 65 | 66 | 67 | 68 | #### (4) **持久性(Durability)** 69 | 70 | ​ 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。 71 | 72 | 73 | 74 | ​ 本地事务ACID实际上可用”统一提交,失败回滚“几个字总结,严格保证了同一事务内数据的一致性! 75 | 76 | 而分布式事务不能实现这种`ACID`。因为有CAP理论约束。接下来我们来了解一下,分布式中是如何保证以上特性的,那么就有了一个著名的CAP理论。 77 | 78 | --- 79 | 80 | 81 | 82 | ### 三、CAP理论 83 | 84 | ​ 在设计一个大规模可扩放的网络服务时候会遇到三个特性:一致性(consistency)、可用性(Availability)、分区容错(partition-tolerance)都需要的情景. 85 | 86 | ​ CAP定律说的是在一个分布式计算机系统中,一致性,可用性和分区容错性这三种保证无法同时得到满足,最多满足两个。 87 | 88 | ![](images/129-CAP1.jpeg) 89 | 90 | ​ 如上图,CAP的三种特性只能同时满足两个。而且在不同的两两组合,也有一些成熟的分布式产品。 91 | 92 | 接下来,我们来介绍一下CAP的三种特性,我们采用一个应用场景来分析CAP中的每个特点的含义。 93 | 94 | ![](images/130-CAP2.jpeg) 95 | 96 | 该场景整体分为5个流程: 97 | 98 | 流程一、客户端发送请求(如:添加订单、修改订单、删除订单) 99 | 100 | 流程二、Web业务层处理业务,并修改存储成数据信息 101 | 102 | 流程三、存储层内部Master与Backup的数据同步 103 | 104 | 流程四、Web业务层从存储层取出数据 105 | 106 | 流程五、Web业务层返回数据给客户端 107 | 108 | 109 | 110 | #### (1) 一致性Consistency 111 | 112 | > “`all nodes see the same data at the same time`” 113 | 114 | 一旦数据更新完成并成功返回客户端后,那么分布式系统中所有节点在同一时间的数据完全一致。 115 | 116 | > 在CAP的一致性中还包括强一致性、弱一致性、最终一致性等级别,稍后我们在后续章节介绍。 117 | 118 | 一致性是指写操作后的读操作可以读取到最新的数据状态,当数据分布在多个节点上,从任意结点读取到的数据都是最新的状态。 119 | 120 | 121 | 122 | ##### 一致性实现目标: 123 | 124 | * Web业务层向主Master写数据库成功,从Backup读数据也成功。 125 | 126 | ![](images/131-CAP3.jpeg) 127 | 128 | 129 | * Web业务层向主Master读数据库失败,从Backup读数据也失败。 130 | 131 | ![](images/132-CAP4.jpeg) 132 | 133 | 134 | ##### 必要实现流程: 135 | 136 | ![](images/133-CAP5.jpeg) 137 | 138 | 写入主数据库后,在向从数据库同步期间要将从数据库锁定,待同步完成后再释放锁,以免在新数据写入成功后,向从数据库查询到旧的数据。 139 | 140 | 141 | 142 | ##### 分布式一致性特点: 143 | 144 | 1. 由于存在数据同步的过程,写操作的响应会有一定的延迟。 145 | 2. 为了保证数据一致性会对资源暂时锁定,待数据同步完成释放锁定资源。 146 | 3. 如果请求数据同步失败的结点则会返回错误信息,一定不会返回旧数据。 147 | 148 | 149 | 150 | #### (2) 可用性(Availability) 151 | 152 | > “`Reads and writes always succeed`” 153 | 154 | 服务一直可用,而且是正常响应时间。 155 | 156 | 157 | 158 | 对于可用性的衡量标准如下: 159 | 160 | | 可用性分类 | 可用水平(%) | 一年中可容忍停机时间 | 161 | | :--------------------------- | :------------ | :------------------- | 162 | | 容错可用性 | 99.9999 | <1 min | 163 | | 极高可用性 | 99.999 | <5 min | 164 | | 具有故障自动恢复能力的可用性 | 99.99 | <53 min | 165 | | 高可用性 | 99.9 | <8.8h | 166 | | 商品可用性 | 99 | <43.8 min | 167 | 168 | 169 | 170 | ##### 可用性实现目标: 171 | 172 | * 当Master正在被更新,Backup数据库接收到数据查询的请求则立即能够响应数据查询结果。 173 | 174 | * backup数据库不允许出现响应超时或响应错误。 175 | 176 | ![](images/134-CAP6.jpeg) 177 | 178 | 179 | 180 | ##### 必要实现流程: 181 | 182 | ![](images/135-CAP7.jpeg) 183 | 184 | 1. 写入Master主数据库后要将数据同步到从数据库。 185 | 2. 由于要保证Backup从数据库的可用性,不可将Backup从数据库中的资源进行锁定。 186 | 3. 即时数据还没有同步过来,从数据库也要返回要查询的数据,哪怕是旧数据/或者默认数据,但不能返回错误或响应超时。 187 | 188 | 189 | 190 | ##### 分布式可用性特点: 191 | 192 | 所有请求都有响应,且不会出现响应超时或响应错误。 193 | 194 | 195 | 196 | 197 | 198 | #### (3) 分区容错性(Partition tolerance) 199 | 200 | > “`the system continues to operate despite arbitrary message loss or failure of part of the system`” 201 | 202 | 分布式系统中,尽管部分节点出现任何消息丢失或者故障,系统应继续运行。 203 | 204 | 通常分布式系统的各各结点部署在不同的子网,这就是网络分区,不可避免的会出现由于网络问题而导致结点之间通信失败,此时仍可对外提供服务。 205 | 206 | 207 | 208 | ##### 分区容错性实现目标: 209 | 210 | * 主数据库向从数据库同步数据失败不影响读写操作。 211 | 212 | ![](images/136-CAP8.jpeg) 213 | 214 | * 其一个结点挂掉不影响另一个结点对外提供服务。 215 | 216 | ![](images/137-CAP9.jpeg) 217 | 218 | 219 | ##### 必要实现流程: 220 | 221 | 1. 尽量使用异步取代同步操作,例如使用异步方式将数据从主数据库同步到从数据,这样结点之间能有效的实现松耦合。 222 | 2. 添加Backup从数据库结点,其中一个Backup从结点挂掉其它Backup从结点提供服务。 223 | 224 | ![](images/138-CAP10.jpeg) 225 | 226 | 227 | 228 | ##### 分区容错性特点: 229 | 230 | 分区容忍性分是布式系统具备的基本能力。 231 | 232 | 233 | 234 | ### 四、CAP的”3选2“证明 235 | 236 | 237 | 238 | #### (1) 基本场景 239 | 240 | 在小结中,我们主要介绍CAP的理论为什么不能够3个特性同时满足。 241 | 242 | ![](images/146-Cap0.jpeg) 243 | 244 | 如上图,是我们证明CAP的基本场景,分布式网络中有两个节点Host1和Host2,他们之间网络可以连通,Host1中运行Process1程序和对应的数据库Data,Host2中运行Process2程序和对应数据库Data。 245 | 246 | 247 | 248 | #### (2) CAP特性 249 | 250 | `如果满足一致性(C)`:那么`Data(0) = Data(0)`. 251 | 252 | `如果满足可用性(A)`: 用户不管请求Host1或Host2,都会立刻响应结果。 253 | 254 | `如果满足分区容错性(P)`: Host1或Host2有一方脱离系统(故障), 都不会影响Host1和Host2彼此之间正常运作。 255 | 256 | 257 | 258 | #### (3) 分布式系统正常运行流程 259 | 260 | ![](images/139-CAP11.jpeg) 261 | 262 | 如上图,是分布式系统正常运转的流程。 263 | 264 | A、用户向`Host1`主机请求数据更新,程序`Process1`更新数据库`Data(0)`为`Data(1)` 265 | 266 | B、分布式系统将数据进行同步操作,将`Host1`中的`Data(1)`同步的`Host2`中``Data(0)`,使`Host2`中的数据也变为`Data(1)` 267 | 268 | C、当用户请求主机`Host2`时,则`Process2`则响应最新的`Data(1)`数据 269 | 270 | 271 | 272 | 根据CAP的特性: 273 | 274 | * `Host1`和`Host2`的数据库`Data`之间的数据是否一样为一致性(C) 275 | 276 | * 用户对`Host1`和`Host2`的请求响应为可用性(A) 277 | 278 | * `Host1`和`Host2`之间的各自网络环境为分区容错性(P) 279 | 280 | 281 | 282 | 当前是一个正常运作的流程,目前CAP三个特性可以同时满足,也是一个`理想状态`,但是实际应用场景中,发生错误在所难免,那么如果发生错误CAP是否能同时满足,或者该如何取舍? 283 | 284 | --- 285 | 286 | 287 | 288 | #### (4) 分布式系统异常运行流程 289 | 290 | 假设`Host1`和`Host2`之间的网络断开了,我们要支持这种网络异常,相当于要满足`分区容错性(P)`,能不能同时满足`一致性(C)`和`可用响应性(A)`呢? 291 | 292 | ![](images/140-CAP12.jpeg) 293 | 294 | 295 | 假设在N1和N2之间网络断开的时候, 296 | 297 | A、用户向`Host1`发送数据更新请求,那`Host1`中的数据`Data(0)`将被更新为`Data(1)` 298 | 299 | B、弱此时`Host1`和`Host2`网络是断开的,所以分布式系统同步操作将失败,`Host2`中的数据依旧是`Data(0)` 300 | 301 | C、有用户向`Host2`发送数据读取请求,由于数据还没有进行同步,`Process2`没办法立即给用户返回最新的数据V1,那么将面临两个选择。 302 | 303 | 304 | 305 | 第一,牺牲`数据一致性(c)`,响应旧的数据`Data(0)`给用户; 306 | 307 | 第二,牺牲`可用性(A)`,阻塞等待,直到网络连接恢复,数据同步完成之后,再给用户响应最新的数据`Data(1)`。 308 | 309 | 这个过程,证明了要满足`分区容错性(p)`的分布式系统,只能在`一致性(C)`和`可用性(A)`两者中,选择其中一个。 310 | 311 | 312 | 313 | #### (5) "3选2"的必然性 314 | 315 | 通过CAP理论,我们知道无法同时满足`一致性`、`可用性`和`分区容错性`这三个特性,那要舍弃哪个呢? 316 | 317 | 318 | 319 | ##### CA 放弃 P: 320 | 321 | 一个分布式系统中,不可能存在不满足P,放弃`分区容错性(p)`,即不进行分区,不考虑由于网络不通或结点挂掉的问题,则可以实现一致性和可用性。那么系统将不是一个标准的分布式系统。我们最常用的关系型数据就满足了CA,如下: 322 | 323 | ![](images/141-CAP13.jpeg) 324 | 325 | 主数据库和从数据库中间不再进行数据同步,数据库可以响应每次的查询请求,通过事务(原子性操作)隔离级别实现每个查询请求都可以返回最新的数据。 326 | 327 | 328 | 329 | 注意: 330 | 331 | > 对于一个分布式系统来说。P是一个基本要求,CAP三者中,只能在CA两者之间做权衡,并且要想尽办法提升P。 332 | 333 | 334 | 335 | ##### CP 放弃 A 336 | 337 | 如果一个分布式系统不要求强的可用性,即容许系统停机或者长时间无响应的话,就可以在CAP三者中保障CP而舍弃A。 338 | 339 | 放弃可用性,追求一致性和分区容错性,如Redis、HBase等,还有分布式系统中常用的Zookeeper也是在CAP三者之中选择优先保证CP的。 340 | 341 | 场景: 342 | 343 | 跨行转账,一次转账请求要等待双方银行系统都完成整个事务才算完成。 344 | 345 | 346 | 347 | ##### AP 放弃 C 348 | 349 | 放弃一致性,追求分区容忍性和可用性。这是很多分布式系统设计时的选择。实现AP,前提是只要用户可以接受所查询的到数据在一定时间内不是最新的即可。 350 | 351 | 352 | 353 | 通常实现AP都会保证最终一致性,后面讲的BASE理论就是根据AP来扩展的。 354 | 355 | 场景1: 356 | 357 | 淘宝订单退款。今日退款成功,明日账户到账,只要用户可以接受在一定时间内到账即可。 358 | 359 | 场景2: 360 | 361 | 12306的买票。都是在可用性和一致性之间舍弃了一致性而选择可用性。 362 | 363 | 你在12306买票的时候肯定遇到过这种场景,当你购买的时候提示你是有票的(但是可能实际已经没票了),你也正常的去输入验证码,下单了。但是过了一会系统提示你下单失败,余票不足。这其实就是先在可用性方面保证系统可以正常的服务,然后在数据的一致性方面做了些牺牲,会影响一些用户体验,但是也不至于造成用户流程的严重阻塞。 364 | 365 | 但是,我们说很多网站牺牲了一致性,选择了可用性,这其实也不准确的。就比如上面的买票的例子,其实舍弃的只是强一致性。退而求其次保证了最终一致性。也就是说,虽然下单的瞬间,关于车票的库存可能存在数据不一致的情况,但是过了一段时间,还是要保证最终一致性的。 366 | 367 | 368 | 369 | #### (6) 总结: 370 | 371 | > CA 放弃 P:如果不要求P(不允许分区),则C(强一致性)和A(可用性)是可以保证的。这样分区将永远不会存在,因此CA的系统更多的是允许分区后各子系统依然保持CA。 372 | 373 | > CP 放弃 A:如果不要求A(可用),相当于每个请求都需要在Server之间强一致,而P(分区)会导致同步时间无限延长,如此CP也是可以保证的。很多传统的数据库分布式事务都属于这种模式。 374 | 375 | > AP 放弃 C:要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。现在众多的NoSQL都属于此类。 376 | 377 | 378 | 379 | ### 五、思考 380 | 381 | #### 思考:按照CAP理论如何设计一个电商系统? 382 | 383 | - 首先个电商网站核心模块有**用户,订单,商品,支付,促销管理**等 384 | 385 | > 1、对于用户模块,包括登录,个人设置,个人订单,购物车,收藏夹等,这些模块保证AP,数据短时间不一致不影响使用。 386 | > 2、订单模块的下单付款扣减库存操作是整个系统的核心,CA都需要保证,极端情况下面牺牲A保证C 387 | > 3、商品模块的商品上下架和库存管理保证CP 388 | > 4、搜索功能因为本身就不是实时性非常高的模块,所以保证AP就可以了。 389 | > 5、促销是短时间的数据不一致,结果就是优惠信息看不到,但是已有的优惠要保证可用,而且优惠可以提前预计算,所以可以保证AP。 390 | > 6、支付这一块是独立的系统,或者使用第三方的支付宝,微信。其实CAP是由第三方来保证的,支付系统是一个对CAP要求极高的系统,C是必须要保证的,AP中A相对更重要,不能因为分区,导致所有人都不能支付 391 | 392 | 393 | 394 | 395 | 396 | ### 六、分布式BASE理论 397 | 398 | ​ CAP 不可能同时满足,而`分区容错性(P)`是对于分布式系统而言是必须的。如果系统能够同时实现 CAP 是再好不过的了,所以出现了 BASE 理论。 399 | 400 | #### (1) BASE理论 401 | 402 | > 通用定义 403 | 404 | BASE是**Basically Available(基本可用)**、**Soft state(软状态)**和**Eventually consistent(最终一致性)**三个短语的简写。 405 | 406 | BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是**基于CAP定理逐步演化**而来的,其核心思想是即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方法来使系统达到**最终一致性**。 407 | 408 | 409 | 410 | > 两个对冲理念:ACID和BASE 411 | 412 | `ACID`是传统数据库常用的设计理念,`追求强一致性`模型。 413 | 414 | `BASE`支持的是大型分布式系统,提出通过`牺牲强一致性`获得`高可用性`。 415 | 416 | 417 | 418 | #### (2) Basically Available(基本可用) 419 | 420 | 实际上就是两个妥协。 421 | 422 | 423 | 424 | * 对响应上时间的妥协:正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了1~2秒。 425 | 426 | * 对功能损失的妥协:正常情况下,在一个电子商务网站(比如淘宝)上购物,消费者几乎能够顺利地完成每一笔订单。但在一些节日大促购物高峰的时候(比如双十一、双十二),由于消费者的购物行为激增,为了保护系统的稳定性(或者保证一致性),部分消费者可能会被引导到一个降级页面,如下: 427 | 428 | ![](./pic/142-Base1.png) 429 | 430 | 431 | 432 | #### (3) Soft state(软状态) 433 | 434 | - 原子性(硬状态) -> 要求多个节点的数据副本都是一致的,这是一种"硬状态" 435 | 436 | ![](images/143-Base2.jpeg) 437 | 438 | - 软状态(弱状态) -> 允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延迟。 439 | ![](images/144-Base2.jpeg) 440 | 441 | #### (4) Eventually consistent(最终一致性) 442 | 443 | 上面说软状态,然后不可能一直是软状态,必须有个时间期限。在期限过后,应当保证所有副本保持数据一致性。从而达到数据的最终一致性。这个时间期限取决于网络延时,系统负载,数据复制方案设计等等因素。 444 | ![](images/145-Base3.jpeg) 445 | 446 | 稍微官方一点的说法就是: 447 | 448 | > 系统能够保证在没有其他新的更新操作的情况下,数据最终一定能够达到一致的状态,因此所有客户端对系统的数据访问最终都能够获取到最新的值。 449 | 450 | 451 | 452 | #### (5) BASE总结 453 | 454 | 总的来说,BASE 理论面向的是大型高可用可扩展的分布式系统,和传统事务的 ACID 是**相反的**,它完全不同于 ACID 的强一致性模型,而是**通过牺牲强一致性**来获得可用性,并允许数据在一段时间是不一致的。 455 | 456 | 457 | 458 | 参考: 459 | 460 | https://blog.csdn.net/weixin_44062339/article/details/99710968 461 | 462 | https://blog.csdn.net/w372426096/article/details/80437198 463 | 464 | https://www.solves.com.cn/it/cxkf/bk/2019-09-24/5229.html 465 | 466 | https://www.jianshu.com/p/46b90dfc7c90 467 | 468 | https://www.jianshu.com/p/9cb2a6fa4e0e 469 | 470 | https://www.jianshu.com/p/68c7c16b3fbd 471 | 472 | -------------------------------------------------------------------------------- /2、数组和切片.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 二、数组和切片 4 | 5 | ### (1) 切片的初始化与追加 6 | 7 | > 1.2 写出程序运行的结果 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | ) 15 | 16 | func main(){ 17 | s := make([]int, 10) 18 | 19 | s = append(s, 1, 2, 3) 20 | 21 | fmt.Println(s) 22 | } 23 | 24 | ``` 25 | 26 | **考点** 27 | 28 | 切片追加, make初始化均为0 29 | 30 | **结果** 31 | 32 | ```bash 33 | [0 0 0 0 0 0 0 0 0 0 1 2 3] 34 | ``` 35 | 36 | 37 | 38 | ### (2) slice拼接问题 39 | 40 | > 下面是否可以编译通过? 41 | 42 | > test6.go 43 | 44 | ```go 45 | package main 46 | 47 | import "fmt" 48 | 49 | func main() { 50 | s1 := []int{1, 2, 3} 51 | s2 := []int{4, 5} 52 | s1 = append(s1, s2) 53 | fmt.Println(s1) 54 | } 55 | ``` 56 | 57 | **结果** 58 | 59 | 编译失败 60 | 61 | 两个slice在append的时候,记住需要进行将第二个slice进行`...`打散再拼接。 62 | 63 | ```go 64 | s1 = append(s1, s2...) 65 | ``` 66 | 67 | 68 | 69 | ### (3) slice中new的使用 70 | 71 | > 下面代码是否可以编译通过? 72 | 73 | >test9.go 74 | 75 | ```go 76 | package main 77 | 78 | import "fmt" 79 | 80 | func main() { 81 | 82 | list := new([]int) 83 | 84 | list = append(list, 1) 85 | 86 | fmt.Println(list) 87 | } 88 | ``` 89 | 90 | 91 | 92 | **结果**: 93 | 94 | 编译失败,`./test9.go:9:15: first argument to append must be slice; have *[]int` 95 | 96 | **分析**: 97 | 98 | >切片指针的解引用。 99 | 100 | > 可以使用list:=make([]int,0) list类型为切片 101 | 102 | > 或使用*list = append(\*list, 1) list类型为指针 103 | 104 | **new和make的区别:** 105 | 106 | ​ 二者都是内存的分配(堆上),但是make只用于slice、map以及channel的初始化(非零值);而new用于类型的内存分配,并且内存置为零。所以在我们编写程序的时候,就可以根据自己的需要很好的选择了。 107 | 108 | ​ make返回的还是这三个引用类型本身;而new返回的是指向类型的指针。 109 | 110 | 111 | -------------------------------------------------------------------------------- /3、Golang中逃逸现象,变量“何时栈何时堆”.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 3、Golang中逃逸现象, 变量“何时栈?何时堆?” 4 | 5 | ### 一、C/C++报错?Golang通过? 6 | 7 | 我们先看一段代码 8 | 9 | ```go 10 | package main 11 | 12 | func foo(arg_val int)(*int) { 13 | 14 | var foo_val int = 11; 15 | return &foo_val; 16 | } 17 | 18 | func main() { 19 | 20 | main_val := foo(666) 21 | 22 | println(*main_val) 23 | } 24 | ``` 25 | 26 | 编译运行 27 | 28 | ```bash 29 | $ go run pro_1.go 30 | 11 31 | ``` 32 | 33 | 竟然没有报错! 34 | 35 | 36 | 37 | 了解C/C++的小伙伴应该知道,这种情况是一定不允许的,因为 外部函数使用了子函数的局部变量, 理论来说,子函数的`foo_val` 的声明周期早就销毁了才对,如下面的C/C++代码 38 | 39 | ```c 40 | #include 41 | 42 | int *foo(int arg_val) { 43 | 44 | int foo_val = 11; 45 | 46 | return &foo_val; 47 | } 48 | 49 | int main() 50 | { 51 | int *main_val = foo(666); 52 | 53 | printf("%d\n", *main_val); 54 | } 55 | 56 | ``` 57 | 58 | 编译 59 | 60 | ```bash 61 | $ gcc pro_1.c 62 | pro_1.c: In function ‘foo’: 63 | pro_1.c:7:12: warning: function returns address of local variable [-Wreturn-local-addr] 64 | return &foo_val; 65 | ^~~~~~~~ 66 | 67 | ``` 68 | 69 | 出了一个警告,不管他,再运行 70 | 71 | ``` 72 | $ ./a.out 73 | 段错误 (核心已转储) 74 | ``` 75 | 76 | 程序崩溃. 77 | 78 | 79 | 80 | 如上C/C++编译器明确给出了警告,foo把一个局部变量的地址返回了;反而高大上的go没有给出任何警告,难道是go编译器识别不出这个问题吗? 81 | 82 | 83 | 84 | ### 二、Golang编译器得逃逸分析 85 | 86 | ​ go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做**逃逸分析(escape analysis)**,**当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆**。 87 | go语言声称这样可以释放程序员关于内存的使用限制,更多的让程序员关注于程序功能逻辑本身。 88 | 89 | 90 | 91 | 我们再看如下代码: 92 | 93 | ```go 94 | package main 95 | 96 | func foo(arg_val int) (*int) { 97 | 98 | var foo_val1 int = 11; 99 | var foo_val2 int = 12; 100 | var foo_val3 int = 13; 101 | var foo_val4 int = 14; 102 | var foo_val5 int = 15; 103 | 104 | 105 | //此处循环是防止go编译器将foo优化成inline(内联函数) 106 | //如果是内联函数,main调用foo将是原地展开,所以foo_val1-5相当于main作用域的变量 107 | //即使foo_val3发生逃逸,地址与其他也是连续的 108 | for i := 0; i < 5; i++ { 109 | println(&arg_val, &foo_val1, &foo_val2, &foo_val3, &foo_val4, &foo_val5) 110 | } 111 | 112 | //返回foo_val3给main函数 113 | return &foo_val3; 114 | } 115 | 116 | 117 | func main() { 118 | main_val := foo(666) 119 | 120 | println(*main_val, main_val) 121 | } 122 | 123 | ``` 124 | 125 | 我们运行一下 126 | 127 | ```bash 128 | $ go run pro_2.go 129 | 0xc000030758 0xc000030738 0xc000030730 0xc000082000 0xc000030728 0xc000030720 130 | 0xc000030758 0xc000030738 0xc000030730 0xc000082000 0xc000030728 0xc000030720 131 | 0xc000030758 0xc000030738 0xc000030730 0xc000082000 0xc000030728 0xc000030720 132 | 0xc000030758 0xc000030738 0xc000030730 0xc000082000 0xc000030728 0xc000030720 133 | 0xc000030758 0xc000030738 0xc000030730 0xc000082000 0xc000030728 0xc000030720 134 | 13 0xc000082000 135 | 136 | ``` 137 | 138 | 我们能看到`foo_val3`是返回给main的局部变量, 其中他的地址应该是`0xc000082000`,很明显与其他的foo_val1、2、3、4不是连续的. 139 | 140 | 我们用`go tool compile`测试一下 141 | 142 | ```bash 143 | $ go tool compile -m pro_2.go 144 | pro_2.go:24:6: can inline main 145 | pro_2.go:7:9: moved to heap: foo_val3 146 | ``` 147 | 148 | 果然,在编译的时候, `foo_val3`具有被编译器判定为逃逸变量, 将`foo_val3`放在堆中开辟. 149 | 150 | 151 | 152 | 我们在用汇编证实一下: 153 | 154 | ```bash 155 | $ go tool compile -S pro_2.go > pro_2.S 156 | ``` 157 | 158 | 打开pro_2.S文件, 搜索`runtime.newobject`关键字 159 | 160 | ```go 161 | ... 162 | 16 0x0021 00033 (pro_2.go:5) PCDATA $0, $0 163 | 17 0x0021 00033 (pro_2.go:5) PCDATA $1, $0 164 | 18 0x0021 00033 (pro_2.go:5) MOVQ $11, "".foo_val1+48(SP) 165 | 19 0x002a 00042 (pro_2.go:6) MOVQ $12, "".foo_val2+40(SP) 166 | 20 0x0033 00051 (pro_2.go:7) PCDATA $0, $1 167 | 21 0x0033 00051 (pro_2.go:7) LEAQ type.int(SB), AX 168 | 22 0x003a 00058 (pro_2.go:7) PCDATA $0, $0 169 | 23 0x003a 00058 (pro_2.go:7) MOVQ AX, (SP) 170 | 24 0x003e 00062 (pro_2.go:7) CALL runtime.newobject(SB) //foo_val3是被new出来的 171 | 25 0x0043 00067 (pro_2.go:7) PCDATA $0, $1 172 | 26 0x0043 00067 (pro_2.go:7) MOVQ 8(SP), AX 173 | 27 0x0048 00072 (pro_2.go:7) PCDATA $1, $1 174 | 28 0x0048 00072 (pro_2.go:7) MOVQ AX, "".&foo_val3+56(SP) 175 | 29 0x004d 00077 (pro_2.go:7) MOVQ $13, (AX) 176 | 30 0x0054 00084 (pro_2.go:8) MOVQ $14, "".foo_val4+32(SP) 177 | 31 0x005d 00093 (pro_2.go:9) MOVQ $15, "".foo_val5+24(SP) 178 | 32 0x0066 00102 (pro_2.go:9) XORL CX, CX 179 | 33 0x0068 00104 (pro_2.go:15) JMP 252 180 | ... 181 | ``` 182 | 183 | 看出来, foo_val3是被runtime.newobject()在堆空间开辟的, 而不是像其他几个是基于地址偏移的开辟的栈空间. 184 | 185 | ### 三、new的变量在栈还是堆? 186 | 187 | 那么对于new出来的变量,是一定在heap中开辟的吗,我们来看看 188 | 189 | ```go 190 | package main 191 | 192 | func foo(arg_val int) (*int) { 193 | 194 | var foo_val1 * int = new(int); 195 | var foo_val2 * int = new(int); 196 | var foo_val3 * int = new(int); 197 | var foo_val4 * int = new(int); 198 | var foo_val5 * int = new(int); 199 | 200 | 201 | //此处循环是防止go编译器将foo优化成inline(内联函数) 202 | //如果是内联函数,main调用foo将是原地展开,所以foo_val1-5相当于main作用域的变量 203 | //即使foo_val3发生逃逸,地址与其他也是连续的 204 | for i := 0; i < 5; i++ { 205 | println(arg_val, foo_val1, foo_val2, foo_val3, foo_val4, foo_val5) 206 | } 207 | 208 | //返回foo_val3给main函数 209 | return foo_val3; 210 | } 211 | 212 | 213 | func main() { 214 | main_val := foo(666) 215 | 216 | println(*main_val, main_val) 217 | } 218 | 219 | ``` 220 | 221 | 我们将foo_val1-5全部用new的方式来开辟, 编译运行看结果 222 | 223 | ```bash 224 | $ go run pro_3.go 225 | 666 0xc000030728 0xc000030720 0xc00001a0e0 0xc000030738 0xc000030730 226 | 666 0xc000030728 0xc000030720 0xc00001a0e0 0xc000030738 0xc000030730 227 | 666 0xc000030728 0xc000030720 0xc00001a0e0 0xc000030738 0xc000030730 228 | 666 0xc000030728 0xc000030720 0xc00001a0e0 0xc000030738 0xc000030730 229 | 666 0xc000030728 0xc000030720 0xc00001a0e0 0xc000030738 0xc000030730 230 | 0 0xc00001a0e0 231 | ``` 232 | 233 | 很明显, `foo_val3`的地址`0xc00001a0e0 `依然与其他的不是连续的. 依然具备逃逸行为. 234 | 235 | ### 四、结论 236 | 237 | Golang中一个函数内局部变量,不管是不是动态new出来的,它会被分配在堆还是栈,是由编译器做逃逸分析之后做出的决定。 238 | 239 | 240 | 241 | 按理来说, 人家go的设计者明明就不希望开发者管这些,但是面试官就偏偏找这种问题问? 醉了也是. -------------------------------------------------------------------------------- /3、Map.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 三、Map 4 | 5 | ### (1) Map的Value赋值 6 | 7 | > 下面代码编译会出现什么结果? 8 | 9 | > test7.go 10 | 11 | ```go 12 | package main 13 | 14 | import "fmt" 15 | 16 | type Student struct { 17 | Name string 18 | } 19 | 20 | var list map[string]Student 21 | 22 | func main() { 23 | 24 | list = make(map[string]Student) 25 | 26 | student := Student{"Aceld"} 27 | 28 | list["student"] = student 29 | list["student"].Name = "LDB" 30 | 31 | fmt.Println(list["student"]) 32 | } 33 | ``` 34 | 35 | **结果** 36 | 37 | 编译失败,`./test7.go:18:23: cannot assign to struct field list["student"].Name in map` 38 | 39 | **分析** 40 | 41 | `map[string]Student` 的value是一个Student结构值,所以当`list["student"] = student`,是一个值拷贝过程。而`list["student"]`则是一个值引用。那么值引用的特点是`只读`。所以对`list["student"].Name = "LDB"`的修改是不允许的。 42 | 43 | **方法一:** 44 | 45 | ```go 46 | package main 47 | 48 | import "fmt" 49 | 50 | type Student struct { 51 | Name string 52 | } 53 | 54 | var list map[string]Student 55 | 56 | func main() { 57 | 58 | list = make(map[string]Student) 59 | 60 | student := Student{"Aceld"} 61 | 62 | list["student"] = student 63 | //list["student"].Name = "LDB" 64 | 65 | /* 66 | 方法1: 67 | */ 68 | tmpStudent := list["student"] 69 | tmpStudent.Name = "LDB" 70 | list["student"] = tmpStudent 71 | 72 | fmt.Println(list["student"]) 73 | } 74 | 75 | ``` 76 | 77 | 其中 78 | 79 | ```go 80 | /* 81 | 方法1: 82 | */ 83 | tmpStudent := list["student"] 84 | tmpStudent.Name = "LDB" 85 | list["student"] = tmpStudent 86 | ``` 87 | 88 | 是先做一次值拷贝,做出一个`tmpStudent副本`,然后修改该副本,然后再次发生一次值拷贝复制回去,`list["student"] = tmpStudent`,但是这种会在整体过程中发生2次结构体值拷贝,性能很差。 89 | 90 | 91 | 92 | **方法二**: 93 | 94 | ```go 95 | package main 96 | 97 | import "fmt" 98 | 99 | type Student struct { 100 | Name string 101 | } 102 | 103 | var list map[string]*Student 104 | 105 | func main() { 106 | 107 | list = make(map[string]*Student) 108 | 109 | student := Student{"Aceld"} 110 | 111 | list["student"] = &student 112 | list["student"].Name = "LDB" 113 | 114 | fmt.Println(list["student"]) 115 | } 116 | 117 | ``` 118 | 119 | 我们将map的类型的value由Student值,改成Student指针。 120 | 121 | ```go 122 | var list map[string]*Student 123 | ``` 124 | 125 | 这样,我们实际上每次修改的都是指针所指向的Student空间,指针本身是常指针,不能修改,`只读`属性,但是指向的Student是可以随便修改的,而且这里并不需要值拷贝。只是一个指针的赋值。 126 | 127 | 128 | 129 | ### (2) map的遍历赋值 130 | 131 | --- 132 | 133 | > 以下代码有什么问题,说明原因 134 | 135 | > test8.go 136 | 137 | ```go 138 | package main 139 | 140 | import ( 141 | "fmt" 142 | ) 143 | 144 | type student struct { 145 | Name string 146 | Age int 147 | } 148 | 149 | func main() { 150 | //定义map 151 | m := make(map[string]*student) 152 | 153 | //定义student数组 154 | stus := []student{ 155 | {Name: "zhou", Age: 24}, 156 | {Name: "li", Age: 23}, 157 | {Name: "wang", Age: 22}, 158 | } 159 | 160 | //将数组依次添加到map中 161 | for _, stu := range stus { 162 | m[stu.Name] = &stu 163 | } 164 | 165 | //打印map 166 | for k,v := range m { 167 | fmt.Println(k ,"=>", v.Name) 168 | } 169 | } 170 | ``` 171 | 172 | **结果** 173 | 174 | 遍历结果出现错误,输出结果为 175 | 176 | ```bash 177 | zhou => wang 178 | li => wang 179 | wang => wang 180 | ``` 181 | 182 | map中的3个key均指向数组中最后一个结构体。 183 | 184 | **分析** 185 | 186 | foreach中,stu是结构体的一个拷贝副本,所以`m[stu.Name]=&stu`实际上一致指向同一个指针, 最终该指针的值为遍历的最后一个`struct的值拷贝`。 187 | 188 | 189 | ![](images/109-foreach.jpeg) 190 | 191 | **正确写法** 192 | 193 | ```go 194 | package main 195 | 196 | import ( 197 | "fmt" 198 | ) 199 | 200 | type student struct { 201 | Name string 202 | Age int 203 | } 204 | 205 | func main() { 206 | //定义map 207 | m := make(map[string]*student) 208 | 209 | //定义student数组 210 | stus := []student{ 211 | {Name: "zhou", Age: 24}, 212 | {Name: "li", Age: 23}, 213 | {Name: "wang", Age: 22}, 214 | } 215 | 216 | // 遍历结构体数组,依次赋值给map 217 | for i := 0; i < len(stus); i++ { 218 | m[stus[i].Name] = &stus[i] 219 | } 220 | 221 | //打印map 222 | for k,v := range m { 223 | fmt.Println(k ,"=>", v.Name) 224 | } 225 | } 226 | ``` 227 | ![](images/110-foreach2.jpeg) 228 | 229 | **运行结果** 230 | 231 | ```bash 232 | zhou => zhou 233 | li => li 234 | wang => wang 235 | ``` 236 | 237 | 238 | -------------------------------------------------------------------------------- /3、对于操作系统而言进程、线程以及Goroutine协程的区别.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 3、对于操作系统而言进程、线程以及Goroutine协程的区别 4 | 5 | 6 | 7 | 进程、线程、协程实际上都是为并发而生。 8 | 9 | 但是他们的各自的模样是完全不一致的,下面我们来分析一下他们各自的特点和关系。 10 | 11 | 12 | 13 | > 本文不重点介绍什么是进程和线程,而是提炼进程、线程、协程干货。且是基于Linux下的进程、线程解释 14 | 15 | ### 一、进程内存 16 | 17 | 进程,可执行程序运行中形成一个独立的内存体,这个内存体**有自己独立的地址空间(Linux会给每个进程分配一个虚拟内存空间32位操作系统为4G, 64位为很多T),有自己的堆**,上级挂靠单位是操作系统。**操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位**。 18 | 19 | ![](images/152-进程线程1.jpeg) 20 | 21 | ### 二、线程内存 22 | 23 | **线程,有时被称为轻量级进程(Lightweight Process,LWP),是操作系统调度(CPU调度)执行的最小单位**。 24 | 25 | ![](images/153-进程线程2.jpeg) 26 | 27 | 多个线程共同“寄生”在一个进程上,除了拥有各自的栈空间,其他的内存空间都是一起共享。所以由于这个特性,使得线程之间的内存关联性很大,互相通信就很简单(堆区、全局区等数据都共享,需要加锁机制即可完成同步通信),但是同时也让线程之间生命体联系较大,比如一个线程出问题,到底进程问题,也就导致了其他线程问题。 28 | 29 | 30 | 31 | ### 三、执行单元 32 | 33 | 对于Linux来讲,不区分进程还是线程,他们都是一个单独的执行单位,CPU一视同仁,均分配时间片。 34 | 35 | 36 | ![](images/154-进程线程3.jpeg) 37 | 38 | 39 | 40 | 所以,如果一个进程想更大程度的与其他进程抢占CPU的资源,那么多开线程是一个好的办法。 41 | 42 | 如上图,进程A没有开线程,那么默认就是`1个线程`,对于内核来讲,它只有1个`执行单元`,进程B开了`3个线程`,那么在内核中,该进程就占有3个`执行单元`。CPU的视野是只能看见内核的,它不知晓谁是进程和谁是线程,谁和谁是一家人。时间片轮询平均调度分配。那么进程B拥有的3个单元就有了资源供给的优势。 43 | 44 | 45 | 46 | ### 四、切换问题与协程 47 | 48 | 49 | 50 | 我们通过上述的描述,可以知道,线程越多,进程利用(或者)抢占的cpu资源就越高。 51 | ![](images/155-进程线程4.jpeg) 52 | 53 | 那么是不是线程可以无限制的多呢? 54 | 55 | 答案当然不是的,我们知道,当我们cpu在内核态切换一个`执行单元`的时候,会有一个时间成本和性能开销 56 | ![](images/156-进程线程5.jpeg) 57 | 58 | 其中性能开销至少会有两个开销 59 | 60 | * 切换内核栈 61 | 62 | * 切换硬件上下文 63 | 64 | 65 | 66 | > 这两个切换,我们没必要太深入研究,可以理解为他所带来的后果和影响是 67 | 68 | > --- 69 | 70 | * 保存寄存器中的内容 71 | 72 | 将之前执行流程的状态保存。 73 | 74 | * CPU高速缓存失效 75 | 76 | 页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个cache就是TLB.当进程切换后页表也要进行切换,页表切换后TLB就失效了,cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,**表现出来的就是程序运行会变慢**。 77 | 78 | > --- 79 | 80 | 81 | 82 | 综上,我们不能够大量的开辟,因为`线程执行流程`越多,cpu在切换的时间成本越大。很多编程语言就想了办法,既然我们不能左右和优化cpu切换线程的开销,那么,我们能否让cpu内核态不切换`执行单元`, 而是在用户态切换执行流程呢? 83 | 84 | 85 | 86 | 很显然,我们是没权限修改操作系统内核机制的,那么只能在用户态再来一个`伪执行单元`,那么就是`协程`了。 87 | 88 | ![](images/157-进程线程6.jpeg) 89 | 90 | 91 | 92 | ### 五、协程的切换成本 93 | 94 | 协程切换比线程切换快主要有两点: 95 | 96 | (1)协程切换**完全在用户空间进行**。线程切换涉及**特权模式切换,需要在内核空间完成**; 97 | 98 | (2)协程切换相比线程切换**做的事情更少**,线程需要有内核和用户态的切换,系统调用过程。 99 | 100 | 101 | 102 | #### 协程切换成本: 103 | 104 | 协程切换非常简单,就是把**当前协程的 CPU 寄存器状态保存起来,然后将需要切换进来的协程的 CPU 寄存器状态加载到 CPU 寄存器上**就 ok 了。而且**完全在用户态进行**,一般来说一次协程上下文切换最多就是**几十ns** 这个量级。 105 | 106 | 107 | 108 | #### 线程切换成本: 109 | 110 | 系统内核调度的对象是线程,因为线程是调度的基本单元(进程是资源拥有的基本单元,进程的切换需要做的事情更多,这里占时不讨论进程切换),而**线程的调度只有拥有最高权限的内核空间才可以完成**,所以线程的切换涉及到**用户空间和内核空间的切换**,也就是特权模式切换,然后需要操作系统调度模块完成**线程调度(task_struct)**,而且除了和协程相同基本的 CPU 上下文,还有线程私有的栈和寄存器等,说白了就是上下文比协程多一些,其实简单比较下 task_strcut 和 任何一个协程库的 coroutine 的 struct 结构体大小就能明显区分出来。而且特权模式切换的开销确实不小,随便搜一组测试数据 [3],随便算算都比协程切换开销大很多。 111 | 112 | 113 | 114 | **进程占用多少内存** 115 | 116 | 4g 117 | 118 | 119 | 120 | **线程占用多少内存** 121 | 122 | 线程跟不同的操作系统版本有有差异 123 | 124 | ```bash 125 | $ulimit -s 126 | 8192 127 | ``` 128 | 129 | 单位`kb` 130 | 131 | 132 | 133 | 但线程基本都是维持Mb的量级单位,一般是4~64Mb不等, 多数维持约10M上下 134 | 135 | 136 | 137 | **协程占用多少内存** 138 | 139 | 测试环境 140 | 141 | ```bash 142 | $ more /proc/cpuinfo | grep "model name" 143 | model name : Intel(R) Core(TM) i7-5775R CPU @ 3.30GHz 144 | model name : Intel(R) Core(TM) i7-5775R CPU @ 3.30GHz 145 | 146 | (2个CPU ) 147 | 148 | $ grep MemTotal /proc/meminfo 149 | MemTotal: 2017516 kB 150 | 151 | (2G内存) 152 | 153 | $ getconf LONG_BIT 154 | 64 155 | 156 | (64位操作系统) 157 | 158 | $ uname -a 159 | Linux ubuntu 4.15.0-91-generic #92-Ubuntu SMP Fri Feb 28 11:09:48 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux 160 | 161 | 162 | ``` 163 | 164 | 165 | 166 | 测试程序 167 | 168 | ```go 169 | package main 170 | 171 | import ( 172 | 173 | "time" 174 | ) 175 | 176 | func main() { 177 | 178 | for i := 0; i < 200000; i++ { 179 | 180 | go func() { 181 | 182 | time.Sleep(5 * time.Second) 183 | 184 | }() 185 | 186 | } 187 | 188 | time.Sleep(10 * time.Second) 189 | } 190 | 191 | ``` 192 | 193 | 194 | 195 | 196 | 197 | 程序运行前 198 | 199 | ```bash 200 | top - 00:16:24 up 7:08, 1 user, load average: 0.08, 0.03, 0.01 201 | 任务: 288 total, 1 running, 218 sleeping, 0 stopped, 0 zombie 202 | %Cpu0 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st 203 | %Cpu1 : 0.3 us, 0.3 sy, 0.0 ni, 99.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st 204 | KiB Mem : 2017516 total, 593836 free, 1163524 used, 260156 buff/cache 205 | KiB Swap: 969960 total, 574184 free, 395776 used. 679520 avail Mem 206 | ``` 207 | 208 | free的mem为1163524, 209 | 210 | 211 | 212 | 程序运行中 213 | 214 | ```bash 215 | top - 00:17:12 up 7:09, 1 user, load average: 0.04, 0.02, 0.00 216 | 任务: 290 total, 1 running, 220 sleeping, 0 stopped, 0 zombie 217 | %Cpu0 : 4.0 us, 1.0 sy, 0.0 ni, 95.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st 218 | %Cpu1 : 8.8 us, 1.4 sy, 0.0 ni, 89.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st 219 | KiB Mem : 2017516 total, 89048 free, 1675844 used, 252624 buff/cache 220 | KiB Swap: 969960 total, 563688 free, 406272 used. 168812 avail Mem 221 | ``` 222 | 223 | free的mem为1675844, 224 | 225 | 所以 **20万个** 协程占用了约 **50万KB**, **平均** 一个协程占用约 **2.5KB** 226 | 227 | 228 | 229 | 那么,go的协程切换成本如此小,占用也那么小,是否可以无限开辟呢? 230 | 231 | 232 | -------------------------------------------------------------------------------- /4、Golang中make与new有何区别?.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 4、Golang中make与new有何区别? 4 | 5 | ### 一、**前言** 6 | 7 | 本文主要给大家介绍了Go语言中函数`new`与`make`的使用和区别,关于Go语言中`new`和`make`是内建的两个函数,主要用来创建分配类型内存。在我们定义生成变量的时候,可能会觉得有点迷惑,其实他们的规则很简单,下面我们就通过一些示例说明他们的区别和使用。 8 | 9 | ### 二、变量的声明 10 | 11 | ```go 12 | var i int 13 | var s string 14 | ``` 15 | 16 | ​ 变量的声明我们可以通过var关键字,然后就可以在程序中使用。当我们不指定变量的默认值时,这些变量的默认值是他们的零值,比如int类型的零值是0,string类型的零值是`""`,引用类型的零值是`nil`。 17 | 18 | 对于例子中的两种类型的声明,我们可以直接使用,对其进行赋值输出。但是如果我们换成指针类型呢? 19 | 20 | > test1.go 21 | 22 | ```go 23 | package main 24 | 25 | import ( 26 | "fmt" 27 | ) 28 | 29 | func main() { 30 | var i *int 31 | *i=10 32 | fmt.Println(*i) 33 | } 34 | ``` 35 | 36 | 37 | 38 | ```bash 39 | $ go run test1.go 40 | panic: runtime error: invalid memory address or nil pointer dereference 41 | [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x4849df] 42 | 43 | goroutine 1 [running]: 44 | main.main() 45 | /home/itheima/go/src/golang_deeper/make_new/t 46 | ``` 47 | 48 | 从这个提示中可以看出,对于引用类型的变量,我们不光要声明它,还要为它分配内容空间,否则我们的值放在哪里去呢?这就是上面错误提示的原因。 49 | 50 | 对于值类型的声明不需要,是因为已经默认帮我们分配好了。 51 | 52 | 要分配内存,就引出来今天的`new`和`make`。 53 | 54 | 55 | 56 | ### 三、new 57 | 58 | 对于上面的问题我们如何解决呢?既然我们知道了没有为其分配内存,那么我们使用new分配一个吧。 59 | 60 | ```go 61 | func main() { 62 | 63 | var i *int 64 | i=new(int) 65 | *i=10 66 | fmt.Println(*i) 67 | 68 | } 69 | ``` 70 | 71 | 现在再运行程序,完美PASS,打印10。现在让我们看下new这个内置的函数。 72 | 73 | ```go 74 | // The new built-in function allocates memory. The first argument is a type, 75 | // not a value, and the value returned is a pointer to a newly 76 | // allocated zero value of that type. 77 | func new(Type) *Type 78 | ``` 79 | 80 | ​ 它只接受一个参数,这个参数是一个类型,分配好内存后,返回一个指向该类型内存地址的指针。同时请注意它同时把分配的内存置为零,也就是类型的零值。 81 | 82 | 我们的例子中,如果没有`*i=10`,那么打印的就是0。这里体现不出来new函数这种内存置为零的好处,我们再看一个例子。 83 | 84 | > test2.go 85 | 86 | ```go 87 | package main 88 | 89 | import ( 90 | "fmt" 91 | "sync" 92 | ) 93 | 94 | type user struct { 95 | lock sync.Mutex 96 | name string 97 | age int 98 | } 99 | 100 | func main() { 101 | 102 | u := new(user) //默认给u分配到内存全部为0 103 | 104 | u.lock.Lock() //可以直接使用,因为lock为0,是开锁状态 105 | u.name = "张三" 106 | u.lock.Unlock() 107 | 108 | fmt.Println(u) 109 | } 110 | ``` 111 | 112 | 运行 113 | 114 | ```bash 115 | $ go run test2.go 116 | &{{0 0} 张三 0} 117 | ``` 118 | 119 | 120 | 121 | 示例中的user类型中的lock字段我不用初始化,直接可以拿来用,不会有无效内存引用异常,因为它已经被零值了。 122 | 123 | 这就是new,它返回的永远是类型的指针,指向分配类型的内存地址。 124 | 125 | 126 | 127 | ### 四、make 128 | 129 | make也是用于内存分配的,但是和new不同。 130 | 131 | 它只用于 132 | 133 | * chan 134 | * map 135 | * slice 136 | 137 | 的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。 138 | 139 | 注意,因为这三种类型是引用类型,所以必须得初始化,但是不是置为零值,这个和new是不一样的。 140 | 141 | ```go 142 | func make(t Type, size ...IntegerType) Type 143 | ``` 144 | 145 | 从函数声明中可以看到,返回的还是该类型。 146 | 147 | 148 | 149 | ### 五、make与new的异同 150 | 151 | 152 | 153 | 相同 154 | 155 | * 堆空间分配 156 | 157 | 158 | 159 | 不同 160 | 161 | make: 只用于slice、map以及channel的初始化, 无可替代 162 | 163 | new: 用于类型内存分配(初始化值为0), 不常用 164 | 165 | 166 | 167 | > new不常用 168 | > 169 | > 所以有new这个内置函数,可以给我们分配一块内存让我们使用,但是现实的编码中,它是不常用的。我们通常都是采用短语句声明以及结构体的字面量达到我们的目的,比如: 170 | > 171 | > ```go 172 | > i : =0 173 | > u := user{} 174 | > ``` 175 | 176 | 177 | 178 | > make 无可替代 179 | > 180 | > 我们在使用slice、map以及channel的时候,还是要使用make进行初始化,然后才才可以对他们进行操作。 181 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /4、Go是否可以无限go?如何限定数量?.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 4、Go是否可以无限go? 如何限定数量? 4 | 5 | 6 | 7 | ### 一、不控制goroutine数量引发的问题 8 | 9 | 我们都知道Goroutine具备如下两个特点 10 | 11 | * 体积轻量 12 | * 优质的GMP调度 13 | 14 | 那么goroutine是否可以无限开辟呢,如果做一个服务器或者一些高业务的场景,能否随意的开辟goroutine并且放养不管呢?让他们自生自灭,毕竟有强大的GC和优质的调度算法支撑? 15 | 16 | 那么我可以先看如下一个问题。 17 | 18 | 19 | 20 | > code1.go 21 | 22 | ```go 23 | package main 24 | 25 | import ( 26 | "fmt" 27 | "math" 28 | "runtime" 29 | ) 30 | 31 | func main() { 32 | //模拟用户需求业务的数量 33 | task_cnt := math.MaxInt64 34 | 35 | for i := 0; i < task_cnt; i++ { 36 | go func(i int) { 37 | //... do some busi... 38 | 39 | fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine()) 40 | }(i) 41 | } 42 | } 43 | ``` 44 | 45 | 结果 46 | 47 | image-20200328231947588 48 | 49 | 50 | 51 | 最后被操作系统以kill信号,强制终结该进程。 52 | 53 | ```bash 54 | signal: killed 55 | ``` 56 | 57 | 所以,我们迅速的开辟goroutine(**不控制并发的 goroutine 数量** )会在短时间内占据操作系统的资源(CPU、内存、文件描述符等)。 58 | 59 | 60 | 61 | - CPU 使用率浮动上涨 62 | - Memory 占用不断上涨。 63 | - 主进程崩溃(被杀掉了) 64 | 65 | 66 | 67 | 这些资源实际上是所有用户态程序共享的资源,所以大批的goroutine最终引发的灾难不仅仅是自身,还会关联其他运行的程序。 68 | 69 | 所以在编写逻辑业务的时候,限制goroutine是我们必须要重视的问题。 70 | 71 | --- 72 | 73 | ### 二、一些简单方法控制goroutines数量 74 | 75 | #### 方法一:只是用有buffer的channel来限制 76 | 77 | 78 | 79 | > code2.go 80 | 81 | ```go 82 | package main 83 | 84 | import ( 85 | "fmt" 86 | "math" 87 | "runtime" 88 | ) 89 | 90 | func busi(ch chan bool, i int) { 91 | 92 | fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine()) 93 | <-ch 94 | } 95 | 96 | func main() { 97 | //模拟用户需求业务的数量 98 | task_cnt := math.MaxInt64 99 | //task_cnt := 10 100 | 101 | ch := make(chan bool, 3) 102 | 103 | for i := 0; i < task_cnt; i++ { 104 | 105 | ch <- true 106 | 107 | go busi(ch, i) 108 | } 109 | 110 | } 111 | ``` 112 | 113 | 114 | 115 | 结果 116 | 117 | ```bash 118 | ... 119 | go func 352277 goroutine count = 4 120 | go func 352278 goroutine count = 4 121 | go func 352279 goroutine count = 4 122 | go func 352280 goroutine count = 4 123 | go func 352281 goroutine count = 4 124 | go func 352282 goroutine count = 4 125 | go func 352283 goroutine count = 4 126 | go func 352284 goroutine count = 4 127 | go func 352285 goroutine count = 4 128 | go func 352286 goroutine count = 4 129 | go func 352287 goroutine count = 4 130 | go func 352288 goroutine count = 4 131 | go func 352289 goroutine count = 4 132 | go func 352290 goroutine count = 4 133 | go func 352291 goroutine count = 4 134 | go func 352292 goroutine count = 4 135 | go func 352293 goroutine count = 4 136 | go func 352294 goroutine count = 4 137 | go func 352295 goroutine count = 4 138 | go func 352296 goroutine count = 4 139 | go func 352297 goroutine count = 4 140 | go func 352298 goroutine count = 4 141 | go func 352299 goroutine count = 4 142 | go func 352300 goroutine count = 4 143 | go func 352301 goroutine count = 4 144 | go func 352302 goroutine count = 4 145 | ... 146 | ``` 147 | 148 | 149 | 150 | 从结果看,程序并没有出现崩溃,而是按部就班的顺序执行,并且go的数量控制在了3,(4的原因是因为还有一个main goroutine)那么从数字上看,是不是在跑的goroutines有几十万个呢? 151 | 152 | ![](images/149-goroutines3.jpeg) 153 | 154 | 这里我们用了,buffer为3的channel, 在写的过程中,实际上是限制了速度。限制的是 155 | 156 | ```go 157 | for i := 0; i < go_cnt; i++ { //循环速度 158 | 159 | ch <- true 160 | 161 | go busi(ch, i) 162 | } 163 | ``` 164 | 165 | `for`循环的速度,因为这个速度决定了go的创建速度,而go的结束速度取决于 `busi()`函数的执行速度。 这样实际上,我们就能够保证了,同一时间内运行的goroutine的数量与buffer的数量一致。从而达到了限定效果。 166 | 167 | 168 | 169 | 但是这段代码有一个小问题,就是如果我们把go_cnt的数量变的小一些,会出现打出的结果不正确。 170 | 171 | ```go 172 | package main 173 | 174 | import ( 175 | "fmt" 176 | //"math" 177 | "runtime" 178 | ) 179 | 180 | func busi(ch chan bool, i int) { 181 | 182 | fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine()) 183 | <-ch 184 | } 185 | 186 | func main() { 187 | //模拟用户需求业务的数量 188 | //task_cnt := math.MaxInt64 189 | task_cnt := 10 190 | 191 | ch := make(chan bool, 3) 192 | 193 | for i := 0; i < task_cnt; i++ { 194 | 195 | ch <- true 196 | 197 | go busi(ch, i) 198 | } 199 | 200 | } 201 | ``` 202 | 203 | 结果 204 | 205 | ```bash 206 | go func 2 goroutine count = 4 207 | go func 3 goroutine count = 4 208 | go func 4 goroutine count = 4 209 | go func 5 goroutine count = 4 210 | go func 6 goroutine count = 4 211 | go func 1 goroutine count = 4 212 | go func 8 goroutine count = 4 213 | ``` 214 | 215 | 是因为`main`将全部的go开辟完之后,就立刻退出进程了。所以想全部go都执行,需要在main的最后进行阻塞操作。 216 | 217 | #### 方法二:只使用sync同步机制 218 | 219 | > code3.go 220 | 221 | ```go 222 | import ( 223 | "fmt" 224 | "math" 225 | "sync" 226 | "runtime" 227 | ) 228 | 229 | var wg = sync.WaitGroup{} 230 | 231 | func busi(i int) { 232 | 233 | fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine()) 234 | wg.Done() 235 | } 236 | 237 | func main() { 238 | //模拟用户需求业务的数量 239 | task_cnt := math.MaxInt64 240 | 241 | 242 | for i := 0; i < task_cnt; i++ { 243 | wg.Add(1) 244 | go busi(i) 245 | } 246 | 247 | wg.Wait() 248 | } 249 | ``` 250 | 251 | 很明显,单纯的使用`sync`依然达不到控制goroutine的数量,所以最终结果依然是崩溃。 252 | 253 | 结果 254 | 255 | ```bash 256 | ... 257 | go func 7562 goroutine count = 7582 258 | go func 24819 goroutine count = 17985 259 | go func 7685 goroutine count = 7582 260 | go func 24701 goroutine count = 17984 261 | go func 7563 goroutine count = 7582 262 | go func 24821 goroutine count = 17983 263 | go func 24822 goroutine count = 17983 264 | go func 7686 goroutine count = 7582 265 | go func 24703 goroutine count = 17982 266 | go func 7564 goroutine count = 7582 267 | go func 24824 goroutine count = 17981 268 | go func 7687 goroutine count = 7582 269 | go func 24705 goroutine count = 17980 270 | go func 24706 goroutine count = 17980 271 | go func 24707 goroutine count = 17979 272 | go func 7688 goroutine count = 7582 273 | go func 24826 goroutine count = 17978 274 | go func 7566 goroutine count = 7582 275 | go func 24709 goroutine count = 17977 276 | go func 7689 goroutine count = 7582 277 | go func 24828 goroutine count = 17976 278 | go func 24829 goroutine count = 17976 279 | go func 7567 goroutine count = 7582 280 | go func 24711 goroutine count = 17975 281 | //操作系统停止响应 282 | ``` 283 | 284 | 285 | 286 | 287 | 288 | #### 方法三:channel与sync同步组合方式 289 | 290 | > code4.go 291 | 292 | ```go 293 | package main 294 | 295 | import ( 296 | "fmt" 297 | "math" 298 | "sync" 299 | "runtime" 300 | ) 301 | 302 | var wg = sync.WaitGroup{} 303 | 304 | func busi(ch chan bool, i int) { 305 | 306 | fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine()) 307 | 308 | <-ch 309 | 310 | wg.Done() 311 | } 312 | 313 | func main() { 314 | //模拟用户需求go业务的数量 315 | task_cnt := math.MaxInt64 316 | 317 | ch := make(chan bool, 3) 318 | 319 | for i := 0; i < task_cnt; i++ { 320 | wg.Add(1) 321 | 322 | ch <- true 323 | 324 | go busi(ch, i) 325 | } 326 | 327 | wg.Wait() 328 | } 329 | ``` 330 | 331 | 332 | 333 | 结果 334 | 335 | ```bash 336 | //... 337 | go func 228851 goroutine count = 4 338 | go func 228852 goroutine count = 4 339 | go func 228853 goroutine count = 4 340 | go func 228854 goroutine count = 4 341 | go func 228855 goroutine count = 4 342 | go func 228856 goroutine count = 4 343 | go func 228857 goroutine count = 4 344 | go func 228858 goroutine count = 4 345 | go func 228859 goroutine count = 4 346 | go func 228860 goroutine count = 4 347 | go func 228861 goroutine count = 4 348 | go func 228862 goroutine count = 4 349 | go func 228863 goroutine count = 4 350 | go func 228864 goroutine count = 4 351 | go func 228865 goroutine count = 4 352 | go func 228866 goroutine count = 4 353 | go func 228867 goroutine count = 4 354 | //... 355 | ``` 356 | 357 | 358 | 359 | 这样我们程序就不会再造成资源爆炸而崩溃。而且运行go的数量控制住了在buffer为3的这个范围内。 360 | 361 | 362 | 363 | #### 方法四:利用无缓冲channel与任务发送/执行分离方式 364 | 365 | > code5.go 366 | 367 | ```go 368 | package main 369 | 370 | import ( 371 | "fmt" 372 | "math" 373 | "sync" 374 | "runtime" 375 | ) 376 | 377 | var wg = sync.WaitGroup{} 378 | 379 | func busi(ch chan int) { 380 | 381 | for t := range ch { 382 | fmt.Println("go task = ", t, ", goroutine count = ", runtime.NumGoroutine()) 383 | wg.Done() 384 | } 385 | } 386 | 387 | func sendTask(task int, ch chan int) { 388 | wg.Add(1) 389 | ch <- task 390 | } 391 | 392 | func main() { 393 | 394 | ch := make(chan int) //无buffer channel 395 | 396 | goCnt := 3 //启动goroutine的数量 397 | for i := 0; i < goCnt; i++ { 398 | //启动go 399 | go busi(ch) 400 | } 401 | 402 | taskCnt := math.MaxInt64 //模拟用户需求业务的数量 403 | for t := 0; t < taskCnt; t++ { 404 | //发送任务 405 | sendTask(t, ch) 406 | } 407 | 408 | wg.Wait() 409 | } 410 | ``` 411 | 412 | 结构 413 | 414 | ```bash 415 | //... 416 | go task = 130069 , goroutine count = 4 417 | go task = 130070 , goroutine count = 4 418 | go task = 130071 , goroutine count = 4 419 | go task = 130072 , goroutine count = 4 420 | go task = 130073 , goroutine count = 4 421 | go task = 130074 , goroutine count = 4 422 | go task = 130075 , goroutine count = 4 423 | go task = 130076 , goroutine count = 4 424 | go task = 130077 , goroutine count = 4 425 | go task = 130078 , goroutine count = 4 426 | go task = 130079 , goroutine count = 4 427 | go task = 130080 , goroutine count = 4 428 | go task = 130081 , goroutine count = 4 429 | go task = 130082 , goroutine count = 4 430 | go task = 130083 , goroutine count = 4 431 | go task = 130084 , goroutine count = 4 432 | go task = 130085 , goroutine count = 4 433 | go task = 130086 , goroutine count = 4 434 | go task = 130087 , goroutine count = 4 435 | go task = 130088 , goroutine count = 4 436 | go task = 130089 , goroutine count = 4 437 | go task = 130090 , goroutine count = 4 438 | go task = 130091 , goroutine count = 4 439 | go task = 130092 , goroutine count = 4 440 | go task = 130093 , goroutine count = 4 441 | ... 442 | ``` 443 | 444 | 执行流程大致如下,这里实际上是将任务的发送和执行做了业务上的分离。使得消息出去,输入SendTask的频率可设置、执行Goroutine的数量也可设置。也就是既控制输入(生产),又控制输出(消费)。使得可控更加灵活。这也是很多Go框架的Worker工作池的最初设计思想理念。 445 | 446 | ![](images/151-goroutines5.jpeg) 447 | 448 | --- 449 | 450 | 以上便是目前有关限定goroutine基础设计思路。 451 | 452 | 参考: 453 | 454 | http://team.jiunile.com/blog/2019/09/go-control-goroutine-number.html 455 | 456 | https://www.joyk.com/dig/detail/1547976674512705 457 | 458 | 459 | -------------------------------------------------------------------------------- /4、interface.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 四、interface 4 | 5 | ### (1) interface的赋值问题 6 | 7 | > 以下代码能编译过去吗?为什么? 8 | 9 | > test12.go 10 | 11 | ```go 12 | package main 13 | 14 | import ( 15 | "fmt" 16 | ) 17 | 18 | type People interface { 19 | Speak(string) string 20 | } 21 | 22 | type Student struct{} 23 | 24 | func (stu *Student) Speak(think string) (talk string) { 25 | if think == "love" { 26 | talk = "You are a good boy" 27 | } else { 28 | talk = "hi" 29 | } 30 | return 31 | } 32 | 33 | func main() { 34 | var peo People = Student{} 35 | think := "love" 36 | fmt.Println(peo.Speak(think)) 37 | } 38 | ``` 39 | 40 | 继承与多态的特点 41 | 42 | 在golang中对多态的特点体现从语法上并不是很明显。 43 | 44 | 我们知道发生多态的几个要素: 45 | 46 | 1、有interface接口,并且有接口定义的方法。 47 | 48 | 2、有子类去重写interface的接口。 49 | 50 | 3、有父类指针指向子类的具体对象 51 | 52 | 那么,满足上述3个条件,就可以产生多态效果,就是,父类指针可以调用子类的具体方法。 53 | 54 | 所以上述代码报错的地方在`var peo People = Student{}`这条语句, `Student{}`已经重写了父类`People{}`中的`Speak(string) string`方法,那么只需要用父类指针指向子类对象即可。 55 | 56 | 所以应该改成`var peo People = &Student{}` 即可编译通过。(People为interface类型,就是指针类型) 57 | 58 | 59 | 60 | ### (2) interface的内部构造(非空接口iface情况) 61 | 62 | > 以下代码打印出来什么内容,说出为什么。 63 | 64 | > test14.go 65 | 66 | ```go 67 | package main 68 | 69 | import ( 70 | "fmt" 71 | ) 72 | 73 | type People interface { 74 | Show() 75 | } 76 | 77 | type Student struct{} 78 | 79 | func (stu *Student) Show() { 80 | 81 | } 82 | 83 | func live() People { 84 | var stu *Student 85 | return stu 86 | } 87 | 88 | func main() { 89 | if live() == nil { 90 | fmt.Println("AAAAAAA") 91 | } else { 92 | fmt.Println("BBBBBBB") 93 | } 94 | } 95 | ``` 96 | 97 | **结果** 98 | 99 | ```bash 100 | BBBBBBB 101 | ``` 102 | 103 | **分析:** 104 | 105 | 我们需要了解`interface`的内部结构,才能理解这个题目的含义。 106 | 107 | interface在使用的过程中,共有两种表现形式 108 | 109 | 一种为**空接口(empty interface)**,定义如下: 110 | 111 | ```go 112 | var MyInterface interface{} 113 | ``` 114 | 115 | 116 | 117 | 另一种为**非空接口(non-empty interface)**, 定义如下: 118 | 119 | ```go 120 | type MyInterface interface { 121 | function() 122 | } 123 | ``` 124 | 125 | 126 | 127 | 这两种interface类型分别用两种`struct`表示,空接口为`eface`, 非空接口为`iface`. 128 | ![](images/115-interface1.jpeg) 129 | 130 | --- 131 | 132 | 133 | 134 | #### **空接口eface** 135 | 136 | 空接口eface结构,由两个属性构成,一个是类型信息_type,一个是数据信息。其数据结构声明如下: 137 | 138 | ```go 139 | type eface struct { //空接口 140 | _type *_type //类型信息 141 | data unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*) 142 | } 143 | ``` 144 | 145 | **_type属性**:是GO语言中所有类型的公共描述,Go语言几乎所有的数据结构都可以抽象成 _type,是所有类型的公共描述,**type负责决定data应该如何解释和操作,**type的结构代码如下: 146 | 147 | ```go 148 | type _type struct { 149 | size uintptr //类型大小 150 | ptrdata uintptr //前缀持有所有指针的内存大小 151 | hash uint32 //数据hash值 152 | tflag tflag 153 | align uint8 //对齐 154 | fieldalign uint8 //嵌入结构体时的对齐 155 | kind uint8 //kind 有些枚举值kind等于0是无效的 156 | alg *typeAlg //函数指针数组,类型实现的所有方法 157 | gcdata *byte 158 | str nameOff 159 | ptrToThis typeOff 160 | } 161 | ``` 162 | 163 | **data属性:** 表示指向具体的实例数据的指针,他是一个`unsafe.Pointer`类型,相当于一个C的万能指针`void*`。 164 | 165 | ![](images/116-interface2.jpeg) 166 | 167 | 168 | --- 169 | 170 | #### 非空接口iface 171 | 172 | iface 表示 non-empty interface 的数据结构,非空接口初始化的过程就是初始化一个iface类型的结构,其中`data`的作用同`eface`的相同,这里不再多加描述。 173 | 174 | ```go 175 | type iface struct { 176 | tab *itab 177 | data unsafe.Pointer 178 | } 179 | ``` 180 | 181 | iface结构中最重要的是itab结构(结构如下),每一个 `itab` 都占 32 字节的空间。itab可以理解为`pair` 。itab里面包含了interface的一些关键信息,比如method的具体实现。 182 | 183 | ```go 184 | type itab struct { 185 | inter *interfacetype // 接口自身的元信息 186 | _type *_type // 具体类型的元信息 187 | link *itab 188 | bad int32 189 | hash int32 // _type里也有一个同样的hash,此处多放一个是为了方便运行接口断言 190 | fun [1]uintptr // 函数指针,指向具体类型所实现的方法 191 | } 192 | ``` 193 | 194 | 其中值得注意的字段,个人理解如下: 195 | 196 | 1. `interface type`包含了一些关于interface本身的信息,比如`package path`,包含的`method`。这里的interfacetype是定义interface的一种抽象表示。 197 | 2. `type`表示具体化的类型,与eface的 *type类型相同。* 198 | 3. `hash`字段其实是对`_type.hash`的拷贝,它会在interface的实例化时,用于快速判断目标类型和接口中的类型是否一致。另,Go的interface的Duck-typing机制也是依赖这个字段来实现。 199 | 4. `fun`字段其实是一个动态大小的数组,虽然声明时是固定大小为1,但在使用时会直接通过fun指针获取其中的数据,并且不会检查数组的边界,所以该数组中保存的元素数量是不确定的。 200 | 201 | ![](images/117-interface3.jpeg) 202 | 203 | --- 204 | 205 | 所以,People拥有一个Show方法的,属于非空接口,People的内部定义应该是一个`iface`结构体 206 | 207 | ```go 208 | type People interface { 209 | Show() 210 | } 211 | ``` 212 | 213 | 214 | ![](images/118-interface4.jpeg) 215 | 216 | 217 | 218 | ```go 219 | func live() People { 220 | var stu *Student 221 | return stu 222 | } 223 | ``` 224 | 225 | stu是一个指向nil的空指针,但是最后`return stu` 会触发`匿名变量 People = stu`值拷贝动作,所以最后`live()`放回给上层的是一个`People insterface{}`类型,也就是一个`iface struct{}`类型。 stu为nil,只是`iface`中的data 为nil而已。 但是`iface struct{}`本身并不为nil. 226 | 227 | ![](images/119-interface5.jpeg) 228 | 229 | 所以如下判断的结果为`BBBBBBB`: 230 | 231 | ```go 232 | func main() { 233 | if live() == nil { 234 | fmt.Println("AAAAAAA") 235 | } else { 236 | fmt.Println("BBBBBBB") 237 | } 238 | } 239 | ``` 240 | 241 | 242 | 243 | ### (3) interface内部构造(空接口eface情况) 244 | 245 | > 下面代码结果为什么? 246 | 247 | ```go 248 | func Foo(x interface{}) { 249 | if x == nil { 250 | fmt.Println("empty interface") 251 | return 252 | } 253 | fmt.Println("non-empty interface") 254 | } 255 | func main() { 256 | var p *int = nil 257 | Foo(p) 258 | } 259 | ``` 260 | 261 | **结果** 262 | 263 | ```bash 264 | non-empty interface 265 | ``` 266 | 267 | **分析** 268 | 269 | 不难看出,`Foo()`的形参`x interface{}`是一个空接口类型`eface struct{}`。 270 | 271 | ![](images/120-interface6.jpeg) 272 | 273 | 在执行`Foo(p)`的时候,触发`x interface{} = p`语句,所以此时 x结构如下。 274 | ![](images/121-interface7.jpeg) 275 | 276 | 所以 x 结构体本身不为nil,而是data指针指向的p为nil。 277 | 278 | --- 279 | 280 | 281 | 282 | ### (4) inteface{}与*interface{} 283 | 284 | > ABCD中哪一行存在错误? 285 | 286 | > test15.go 287 | 288 | ```go 289 | type S struct { 290 | } 291 | 292 | func f(x interface{}) { 293 | } 294 | 295 | func g(x *interface{}) { 296 | } 297 | 298 | func main() { 299 | s := S{} 300 | p := &s 301 | f(s) //A 302 | g(s) //B 303 | f(p) //C 304 | g(p) //D 305 | } 306 | ``` 307 | 308 | **结果** 309 | 310 | ```bash 311 | B、D两行错误 312 | B错误为: cannot use s (type S) as type *interface {} in argument to g: 313 | *interface {} is pointer to interface, not interface 314 | 315 | D错误为:cannot use p (type *S) as type *interface {} in argument to g: 316 | *interface {} is pointer to interface, not interface 317 | 318 | ``` 319 | 320 | 看到这道题需要第一时间想到的是Golang是强类型语言,interface是所有golang类型的父类 函数中`func f(x interface{})`的`interface{}`可以支持传入golang的任何类型,包括指针,但是函数`func g(x *interface{})`只能接受`*interface{}` 321 | 322 | -------------------------------------------------------------------------------- /5、Golang三色标记+混合写屏障GC模式全分析.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 5、Golang三色标记+混合写屏障GC模式全分析 4 | > 本节为**重点**章节 5 | 6 | 7 | > 本章节含视频版: 8 | 9 | [![](images/标题.jpeg)](https://www.bilibili.com/video/BV1wz4y1y7Kd) 10 | 11 | 12 | --- 13 | 14 | 垃圾回收(Garbage Collection,简称GC)是编程语言中提供的自动的内存管理机制,自动释放不需要的对象,让出存储器资源,无需程序员手动执行。 15 | 16 | ​ Golang中的垃圾回收主要应用三色标记法,GC过程和其他用户goroutine可并发运行,但需要一定时间的**STW(stop the world)**,STW的过程中,CPU不执行用户代码,全部用于垃圾回收,这个过程的影响很大,Golang进行了多次的迭代优化来解决这个问题。 17 | 18 | 19 | 20 | 21 | 22 | ### 一、Go V1.3之前的标记-清除(mark and sweep)算法 23 | 24 | 此算法主要有两个主要的步骤: 25 | 26 | - 标记(Mark phase) 27 | - 清除(Sweep phase) 28 | 29 | 30 | 31 | **第一步**,暂停程序业务逻辑,分类出可达和不可达的对象,然后做上标记。 32 | 33 | ![](images/44-GC1.png) 34 | 图中表示是程序与对象的可达关系,目前程序的可达对象有对象1-2-3,对象4-7等五个对象。 35 | 36 | **第二步**, 开始标记,程序找出它所有可达的对象,并做上标记。如下图所示: 37 | ![](images/42-GC2.png) 38 | 所以对象1-2-3、对象4-7等五个对象被做上标记。 39 | 40 | **第三步**, 标记完了之后,然后开始清除未标记的对象. 结果如下. 41 | ![](images/45-GC3.png) 42 | 43 | 操作非常简单,但是有一点需要额外注意,Mark and Sweep算法在执行的时候,需要程序暂停,即 STW(Stop The World)。STW的过程中,CPU不执行用户代码,全部用于垃圾回收,这个过程的影响很大,所以STW也是一些回收机制最大的难题和希望优化的点。所以在执行第三步的这段时间,程序会暂定停止任何工作,卡在那等待回收执行完毕。 44 | 45 | **第四步**, 停止暂停,让程序继续跑。然后循环重复这个过程,直到process程序生命周期结束。 46 | 47 | 以上便是标记清除回收的算法。 48 | 49 | ### 二、标记-清扫(mark and sweep)的缺点 50 | 51 | - STW,stop the world;让程序暂停,程序出现卡顿 **(重要问题)**。 52 | - 标记需要扫描整个heap 53 | - 清除数据会产生heap碎片 54 | 55 | 56 | 所以Go V1.3版本之前就是以上来实施的, 流程是 57 | 58 | ![](images/53-STW1.png) 59 | 60 | 61 | 62 | Go V1.3 做了简单的优化,将STW提前, 减少STW暂停的时间范围.如下所示 63 | 64 | ![](images/54-STW2.png) 65 | 66 | 67 | 68 | **这里面最重要的问题就是:mark-and-sweep 算法会暂停整个程序** 。 69 | 70 | 71 | 72 | Go是如何面对并这个问题的呢?接下来G V1.5版本 就用**三色并发标记法**来优化这个问题. 73 | 74 | 75 | 76 | ### 三、Go V1.5的三色并发标记法 77 | 78 | 三色标记法 实际上就是通过三个阶段的标记来确定清楚的对象都有哪些. 我们来看一下具体的过程. 79 | 80 | 81 | 82 | **第一步** , 就是只要是新创建的对象,默认的颜色都是标记为“白色”. 83 | 84 | ![](images/46-GC4.png) 85 | 86 | 这里面需要注意的是, 所谓“程序”, 则是一些对象的跟节点集合. 87 | ![](images/47-GC5.jpeg) 88 | 89 | 所以上图,可以转换如下的方式来表示. 90 | 91 | 92 | 93 | **第二步**, 每次GC回收开始, 然后从根节点开始遍历所有对象,把遍历到的对象从白色集合放入“灰色”集合。 94 | 95 | ![](images/48-GC6.jpeg) 96 | 97 | **第三步**, 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合 98 | 99 | ![](images/49-GC7.jpeg) 100 | 101 | **第四步**, 重复**第三步**, 直到灰色中无任何对象. 102 | ![](images/50-GC8.jpeg) 103 | ![](images/51-GC9.jpeg) 104 | 105 | **第五步**: 回收所有的白色标记表的对象. 也就是回收垃圾. 106 | ![](images/52-GC10.jpeg) 107 | 108 | 109 | 以上便是`三色并发标记法`, 不难看出,我们上面已经清楚的体现`三色`的特性, 那么又是如何实现并行的呢? 110 | 111 | 112 | 113 | > Go是如何解决标记-清除(mark and sweep)算法中的卡顿(stw,stop the world)问题的呢? 114 | 115 | 116 | 117 | 118 | 119 | ### 四、没有STW的三色标记法 120 | 121 | ​ 我们还是基于上述的三色并发标记法来说, 他是一定要依赖STW的. 因为如果不暂停程序, 程序的逻辑改变对象引用关系, 这种动作如果在标记阶段做了修改,会影响标记结果的正确性。我们举一个场景. 122 | 123 | 124 | 125 | 如果三色标记法, 标记过程不使用STW将会发生什么事情? 126 | 127 | 128 | ![](images/55-三色标记问题1.jpeg) 129 | 130 | ![](images/56-三色标记问题2.jpeg) 131 | 132 | ![](images/57-三色标记问题3.jpeg) 133 | 134 | ![](images/58-三色标记问题4.jpeg) 135 | 136 | ![](images/59-三色标记问题5.jpeg) 137 | 138 | 139 | 可以看出,有两个问题, 在三色标记法中,是不希望被发生的 140 | 141 | 142 | 143 | * 条件1: 一个白色对象被黑色对象引用**(白色被挂在黑色下)** 144 | * 条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏**(灰色同时丢了该白色)** 145 | 146 | 147 | 148 | 当以上两个条件同时满足时, 就会出现对象丢失现象! 149 | 150 | ​ 151 | 152 | ​ 当然, 如果上述中的白色对象3, 如果他还有很多下游对象的话, 也会一并都清理掉. 153 | 154 | 155 | 156 | ​ 为了防止这种现象的发生,最简单的方式就是STW,直接禁止掉其他用户程序对对象引用关系的干扰,但是**STW的过程有明显的资源浪费,对所有的用户程序都有很大影响**,如何能在保证对象不丢失的情况下合理的尽可能的提高GC效率,减少STW时间呢? 157 | 158 | 159 | 160 | ​ 答案就是, 那么我们只要使用一个机制,来破坏上面的两个条件就可以了. 161 | 162 | 163 | 164 | ### 五、屏障机制 165 | 166 | ​ 我们让GC回收器,满足下面两种情况之一时,可保对象不丢失. 所以引出两种方式. 167 | 168 | 169 | 170 | #### (1) “强-弱” 三色不变式 171 | 172 | * 强三色不变式 173 | 174 | 不存在黑色对象引用到白色对象的指针。 175 | 176 | ![](images/60-三色标记问题6.jpeg) 177 | 178 | 179 | * 弱三色不变式 180 | 181 | 所有被黑色对象引用的白色对象都处于灰色保护状态. 182 | 183 | 184 | ![](images/61-三色标记问题7.jpeg) 185 | 186 | 187 | 188 | 为了遵循上述的两个方式,Golang团队初步得到了如下具体的两种屏障方式“插入屏障”, “删除屏障”. 189 | 190 | 191 | 192 | #### (2) 插入屏障 193 | 194 | `具体操作`: 在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色) 195 | 196 | `满足`: **强三色不变式**. (不存在黑色对象引用白色对象的情况了, 因为白色会强制变成灰色) 197 | 198 | 伪码如下: 199 | 200 | ```go 201 | 添加下游对象(当前下游对象slot, 新下游对象ptr) { 202 | //1 203 | 标记灰色(新下游对象ptr) 204 | 205 | //2 206 | 当前下游对象slot = 新下游对象ptr 207 | } 208 | ``` 209 | 210 | 场景: 211 | 212 | ```go 213 | A.添加下游对象(nil, B) //A 之前没有下游, 新添加一个下游对象B, B被标记为灰色 214 | A.添加下游对象(C, B) //A 将下游对象C 更换为B, B被标记为灰色 215 | ``` 216 | 217 | 218 | 219 | ​ 这段伪码逻辑就是写屏障,. 我们知道,黑色对象的内存槽有两种位置, `栈`和`堆`. 栈空间的特点是容量小,但是要求相应速度快,因为函数调用弹出频繁使用, 所以“插入屏障”机制,在**栈空间的对象操作中不使用**. 而仅仅使用在堆空间对象的操作中. 220 | 221 | ​ 接下来,我们用几张图,来模拟整个一个详细的过程, 希望您能够更可观的看清晰整体流程。 222 | 223 | --- 224 | ![](images/62-三色标记插入写屏障1.jpeg) 225 | 226 | --- 227 | ![](images/63-三色标记插入写屏障2.jpeg) 228 | 229 | --- 230 | ![](images/64-三色标记插入写屏障3.jpeg) 231 | 232 | --- 233 | ![](images/65-三色标记插入写屏障4.jpeg) 234 | 235 | --- 236 | ![](images/66-三色标记插入写屏障5.jpeg) 237 | 238 | --- 239 | ![](images/67-三色标记插入写屏障6.jpeg) 240 | 241 | ​ 但是如果栈不添加,当全部三色标记扫描之后,栈上有可能依然存在白色对象被引用的情况(如上图的对象9). 所以要对栈重新进行三色标记扫描, 但这次为了对象不丢失, 要对本次标记扫描启动STW暂停. 直到栈空间的三色标记结束. 242 | 243 | --- 244 | ![](images/68-三色标记插入写屏障7.jpeg) 245 | 246 | --- 247 | ![](images/69-三色标记插入写屏障9.jpeg) 248 | 249 | --- 250 | 251 | ![](images/70-三色标记插入写屏障10.jpeg) 252 | 253 | --- 254 | 255 | ​ 最后将栈和堆空间 扫描剩余的全部 白色节点清除. 这次STW大约的时间在10~100ms间. 256 | 257 | ![](images/71-三色标记插入写屏障11.jpeg) 258 | 259 | --- 260 | 261 | 262 | 263 | #### (3) 删除屏障 264 | 265 | `具体操作`: 被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。 266 | 267 | `满足`: **弱三色不变式**. (保护灰色对象到白色对象的路径不会断) 268 | 269 | 270 | 271 | 伪代码: 272 | 273 | ```go 274 | 添加下游对象(当前下游对象slot, 新下游对象ptr) { 275 | //1 276 | if (当前下游对象slot是灰色 || 当前下游对象slot是白色) { 277 | 标记灰色(当前下游对象slot) //slot为被删除对象, 标记为灰色 278 | } 279 | 280 | //2 281 | 当前下游对象slot = 新下游对象ptr 282 | } 283 | ``` 284 | 285 | 场景: 286 | 287 | ```go 288 | A.添加下游对象(B, nil) //A对象,删除B对象的引用。 B被A删除,被标记为灰(如果B之前为白) 289 | A.添加下游对象(B, C) //A对象,更换下游B变成C。 B被A删除,被标记为灰(如果B之前为白) 290 | ``` 291 | 292 | 293 | 294 | 295 | 296 | 接下来,我们用几张图,来模拟整个一个详细的过程, 希望您能够更可观的看清晰整体流程。 297 | 298 | 299 | ![](images/72-三色标记删除写屏障1.jpeg) 300 | 301 | ![](images/73-三色标记删除写屏障2.jpeg) 302 | 303 | ![](images/74-三色标记删除写屏障3.jpeg) 304 | 305 | ![](images/75-三色标记删除写屏障4.jpeg) 306 | 307 | ![](images/76-三色标记删除写屏障5.jpeg) 308 | 309 | ![](images/77-三色标记删除写屏障6.jpeg) 310 | 311 | ![](images/78-三色标记删除写屏障7.jpeg) 312 | 313 | 314 | 315 | 这种方式的回收精度低,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清理掉。 316 | 317 | 318 | 319 | 320 | 321 | ### 六、Go V1.8的混合写屏障(hybrid write barrier)机制 322 | 323 | 插入写屏障和删除写屏障的短板: 324 | 325 | * 插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活; 326 | 327 | * 删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象。 328 | 329 | Go V1.8版本引入了混合写屏障机制(hybrid write barrier),避免了对栈re-scan的过程,极大的减少了STW的时间。结合了两者的优点。 330 | 331 | 332 | 333 | --- 334 | 335 | #### (1) 混合写屏障规则 336 | 337 | `具体操作`: 338 | 339 | 1、GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW), 340 | 341 | 2、GC期间,任何在栈上创建的新对象,均为黑色。 342 | 343 | 3、被删除的对象标记为灰色。 344 | 345 | 4、被添加的对象标记为灰色。 346 | 347 | `满足`: 变形的**弱三色不变式**. 348 | 349 | 350 | 351 | 伪代码: 352 | 353 | ```go 354 | 添加下游对象(当前下游对象slot, 新下游对象ptr) { 355 | //1 356 | 标记灰色(当前下游对象slot) //只要当前下游对象被移走,就标记灰色 357 | 358 | //2 359 | 标记灰色(新下游对象ptr) 360 | 361 | //3 362 | 当前下游对象slot = 新下游对象ptr 363 | } 364 | ``` 365 | 366 | 367 | 368 | > 这里我们注意, 屏障技术是不在栈上应用的,因为要保证栈的运行效率。 369 | 370 | 371 | 372 | 373 | 374 | #### (2) 混合写屏障的具体场景分析 375 | 376 | 接下来,我们用几张图,来模拟整个一个详细的过程, 希望您能够更可观的看清晰整体流程。 377 | 378 | > 注意混合写屏障是Gc的一种屏障机制,所以只是当程序执行GC的时候,才会触发这种机制。 379 | 380 | ##### GC开始:扫描栈区,将可达对象全部标记为黑 381 | 382 | ![](images/79-三色标记混合写屏障1.jpeg) 383 | ![](images/80-三色标记混合写屏障2.jpeg) 384 | 385 | --- 386 | ##### 场景一: 对象被一个堆对象删除引用,成为栈对象的下游 387 | 388 | > 伪代码 389 | 390 | ```go 391 | //前提:堆对象4->对象7 = 对象7; //对象7 被 对象4引用 392 | 栈对象1->对象7 = 堆对象7; //将堆对象7 挂在 栈对象1 下游 393 | 堆对象4->对象7 = null; //对象4 删除引用 对象7 394 | ``` 395 | ![](images/81-三色标记混合写屏障3.jpeg) 396 | 397 | ![](images/82-三色标记混合写屏障4.jpeg) 398 | 399 | 400 | 401 | ##### 场景二: 对象被一个栈对象删除引用,成为另一个栈对象的下游 402 | 403 | > 伪代码 404 | 405 | ```go 406 | new 栈对象9; 407 | 对象8->对象3 = 对象3; //将栈对象3 挂在 栈对象9 下游 408 | 对象2->对象3 = null; //对象2 删除引用 对象3 409 | ``` 410 | 411 | ![](images/83-三色标记混合写屏障5.jpeg) 412 | 413 | ![](images/84-三色标记混合写屏障6.jpeg) 414 | 415 | ![](images/85-三色标记混合写屏障7.jpeg) 416 | 417 | 418 | ##### 场景三:对象被一个堆对象删除引用,成为另一个堆对象的下游 419 | 420 | > 伪代码 421 | 422 | ```go 423 | 堆对象10->对象7 = 堆对象7; //将堆对象7 挂在 堆对象10 下游 424 | 堆对象4->对象7 = null; //对象4 删除引用 对象7 425 | ``` 426 | 427 | 428 | ![](images/86-三色标记混合写屏障8.jpeg) 429 | 430 | ![](images/87-三色标记混合写屏障9.jpeg) 431 | 432 | ![](images/88-三色标记混合写屏障10.jpeg) 433 | 434 | 435 | 436 | 437 | ##### 场景四:对象从一个栈对象删除引用,成为另一个堆对象的下游 438 | 439 | > 伪代码 440 | 441 | ```go 442 | 堆对象10->对象7 = 堆对象7; //将堆对象7 挂在 堆对象10 下游 443 | 堆对象4->对象7 = null; //对象4 删除引用 对象7 444 | ``` 445 | 446 | ![](images/89-三色标记混合写屏障11.jpeg) 447 | 448 | 449 | ![](images/90-三色标记混合写屏障12.jpeg) 450 | 451 | ![](images/91-三色标记混合写屏障13.jpeg) 452 | 453 | 454 | ​ Golang中的混合写屏障满足`弱三色不变式`,结合了删除写屏障和插入写屏障的优点,只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,这个过程不需要STW,而标记结束后,因为栈在扫描后始终是黑色的,也无需再进行re-scan操作了,减少了STW的时间。 455 | 456 | 457 | 458 | #### 七、总结 459 | 460 | ​ 以上便是Golang的GC全部的标记-清除逻辑及场景演示全过程。 461 | 462 | GoV1.3- 普通标记清除法,整体过程需要启动STW,效率极低。 463 | 464 | GoV1.5- 三色标记法, 堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要STW),效率普通 465 | 466 | GoV1.8-三色标记法,混合写屏障机制, 栈空间不启动,堆空间启动。整个过程几乎不需要STW,效率较高。 467 | 468 | -------------------------------------------------------------------------------- /5、channel.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 五、channel 4 | 5 | ### (1)Channel读写特性(15字口诀) 6 | 7 | 首先,我们先复习一下Channel都有哪些特性? 8 | 9 | * 给一个 nil channel 发送数据,造成永远阻塞 10 | 11 | * 从一个 nil channel 接收数据,造成永远阻塞 12 | 13 | * 给一个已经关闭的 channel 发送数据,引起 panic 14 | 15 | * 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回 **一个零值 和 false** (ex: `val,ok:= <-ch1`) 16 | 17 | * 无缓冲的channel是同步的,而有缓冲的channel是非同步的 18 | 19 | 以上5个特性是死东西,也可以通过口诀来记忆:“读写**空**阻塞,写**关闭**异常,读**关闭空**零假”。 20 | 21 | ![](images/169-channel异常情况总结.png) 22 | 23 | 24 | > 执行下面的代码发生什么? 25 | 26 | > test17.go 27 | 28 | ```go 29 | package main 30 | 31 | import ( 32 | "fmt" 33 | "time" 34 | ) 35 | 36 | func main() { 37 | ch := make(chan int, 1000) 38 | go func() { 39 | for i := 0; i < 10; i++ { 40 | ch <- i 41 | } 42 | }() 43 | go func() { 44 | for { 45 | a, ok := <-ch 46 | if !ok { 47 | fmt.Println("close") 48 | return 49 | } 50 | fmt.Println("a: ", a) 51 | } 52 | }() 53 | close(ch) 54 | fmt.Println("ok") 55 | time.Sleep(time.Second * 100) 56 | } 57 | ``` 58 | 59 | 60 | 61 | 15字口诀:“读写**空**阻塞,写**关闭**异常,读**关闭空**零假”,往已经关闭的channel写入数据会panic的。因为main在开辟完两个goroutine之后,立刻关闭了ch, 结果: 62 | 63 | ``` 64 | panic: send on closed channel 65 | ``` -------------------------------------------------------------------------------- /5、单点Server的N种并发模型汇总.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 5、单点Server的N种并发模型汇总 4 | 5 | 本文主要介绍常见的Server的并发模型,这些模型与编程语言本身无关,有的编程语言可能在语法上直接透明了模型本质,所以开发者没必要一定要基于模型去编写,只是需要知道和了解并发模型的构成和特点即可。 6 | 7 | 8 | 9 | 那么在了解并发模型之前,我们需要两个必备的前置知识: 10 | 11 | * socket网络编程 12 | * 多路IO复用机制 13 | * 多线程/多进程等并发编程理论 14 | 15 | 16 | 17 | 18 | 19 | ### 模型一、单线程Accept(无IO复用) 20 | 21 | #### (1) 模型结构图 22 | ![](images/158-并发模型1.jpeg) 23 | 24 | #### (2) 模型分析 25 | 26 | ① 主线程`main thread`执行阻塞Accept,每次客户端Connect链接过来,`main thread`中accept响应并建立连接 27 | 28 | ② 创建链接成功,得到`Connfd1`套接字后, 依然在`main thread`串行处理套接字读写,并处理业务。 29 | 30 | ③ 在②处理业务中,如果有新客户端`Connect`过来,`Server`无响应,直到当前套接字全部业务处理完毕。 31 | 32 | ④ 当前客户端处理完后,完毕链接,处理下一个客户端请求。 33 | 34 | 35 | 36 | #### (3) 优缺点 37 | 38 | **优点**: 39 | 40 | * socket编程流程清晰且简单,适合学习使用,了解socket基本编程流程。 41 | 42 | **缺点**: 43 | 44 | * 该模型并非并发模型,是串行的服务器,同一时刻,监听并响应最大的网络请求量为`1`。 即并发量为`1`。 45 | 46 | * 仅适合学习基本socket编程,不适合任何服务器Server构建。 47 | 48 | 49 | 50 | ### 模型二、单线程Accept+多线程读写业务(无IO复用) 51 | 52 | #### (1) 模型结构图 53 | ![](images/159-并发模型2.jpeg) 54 | 55 | #### (2) 模型分析 56 | 57 | ① 主线程`main thread`执行阻塞Accept,每次客户端Connect链接过来,`main thread`中accept响应并建立连接 58 | 59 | ② 创建链接成功,得到`Connfd1`套接字后,创建一个新线程`thread1`用来处理客户端的读写业务。`main thead`依然回到`Accept`阻塞等待新客户端。 60 | 61 | ③ `thread1`通过套接字`Connfd1`与客户端进行通信读写。 62 | 63 | ④ server在②处理业务中,如果有新客户端`Connect`过来,`main thread`中`Accept`依然响应并建立连接,重复②过程。 64 | 65 | 66 | 67 | #### (3) 优缺点 68 | 69 | **优点**: 70 | 71 | * 基于`模型一:单线程Accept(无IO复用)` 支持了并发的特性。 72 | * 使用灵活,一个客户端对应一个线程单独处理,`server`处理业务内聚程度高,客户端无论如何写,服务端均会有一个线程做资源响应。 73 | 74 | **缺点**: 75 | 76 | * 随着客户端的数量增多,需要开辟的线程也增加,客户端与server线程数量`1:1`正比关系,一次对于高并发场景,线程数量收到硬件上限瓶颈。 77 | * 对于长链接,客户端一旦无业务读写,只要不关闭,server的对应线程依然需要保持连接(心跳、健康监测等机制),占用连接资源和线程开销资源浪费。 78 | * 仅适合客户端数量不大,并且数量可控的场景使用。 79 | 80 | 仅适合学习基本socket编程,不适合任何服务器Server构建。 81 | 82 | 83 | 84 | 85 | 86 | ### 模型三、单线程多路IO复用 87 | 88 | #### (1) 模型结构图 89 | ![](images/160-并发模型3.jpeg) 90 | 91 | #### (2) 模型分析 92 | 93 | ① 主线程`main thread`创建`listenFd`之后,采用多路I/O复用机制(如:select、epoll)进行IO状态阻塞监控。有`Client1`客户端`Connect`请求,I/O复用机制检测到`ListenFd`触发读事件,则进行`Accept`建立连接,并将新生成的`connFd1`加入到`监听I/O集合`中。 94 | 95 | ② `Client1`再次进行正常读写业务请求,`main thread`的`多路I/O复用机制`阻塞返回,会触该套接字的读/写事件等。 96 | 97 | ③ 对于`Client1`的读写业务,Server依然在`main thread`执行流程提继续执行,此时如果有新的客户端`Connect`链接请求过来,Server将没有即时响应。 98 | 99 | ④ 等到Server处理完一个连接的`Read+Write`操作,继续回到`多路I/O复用机制`阻塞,其他链接过来重复 ②、③流程。 100 | 101 | 102 | 103 | #### (3) 优缺点 104 | 105 | **优点**: 106 | 107 | * 单流程解决了可以同时监听多个客户端读写状态的模型,不需要`1:1`与客户端的线程数量关系。 108 | * 多路I/O复用阻塞,非忙询状态,不浪费CPU资源, CPU利用率较高。 109 | 110 | **缺点**: 111 | 112 | * 虽然可以监听多个客户端的读写状态,但是同一时间内,只能处理一个客户端的读写操作,实际上读写的业务并发为1。 113 | * 多客户端访问Server,业务为串行执行,大量请求会有排队延迟现象,如图中⑤所示,当`Client3`占据`main thread`流程时,`Client1,Client2`流程卡在`IO复用`等待下次监听触发事件。 114 | 115 | 116 | 117 | 118 | 119 | ### 模型四、单线程多路IO复用+多线程读写业务(业务工作池) 120 | 121 | #### (1) 模型结构图 122 | ![](images/161-并发模型4.jpeg) 123 | 124 | #### (2) 模型分析 125 | 126 | ① 主线程`main thread`创建`listenFd`之后,采用多路I/O复用机制(如:select、epoll)进行IO状态阻塞监控。有`Client1`客户端`Connect`请求,I/O复用机制检测到`ListenFd`触发读事件,则进行`Accept`建立连接,并将新生成的`connFd1`加入到`监听I/O集合`中。 127 | 128 | ② 当`connFd1`有可读消息,触发读事件,并且进行读写消息 129 | 130 | ③ `main thread`按照固定的协议读取消息,并且交给`worker pool`工作线程池, 工作线程池在server启动之前就已经开启固定数量的`thread`,里面的线程只处理消息业务,不进行套接字读写操作。 131 | 132 | ④ 工作池处理完业务,触发`connFd1`写事件,将回执客户端的消息通过`main thead`写给对方。 133 | 134 | 135 | 136 | ##### (3) 优缺点 137 | 138 | **优点**: 139 | 140 | * 对于`模型三`, 将业务处理部分,通过工作池分离出来,减少多客户端访问Server,业务为串行执行,大量请求会有排队延迟时间。 141 | * 实际上读写的业务并发为1,但是业务流程并发为worker pool线程数量,加快了业务处理并行效率。 142 | 143 | **缺点**: 144 | 145 | * 读写依然为`main thread`单独处理,最高读写并行通道依然为1. 146 | * 虽然多个worker线程处理业务,但是最后返回给客户端,依旧需要排队,因为出口还是`main thread`的`Read + Write` 147 | 148 | 149 | 150 | 151 | 152 | ### 模型五、单线程IO复用+多线程IO复用(链接线程池) 153 | 154 | #### (1) 模型结构图 155 | ![](images/162-并发模型5.jpeg) 156 | 157 | #### (2) 模型分析 158 | 159 | ① Server在启动监听之前,开辟固定数量(N)的线程,用`Thead Pool`线程池管理 160 | 161 | ② 主线程`main thread`创建`listenFd`之后,采用多路I/O复用机制(如:select、epoll)进行IO状态阻塞监控。有`Client1`客户端`Connect`请求,I/O复用机制检测到`ListenFd`触发读事件,则进行`Accept`建立连接,并将新生成的`connFd1`分发给`Thread Pool`中的某个线程进行监听。 162 | 163 | ③ `Thread Pool`中的每个`thread`都启动`多路I/O复用机制(select、epoll)`,用来监听`main thread`建立成功并且分发下来的socket套接字。 164 | 165 | ④ 如图, `thread`监听`ConnFd1、ConnFd2`, `thread2`监听`ConnFd3`,`thread3`监听`ConnFd4`. 当对应的`ConnFd`有读写事件,对应的线程处理该套接字的读写及业务。 166 | 167 | 168 | 169 | #### (3) 优缺点 170 | 171 | **优点**: 172 | 173 | * 将`main thread`的单流程读写,分散到多线程完成,这样增加了同一时刻的读写并行通道,并行通道数量`N`, `N`为线程池`Thread`数量。 174 | * server同时监听的`ConnFd套接字`数量几乎成倍增大,之前的全部监控数量取决于`main thread`的`多路I/O复用机制`的最大限制***(select 默认为1024, epoll默认与内存大小相关,约3~6w不等)***,所以理论单点Server最高响应并发数量为`N*(3~6W)`(`N`为线程池`Thread`数量,建议与CPU核心成比例1:1)。 175 | * 如果良好的线程池数量和CPU核心数适配,那么可以尝试CPU核心与Thread进行绑定,从而降低CPU的切换频率,提升每个`Thread`处理合理业务的效率,降低CPU切换成本开销。 176 | 177 | **缺点**: 178 | 179 | * 虽然监听的并发数量提升,但是最高读写并行通道依然为`N`,而且多个身处同一个Thread的客户端,会出现读写延迟现象,实际上每个`Thread`的模型特征与`模型三:单线程多路IO复用`一致。 180 | 181 | 182 | 183 | ### 模型五(进程版)、**单进程多路I/O复用+多进程多路I/O复用(进程池)** 184 | 185 | 186 | 187 | #### (1) 模型结构图 188 | ![](images/163-并发模型6.jpeg) 189 | 190 | #### (2) 模型分析 191 | 192 | 与`五、单线程IO复用+多线程IO复用(链接线程池)`无大差异。 193 | 194 | 195 | 196 | 不同处 197 | 198 | * 进程和线程的内存布局不同导致,`main process`(主进程)不再进行`Accept`操作,而是将`Accept`过程分散到各个`子进程(process)`中. 199 | * 进程的特性,资源独立,所以`main process`如果Accept成功的fd,其他进程无法共享资源,所以需要各子进程自行Accept创建链接 200 | * `main process`只是监听`ListenFd`状态,一旦触发读事件(有新连接请求). 通过一些IPC(进程间通信:如信号、共享内存、管道)等, 让各自子进程`Process`竞争`Accept`完成链接建立,并各自监听。 201 | 202 | 203 | #### (3) 优缺点 204 | 205 | 与`五、单线程IO复用+多线程IO复用(链接线程池)`无大差异。 206 | 207 | 208 | 209 | 不同处: 210 | 211 | 多进程内存资源空间占用稍微大一些 212 | 213 | 多进程模型安全稳定型较强,这也是因为各自进程互不干扰的特点导致。 214 | 215 | 216 | 217 | ### 模型六、**单线程多路I/O复用+多线程多路I/O复用+多线程** 218 | 219 | #### (1) 模型结构图 220 | ![](images/164-并发模型7.jpeg) 221 | 222 | #### (2) 模型分析 223 | 224 | ① Server在启动监听之前,开辟固定数量(N)的线程,用`Thead Pool`线程池管理 225 | 226 | ② 主线程`main thread`创建`listenFd`之后,采用多路I/O复用机制(如:select、epoll)进行IO状态阻塞监控。有`Client1`客户端`Connect`请求,I/O复用机制检测到`ListenFd`触发读事件,则进行`Accept`建立连接,并将新生成的`connFd1`分发给`Thread Pool`中的某个线程进行监听。 227 | 228 | ③ `Thread Pool`中的每个`thread`都启动`多路I/O复用机制(select、epoll)`,用来监听`main thread`建立成功并且分发下来的socket套接字。一旦其中某个被监听的客户端套接字触发`I/O读写事件`,那么,会立刻开辟一个新线程来处理`I/O读写`业务。 229 | 230 | ④ 但某个读写线程完成当前读写业务,如果当前套接字没有被关闭,那么将当前客户端套接字`如:ConnFd3`重新加回线程池的监控线程中,同时自身线程自我销毁。 231 | 232 | 233 | 234 | #### (3) 优缺点 235 | 236 | **优点**: 237 | 238 | * 在`模型五、单线程IO复用+多线程IO复用(链接线程池)`基础上,除了能够保证同时响应的`最高并发数`,又能解决`读写并行通道`局限的问题。 239 | 240 | * 同一时刻的读写并行通道,达到`最大化极限`,一个客户端可以对应一个单独执行流程处理读写业务,读写并行通道与客户端数量`1:1`关系。 241 | 242 | **缺点**: 243 | 244 | * 该模型过于理想化,因为要求CPU核心数量足够大。 245 | * 如果硬件CPU数量可数(目前的硬件情况),那么该模型将造成大量的CPU切换成本浪费。因为为了保证读写并行通道与客户端`1:1`的关系,那么Server需要开辟的`Thread`数量就与客户端一致,那么线程池中做`多路I/O复用`的监听线程池绑定CPU数量将变得毫无意义。 246 | * 如果每个临时的读写`Thread`都能够绑定一个单独的CPU,那么此模型将是最优模型。但是目前CPU的数量无法与客户端的数量达到一个量级,目前甚至差的不是几个量级的事。 247 | 248 | 249 | 250 | 251 | 252 | ### 总结 253 | 254 | 综上,我们整理了7中Server的服务器处理结构模型,每个模型都有各自的特点和优势,那么对于多少应付高并发和高CPU利用率的模型,目前多数采用的是模型五(或模型五进程版,如Nginx就是类似模型五进程版的改版)。 255 | 256 | 257 | 258 | 至于并发模型并非设计的约复杂越好,也不是线程开辟的越多越好,我们要考虑硬件的利用与和切换成本的开销。模型六设计就极为复杂,线程较多,但以当今的硬件能力无法支撑,反倒导致该模型性能极差。所以对于不同的业务场景也要选择适合的模型构建,并不是一定固定就要使用某个来应用。 259 | 260 | 261 | -------------------------------------------------------------------------------- /6、TCP中TIME_WAIT状态意义详解.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 6、为什么需要TIME_WAIT 4 | 5 | 6 | ### 一、何为TIME_WAIT? 7 | 8 | 我们在日常做服务器的研发中、或者面试网络部分知识的时候,会经常问到TIME_WAIT这个词,这个词作为服务端的开发者尤为重要。TIME_WAIT是TCP协议中断开连接所经历的一种状态。 9 | 10 | ![](images/165-timewait1.png) 11 | 12 | 13 | 14 | 15 | ​ 上图是TCP连接的状态转换,包括了一些触发条件,如果不是很直观,可以对比看下面的简图。 16 | 17 | ![](images/166-timewait2.png) 18 | 19 | ​ 这里面作为主动关闭的一方(Client)出现了`TIME_WAIT`状态,目的是告诉Server端,**自己没有需要发送的数据**,但是它仍然**保持了接收对方数据的能力**,一个常见的关闭连接过程如下: 20 | 21 | 22 | 23 | 1、当客户端没有待发送的数据时,它会向服务端发送 `FIN` 消息,发送消息后会进入 `FIN_WAIT_1` 状态; 24 | 25 | 2、服务端接收到客户端的 `FIN` 消息后,会进入 `CLOSE_WAIT` 状态并向客户端发送 `ACK` 消息,客户端接收到 `ACK` 消息时会进入 `FIN_WAIT_2` 状态; 26 | 27 | 3、当服务端没有待发送的数据时,服务端会向客户端发送 `FIN` 消息; 28 | 29 | 4、客户端接收到 `FIN` 消息后,会进入 `TIME_WAIT` 状态并向服务端发送 `ACK` 消息,服务端收到后会进入 `CLOSED` 状态; 30 | 31 | 5、客户端等待**两个最大数据段生命周期**(Maximum segment lifetime,MSL)的时间后也会进入 `CLOSED` 状态; 32 | 33 | --- 34 | 35 | ### 二、为什么需要TIME_WAIT 36 | 37 | **TIME_WAIT一定是发生在主动关闭一方** 38 | 39 | 被动关闭一方,会直接进入`CLOSED`状态,而主动关闭一方需要等待2*MSL时间才会最终关闭。 40 | 41 | 原因: 42 | 43 | 1、防止被动关闭方的延迟数据被人窃取 44 | 45 | 2、防止被动关闭方没有收到最后的ACK 46 | 47 | 48 | 49 | #### 原因一:防止被动关闭方的延迟数据被人窃取 50 | 51 | 52 | ![](images/167-timewait3.png) 53 | 如上图所示, 54 | 55 | 1、在①中,服务端发送`seq=1001`的消息,由于网络延迟或其他原因,没有及时到达`Client1`客户端,导致整个包一直存留在网络环境的传输过程中。 56 | 57 | 2、在②中,`Client1`收到server的`FIN`包之后,变成了`TIME_WAIT`状态,这里假设`TIME_WAIT`等待的时间很短暂,那么,还没等之前的那个延迟包`seq=1001`到来,就回复给了`Server`最后一个`ACK`包。那么`Server`就会变成`CLOSED`状态。 58 | 59 | 3、在③中,相同的端口号的`Client2`的TCP链接被重用后 60 | 61 | 4、在④中,`seq=1001`的延迟包消息才发送给客户端,而这个延迟的消息却被`Client2`正常接收,主要就会给Client2带来严重的问题。所以`TIME_WAIT`不要轻易的调整,或者缩小时间,可能就会出现这种问题。 62 | 63 | 64 | 65 | #### 原因二:防止被动关闭方没有收到最后的ACK 66 | 67 | ​ 该作用就是等待足够长的时间以确定远程的TCP链接收到了其发出的终止链接消息`FIN`包的回执消息`ACK`包。 68 | ![](images/168-timewait4.png) 69 | 70 | ​ 如上图所示: 71 | 72 | 1、在①中,`CLient1`端主动发起关闭链接,`Server`针对`Client1`的`FIN`回执了`ACK`包,然后接着发送了自己的`FIN`包,等待`Client1`回执最终的`ACK`包。 73 | 74 | 2、在②中,这里假设`TIME_WAIT`的时间不足够充分,当`Server`还没有收到 `ACK` 消息时,`Client1`就主动变成`CLOSED`状态。 75 | 76 | 3、在③中,由于`Server`一直没有等到自己`FIN`包的`ACK`应答包,导致一直处于`LAST_ACK`状态。 77 | 78 | 4、在④中,因为 服务端因为没有收到 `ACK` 消息,当`Client2`重新与`Server`建立TCP链接,认为当前连接是合法的,`CLient2`重新发送 `SYN` 消息请求握手时会收到`Server`的 `RST` 消息,连接建立的过程就会被终止。 79 | 80 | 81 | 82 | 所以,我们在默认情况下,如果客户端等待足够长的时间就会遇到以下两种情况: 83 | 84 | 1. 服务端正常收到了 `ACK` 消息并关闭当前 TCP 连接; 85 | 2. 服务端没有收到 `ACK` 消息,重新发送 `FIN` 关闭连接并等待新的 `ACK` 消息; 86 | 87 | 只要客户端等待 2 MSL 的时间,客户端和服务端之间的连接就会正常关闭,新创建的 TCP 连接收到影响的概率也微乎其微,保证了数据传输的可靠性。 88 | 89 | -------------------------------------------------------------------------------- /6、WaitGroup.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 六、WaitGroup 4 | 5 | ### (1) WaitGroup与goroutine的竞速问题 6 | 7 | > 编译并运行如下代码会发生什么? 8 | 9 | > test18.go 10 | 11 | ```go 12 | package main 13 | 14 | import ( 15 | "sync" 16 | //"time" 17 | ) 18 | 19 | const N = 10 20 | 21 | var wg = &sync.WaitGroup{} 22 | 23 | func main() { 24 | 25 | for i := 0; i < N; i++ { 26 | go func(i int) { 27 | wg.Add(1) 28 | println(i) 29 | defer wg.Done() 30 | }(i) 31 | } 32 | wg.Wait() 33 | 34 | } 35 | ``` 36 | 37 | **结果** 38 | 39 | ```bash 40 | 结果不唯一,代码存在风险, 所有go未必都能执行到 41 | ``` 42 | 43 | 44 | 45 | 这是使用WaitGroup经常犯下的错误!请各位同学多次运行就会发现输出都会不同甚至又出现报错的问题。 这是因为`go`执行太快了,导致`wg.Add(1)`还没有执行main函数就执行完毕了。 改为如下试试 46 | 47 | ```go 48 | package main 49 | 50 | import ( 51 | "sync" 52 | ) 53 | 54 | const N = 10 55 | 56 | var wg = &sync.WaitGroup{} 57 | 58 | func main() { 59 | 60 | for i:= 0; i< N; i++ { 61 | wg.Add(1) 62 | go func(i int) { 63 | println(i) 64 | defer wg.Done() 65 | }(i) 66 | } 67 | 68 | wg.Wait() 69 | } 70 | 71 | ``` -------------------------------------------------------------------------------- /6、面向对象的编程思维理解interface.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 6、面向对象的编程思维理解interface。 4 | 5 | ### 一、 interface接口 6 | 7 |   interface 是GO语言的基础特性之一。可以理解为一种类型的规范或者约定。它跟java,C# 不太一样,不需要显示说明实现了某个接口,它没有继承或子类或“implements”关键字,只是通过约定的形式,隐式的实现interface 中的方法即可。因此,Golang 中的 interface 让编码更灵活、易扩展。 8 | 9 | 如何理解go 语言中的interface ? 只需记住以下三点即可: 10 | 11 | 1. interface 是方法声明的集合 12 | 2. 任何类型的对象实现了在interface 接口中声明的全部方法,则表明该类型实现了该接口。 13 | 3. interface 可以作为一种数据类型,实现了该接口的任何对象都可以给对应的接口类型变量赋值。 14 | 15 | 16 | >注意: 17 | >  a. interface 可以被任意对象实现,一个类型/对象也可以实现多个 interface 18 | >  b. 方法不能重载,如 `eat(), eat(s string)` 不能同时存在 19 | 20 | 21 | 22 | ```go 23 | package main 24 | 25 | import "fmt" 26 | 27 | type Phone interface { 28 | call() 29 | } 30 | 31 | type NokiaPhone struct { 32 | } 33 | 34 | func (nokiaPhone NokiaPhone) call() { 35 | fmt.Println("I am Nokia, I can call you!") 36 | } 37 | 38 | type ApplePhone struct { 39 | } 40 | 41 | func (iPhone ApplePhone) call() { 42 | fmt.Println("I am Apple Phone, I can call you!") 43 | } 44 | 45 | func main() { 46 | var phone Phone 47 | phone = new(NokiaPhone) 48 | phone.call() 49 | 50 | phone = new(ApplePhone) 51 | phone.call() 52 | } 53 | ``` 54 | 55 | 上述中体现了`interface`接口的语法,在`main`函数中,也体现了`多态`的特性。 56 | 同样一个`phone`的抽象接口,分别指向不同的实体对象,调用的call()方法,打印的效果不同,那么就是体现出了多态的特性。 57 | 58 | 59 | 60 | ### 二、 面向对象中的开闭原则 61 | 62 | #### 2.1 平铺式的模块设计 63 | 64 | 那么作为`interface`数据类型,他存在的意义在哪呢? 实际上是为了满足一些面向对象的编程思想。我们知道,软件设计的最高目标就是`高内聚,低耦合`。那么其中有一个设计原则叫`开闭原则`。什么是开闭原则呢,接下来我们看一个例子: 65 | 66 | ```go 67 | package main 68 | 69 | import "fmt" 70 | 71 | //我们要写一个类,Banker银行业务员 72 | type Banker struct { 73 | } 74 | 75 | //存款业务 76 | func (this *Banker) Save() { 77 | fmt.Println( "进行了 存款业务...") 78 | } 79 | 80 | //转账业务 81 | func (this *Banker) Transfer() { 82 | fmt.Println( "进行了 转账业务...") 83 | } 84 | 85 | //支付业务 86 | func (this *Banker) Pay() { 87 | fmt.Println( "进行了 支付业务...") 88 | } 89 | 90 | func main() { 91 | banker := &Banker{} 92 | 93 | banker.Save() 94 | banker.Transfer() 95 | banker.Pay() 96 | } 97 | 98 | ``` 99 | 100 | 代码很简单,就是一个银行业务员,他可能拥有很多的业务,比如`Save()`存款、`Transfer()`转账、`Pay()`支付等。那么如果这个业务员模块只有这几个方法还好,但是随着我们的程序写的越来越复杂,银行业务员可能就要增加方法,会导致业务员模块越来越臃肿。 101 | ![](images/40-平铺设计.png) 102 | 103 | 104 | 105 | ​ 这样的设计会导致,当我们去给Banker添加新的业务的时候,会直接修改原有的Banker代码,那么Banker模块的功能会越来越多,出现问题的几率也就越来越大,假如此时Banker已经有99个业务了,现在我们要添加第100个业务,可能由于一次的不小心,导致之前99个业务也一起崩溃,因为所有的业务都在一个Banker类里,他们的耦合度太高,Banker的职责也不够单一,代码的维护成本随着业务的复杂正比成倍增大。 106 | 107 | 108 | 109 | #### 2.2 开闭原则设计 110 | 111 | 那么,如果我们拥有接口, `interface`这个东西,那么我们就可以抽象一层出来,制作一个抽象的Banker模块,然后提供一个抽象的方法。 分别根据这个抽象模块,去实现`支付Banker(实现支付方法)`,`转账Banker(实现转账方法)` 112 | 如下: 113 | ![](images/41-开闭设计.png) 114 | 115 | 那么依然可以搞定程序的需求。 然后,当我们想要给Banker添加额外功能的时候,之前我们是直接修改Banker的内容,现在我们可以单独定义一个`股票Banker(实现股票方法)`,到这个系统中。 而且股票Banker的实现成功或者失败都不会影响之前的稳定系统,他很单一,而且独立。 116 | 117 | 所以以上,当我们给一个系统添加一个功能的时候,不是通过修改代码,而是通过增添代码来完成,那么就是开闭原则的核心思想了。所以要想满足上面的要求,是一定需要interface来提供一层抽象的接口的。 118 | 119 | golang代码实现如下: 120 | 121 | ```go 122 | package main 123 | 124 | import "fmt" 125 | 126 | //抽象的银行业务员 127 | type AbstractBanker interface{ 128 | DoBusi() //抽象的处理业务接口 129 | } 130 | 131 | //存款的业务员 132 | type SaveBanker struct { 133 | //AbstractBanker 134 | } 135 | 136 | func (sb *SaveBanker) DoBusi() { 137 | fmt.Println("进行了存款") 138 | } 139 | 140 | //转账的业务员 141 | type TransferBanker struct { 142 | //AbstractBanker 143 | } 144 | 145 | func (tb *TransferBanker) DoBusi() { 146 | fmt.Println("进行了转账") 147 | } 148 | 149 | //支付的业务员 150 | type PayBanker struct { 151 | //AbstractBanker 152 | } 153 | 154 | func (pb *PayBanker) DoBusi() { 155 | fmt.Println("进行了支付") 156 | } 157 | 158 | 159 | func main() { 160 | //进行存款 161 | sb := &SaveBanker{} 162 | sb.DoBusi() 163 | 164 | //进行转账 165 | tb := &TransferBanker{} 166 | tb.DoBusi() 167 | 168 | //进行支付 169 | pb := &PayBanker{} 170 | pb.DoBusi() 171 | 172 | } 173 | ``` 174 | 175 | 当然我们也可以根据`AbstractBanker`设计一个小框架 176 | 177 | ```go 178 | //实现架构层(基于抽象层进行业务封装-针对interface接口进行封装) 179 | func BankerBusiness(banker AbstractBanker) { 180 | //通过接口来向下调用,(多态现象) 181 | banker.DoBusi() 182 | } 183 | ``` 184 | 185 | 那么main中可以如下实现业务调用: 186 | 187 | ```go 188 | func main() { 189 | //进行存款 190 | BankerBusiness(&SaveBanker{}) 191 | 192 | //进行存款 193 | BankerBusiness(&TransferBanker{}) 194 | 195 | //进行存款 196 | BankerBusiness(&PayBanker{}) 197 | } 198 | ``` 199 | 200 | >再看开闭原则定义: 201 | >开闭原则:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。 202 | >简单的说就是在修改需求的时候,应该尽量通过扩展来实现变化,而不是通过修改已有代码来实现变化。 203 | 204 | 205 | 206 | ### 三、 接口的意义 207 | 208 | 好了,现在interface已经基本了解,那么接口的意义最终在哪里呢,想必现在你已经有了一个初步的认知,实际上接口的最大的意义就是实现多态的思想,就是我们可以根据interface类型来设计API接口,那么这种API接口的适应能力不仅能适应当下所实现的全部模块,也适应未来实现的模块来进行调用。 `调用未来`可能就是接口的最大意义所在吧,这也是为什么架构师那么值钱,因为良好的架构师是可以针对interface设计一套框架,在未来许多年却依然适用。 209 | 210 | ### 四、 面向对象中的依赖倒转原则 211 | 212 | #### 4.1 耦合度极高的模块关系设计 213 | ![](images/42-混乱的依赖关系.png) 214 | 215 | ```go 216 | package main 217 | 218 | import "fmt" 219 | 220 | // === > 奔驰汽车 <=== 221 | type Benz struct { 222 | 223 | } 224 | 225 | func (this *Benz) Run() { 226 | fmt.Println("Benz is running...") 227 | } 228 | 229 | // === > 宝马汽车 <=== 230 | type BMW struct { 231 | 232 | } 233 | 234 | func (this *BMW) Run() { 235 | fmt.Println("BMW is running ...") 236 | } 237 | 238 | 239 | //===> 司机张三 <=== 240 | type Zhang3 struct { 241 | //... 242 | } 243 | 244 | func (zhang3 *Zhang3) DriveBenZ(benz *Benz) { 245 | fmt.Println("zhang3 Drive Benz") 246 | benz.Run() 247 | } 248 | 249 | func (zhang3 *Zhang3) DriveBMW(bmw *BMW) { 250 | fmt.Println("zhang3 drive BMW") 251 | bmw.Run() 252 | } 253 | 254 | //===> 司机李四 <=== 255 | type Li4 struct { 256 | //... 257 | } 258 | 259 | func (li4 *Li4) DriveBenZ(benz *Benz) { 260 | fmt.Println("li4 Drive Benz") 261 | benz.Run() 262 | } 263 | 264 | func (li4 *Li4) DriveBMW(bmw *BMW) { 265 | fmt.Println("li4 drive BMW") 266 | bmw.Run() 267 | } 268 | 269 | func main() { 270 | //业务1 张3开奔驰 271 | benz := &Benz{} 272 | zhang3 := &Zhang3{} 273 | zhang3.DriveBenZ(benz) 274 | 275 | //业务2 李四开宝马 276 | bmw := &BMW{} 277 | li4 := &Li4{} 278 | li4.DriveBMW(bmw) 279 | } 280 | 281 | 282 | ``` 283 | 284 | 我们来看上面的代码和图中每个模块之间的依赖关系,实际上并没有用到任何的`interface`接口层的代码,显然最后我们的两个业务 `张三开奔驰`, `李四开宝马`,程序中也都实现了。但是这种设计的问题就在于,小规模没什么问题,但是一旦程序需要扩展,比如我现在要增加一个`丰田汽车` 或者 司机`王五`, 那么模块和模块的依赖关系将成指数级递增,想蜘蛛网一样越来越难维护和捋顺。 285 | 286 | #### 4.2 面向抽象层依赖倒转 287 | ![](images/43-依赖倒转设计.png) 288 | 289 | 如上图所示,如果我们在设计一个系统的时候,将模块分为3个层次,抽象层、实现层、业务逻辑层。那么,我们首先将抽象层的模块和接口定义出来,这里就需要了`interface`接口的设计,然后我们依照抽象层,依次实现每个实现层的模块,在我们写实现层代码的时候,实际上我们只需要参考对应的抽象层实现就好了,实现每个模块,也和其他的实现的模块没有关系,这样也符合了上面介绍的开闭原则。这样实现起来每个模块只依赖对象的接口,而和其他模块没关系,依赖关系单一。系统容易扩展和维护。 290 | 291 | 我们在指定业务逻辑也是一样,只需要参考抽象层的接口来业务就好了,抽象层暴露出来的接口就是我们业务层可以使用的方法,然后可以通过多态的线下,接口指针指向哪个实现模块,调用了就是具体的实现方法,这样我们业务逻辑层也是依赖抽象成编程。 292 | 293 | 我们就将这种的设计原则叫做`依赖倒转原则`。 294 | 295 | 来一起看一下修改的代码: 296 | 297 | ```go 298 | package main 299 | 300 | import "fmt" 301 | 302 | // ===== > 抽象层 < ======== 303 | type Car interface { 304 | Run() 305 | } 306 | 307 | type Driver interface { 308 | Drive(car Car) 309 | } 310 | 311 | // ===== > 实现层 < ======== 312 | type BenZ struct { 313 | //... 314 | } 315 | 316 | func (benz * BenZ) Run() { 317 | fmt.Println("Benz is running...") 318 | } 319 | 320 | type Bmw struct { 321 | //... 322 | } 323 | 324 | func (bmw * Bmw) Run() { 325 | fmt.Println("Bmw is running...") 326 | } 327 | 328 | type Zhang_3 struct { 329 | //... 330 | } 331 | 332 | func (zhang3 *Zhang_3) Drive(car Car) { 333 | fmt.Println("Zhang3 drive car") 334 | car.Run() 335 | } 336 | 337 | type Li_4 struct { 338 | //... 339 | } 340 | 341 | func (li4 *Li_4) Drive(car Car) { 342 | fmt.Println("li4 drive car") 343 | car.Run() 344 | } 345 | 346 | 347 | // ===== > 业务逻辑层 < ======== 348 | func main() { 349 | //张3 开 宝马 350 | var bmw Car 351 | bmw = &Bmw{} 352 | 353 | var zhang3 Driver 354 | zhang3 = &Zhang_3{} 355 | 356 | zhang3.Drive(bmw) 357 | 358 | //李4 开 奔驰 359 | var benz Car 360 | benz = &BenZ{} 361 | 362 | var li4 Driver 363 | li4 = &Li_4{} 364 | 365 | li4.Drive(benz) 366 | } 367 | ``` 368 | 369 | 370 | 371 | #### 4.3 依赖倒转小练习 372 | 373 | > 模拟组装2台电脑, 374 | > --- 抽象层 ---有显卡Card 方法display,有内存Memory 方法storage,有处理器CPU 方法calculate 375 | > --- 实现层层 ---有 Intel因特尔公司 、产品有(显卡、内存、CPU),有 Kingston 公司, 产品有(内存3),有 NVIDIA 公司, 产品有(显卡) 376 | > --- 逻辑层 ---1. 组装一台Intel系列的电脑,并运行,2. 组装一台 Intel CPU Kingston内存 NVIDIA显卡的电脑,并运行 377 | 378 | ```go 379 | /* 380 | 模拟组装2台电脑 381 | --- 抽象层 --- 382 | 有显卡Card 方法display 383 | 有内存Memory 方法storage 384 | 有处理器CPU 方法calculate 385 | 386 | --- 实现层层 --- 387 | 有 Intel因特尔公司 、产品有(显卡、内存、CPU) 388 | 有 Kingston 公司, 产品有(内存3) 389 | 有 NVIDIA 公司, 产品有(显卡) 390 | 391 | --- 逻辑层 --- 392 | 1. 组装一台Intel系列的电脑,并运行 393 | 2. 组装一台 Intel CPU Kingston内存 NVIDIA显卡的电脑,并运行 394 | */ 395 | package main 396 | 397 | import "fmt" 398 | 399 | //------ 抽象层 ----- 400 | type Card interface{ 401 | Display() 402 | } 403 | 404 | type Memory interface { 405 | Storage() 406 | } 407 | 408 | type CPU interface { 409 | Calculate() 410 | } 411 | 412 | type Computer struct { 413 | cpu CPU 414 | mem Memory 415 | card Card 416 | } 417 | 418 | func NewComputer(cpu CPU, mem Memory, card Card) *Computer{ 419 | return &Computer{ 420 | cpu:cpu, 421 | mem:mem, 422 | card:card, 423 | } 424 | } 425 | 426 | func (this *Computer) DoWork() { 427 | this.cpu.Calculate() 428 | this.mem.Storage() 429 | this.card.Display() 430 | } 431 | 432 | //------ 实现层 ----- 433 | //intel 434 | type IntelCPU struct { 435 | CPU 436 | } 437 | 438 | func (this *IntelCPU) Calculate() { 439 | fmt.Println("Intel CPU 开始计算了...") 440 | } 441 | 442 | type IntelMemory struct { 443 | Memory 444 | } 445 | 446 | func (this *IntelMemory) Storage() { 447 | fmt.Println("Intel Memory 开始存储了...") 448 | } 449 | 450 | type IntelCard struct { 451 | Card 452 | } 453 | 454 | func (this *IntelCard) Display() { 455 | fmt.Println("Intel Card 开始显示了...") 456 | } 457 | 458 | //kingston 459 | type KingstonMemory struct { 460 | Memory 461 | } 462 | 463 | func (this *KingstonMemory) Storage() { 464 | fmt.Println("Kingston memory storage...") 465 | } 466 | 467 | //nvidia 468 | type NvidiaCard struct { 469 | Card 470 | } 471 | 472 | func (this *NvidiaCard) Display() { 473 | fmt.Println("Nvidia card display...") 474 | } 475 | 476 | 477 | 478 | //------ 业务逻辑层 ----- 479 | func main() { 480 | //intel系列的电脑 481 | com1 := NewComputer(&IntelCPU{}, &IntelMemory{}, &IntelCard{}) 482 | com1.DoWork() 483 | 484 | //杂牌子 485 | com2 := NewComputer(&IntelCPU{}, &KingstonMemory{}, &NvidiaCard{}) 486 | com2.DoWork() 487 | } 488 | ``` 489 | 490 | 491 | -------------------------------------------------------------------------------- /7、Golang中的Defer必掌握的7知识点.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 7、Golang中的Defer必掌握的7知识点 4 | 5 | ### 知识点1:defer的执行顺序 6 | 7 | 多个defer出现的时候,**它是一个“栈”的关系,也就是先进后出**。一个函数中,写在前面的defer会比写在后面的defer调用的晚。 8 | 9 | 10 | 11 | > 示例代码 12 | 13 | ```go 14 | package main 15 | 16 | import "fmt" 17 | 18 | func main() { 19 | defer func1() 20 | defer func2() 21 | defer func3() 22 | } 23 | 24 | func func1() { 25 | fmt.Println("A") 26 | } 27 | 28 | func func2() { 29 | fmt.Println("B") 30 | } 31 | 32 | func func3() { 33 | fmt.Println("C") 34 | } 35 | 36 | ``` 37 | 38 | 39 | ![](images/112-defer2.jpeg) 40 | 41 | 输出结果: 42 | 43 | ```bash 44 | C 45 | B 46 | A 47 | ``` 48 | 49 | --- 50 | 51 | ### 知识点2: defer与return谁先谁后 52 | 53 | > 示例代码 54 | 55 | ```go 56 | package main 57 | 58 | import "fmt" 59 | 60 | func deferFunc() int { 61 | fmt.Println("defer func called") 62 | return 0 63 | } 64 | 65 | func returnFunc() int { 66 | fmt.Println("return func called") 67 | return 0 68 | } 69 | 70 | func returnAndDefer() int { 71 | 72 | defer deferFunc() 73 | 74 | return returnFunc() 75 | } 76 | 77 | func main() { 78 | returnAndDefer() 79 | } 80 | ``` 81 | 82 | 执行结果为: 83 | 84 | ```bash 85 | return func called 86 | defer func called 87 | ``` 88 | 89 | 结论为:**return之后的语句先执行,defer后的语句后执行** 90 | 91 | --- 92 | 93 | ### 知识点3:函数的返回值初始化 94 | 95 | 该知识点不属于defer本身,但是调用的场景却与defer有联系,所以也算是defer必备了解的知识点之一。 96 | 97 | 如 : `func DeferFunc1(i int) (t int) {}` 98 | 其中返回值`t int`,这个`t`会在函数起始处被初始化为对应类型的零值并且作用域为整个函数。 99 | ![](images/111-defer1.png) 100 | 101 | > 示例代码 102 | 103 | ```go 104 | package main 105 | 106 | import "fmt" 107 | 108 | func DeferFunc1(i int) (t int) { 109 | 110 | fmt.Println("t = ", t) 111 | 112 | return 2 113 | } 114 | 115 | func main() { 116 | DeferFunc11(10) 117 | } 118 | 119 | ``` 120 | 121 | 结果 122 | 123 | ```bash 124 | t = 0 125 | ``` 126 | 127 | 证明,**只要声明函数的返回值变量名称,就会在函数初始化时候为之赋值为0,而且在函数体作用域可见**。 128 | 129 | --- 130 | 131 | 132 | 133 | ### 知识点4: 有名函数返回值遇见defer情况 134 | 135 | ​ 在没有defer的情况下,其实函数的返回就是与return一致的,但是有了defer就不一样了。 136 | 137 | ​ 我们通过**知识点2**得知,先return,再defer,所以在执行完return之后,还要再执行defer里的语句,依然可以修改本应该返回的结果。 138 | 139 | ```go 140 | package main 141 | 142 | import "fmt" 143 | 144 | func returnButDefer() (t int) { //t初始化0, 并且作用域为该函数全域 145 | 146 | defer func() { 147 | t = t * 10 148 | }() 149 | 150 | return 1 151 | } 152 | 153 | func main() { 154 | fmt.Println(returnButDefer()) 155 | } 156 | ``` 157 | 158 | ​ 该`returnButDefer()`本应的返回值是`1`,但是在return之后,又被defer的匿名func函数执行,所以`t=t*10`被执行,最后`returnButDefer()`返回给上层`main()`的结果为`10` 159 | 160 | ```bash 161 | $ go run test.go 162 | 10 163 | ``` 164 | 165 | 166 | 167 | --- 168 | 169 | ### 知识点5: defer遇见panic 170 | 171 | ​ 我们知道,能够触发defer的是遇见return(或函数体到末尾)和遇见panic。 172 | 173 | ​ 根据**知识点2**,我们知道,defer遇见return情况如下: 174 | ![](images/113-defer3.jpeg) 175 | 176 | 177 | 178 | ​ 那么,遇到panic时,遍历本协程的defer链表,并执行defer。在执行defer过程中:遇到recover则停止panic,返回recover处继续往下执行。如果没有遇到recover,遍历完本协程的defer链表后,向stderr抛出panic信息。 179 | 180 | ![](images/114-defer4.jpeg) 181 | 182 | 183 | 184 | #### A. defer遇见panic,但是并不捕获异常的情况 185 | 186 | > test10.go 187 | 188 | ```go 189 | package main 190 | 191 | import ( 192 | "fmt" 193 | ) 194 | 195 | func main() { 196 | defer_call() 197 | 198 | fmt.Println("main 正常结束") 199 | } 200 | 201 | func defer_call() { 202 | defer func() { fmt.Println("defer: panic 之前1") }() 203 | defer func() { fmt.Println("defer: panic 之前2") }() 204 | 205 | panic("异常内容") //触发defer出栈 206 | 207 | defer func() { fmt.Println("defer: panic 之后,永远执行不到") }() 208 | } 209 | 210 | ``` 211 | 212 | **结果** 213 | 214 | ```bash 215 | defer: panic 之前2 216 | defer: panic 之前1 217 | panic: 异常内容 218 | //... 异常堆栈信息 219 | ``` 220 | 221 | 222 | 223 | #### B. defer遇见panic,并捕获异常 224 | 225 | ```go 226 | package main 227 | 228 | import ( 229 | "fmt" 230 | ) 231 | 232 | func main() { 233 | defer_call() 234 | 235 | fmt.Println("main 正常结束") 236 | } 237 | 238 | func defer_call() { 239 | 240 | defer func() { 241 | fmt.Println("defer: panic 之前1, 捕获异常") 242 | if err := recover(); err != nil { 243 | fmt.Println(err) 244 | } 245 | }() 246 | 247 | defer func() { fmt.Println("defer: panic 之前2, 不捕获") }() 248 | 249 | panic("异常内容") //触发defer出栈 250 | 251 | defer func() { fmt.Println("defer: panic 之后, 永远执行不到") }() 252 | } 253 | ``` 254 | 255 | **结果** 256 | 257 | ```go 258 | defer: panic 之前2, 不捕获 259 | defer: panic 之前1, 捕获异常 260 | 异常内容 261 | main 正常结束 262 | ``` 263 | 264 | 265 | 266 | **defer 最大的功能是 panic 后依然有效** 267 | 所以defer可以保证你的一些资源一定会被关闭,从而避免一些异常出现的问题。 268 | 269 | --- 270 | 271 | ### 知识点6: defer中包含panic 272 | 273 | > 编译执行下面代码会出现什么? 274 | 275 | > test16.go 276 | 277 | ```go 278 | package main 279 | 280 | import ( 281 | "fmt" 282 | ) 283 | 284 | func main() { 285 | 286 | defer func() { 287 | if err := recover(); err != nil{ 288 | fmt.Println(err) 289 | }else { 290 | fmt.Println("fatal") 291 | } 292 | }() 293 | 294 | defer func() { 295 | panic("defer panic") 296 | }() 297 | 298 | panic("panic") 299 | } 300 | 301 | ``` 302 | 303 | **结果** 304 | 305 | ```bash 306 | defer panic 307 | ``` 308 | 309 | **分析** 310 | 311 | **panic仅有最后一个可以被revover捕获**。 312 | 313 | 314 | 触发`panic("panic")`后defer顺序出栈执行,第一个被执行的defer中 会有`panic("defer panic")`异常语句,这个异常将会覆盖掉main中的异常`panic("panic")`,最后这个异常被第二个执行的defer捕获到。 315 | 316 | 317 | 318 | --- 319 | 320 | ### 知识点7: defer下的函数参数包含子函数 321 | 322 | ```go 323 | package main 324 | 325 | import "fmt" 326 | 327 | func function(index int, value int) int { 328 | 329 | fmt.Println(index) 330 | 331 | return index 332 | } 333 | 334 | func main() { 335 | defer function(1, function(3, 0)) 336 | defer function(2, function(4, 0)) 337 | } 338 | 339 | ``` 340 | 341 | ​ 这里,有4个函数,他们的index序号分别为1,2,3,4。 342 | 343 | 那么这4个函数的先后执行顺序是什么呢?这里面有两个defer, 所以defer一共会压栈两次,先进栈1,后进栈2。 那么在压栈function1的时候,需要连同函数地址、函数形参一同进栈,那么为了得到function1的第二个参数的结果,所以就需要先执行function3将第二个参数算出,那么function3就被第一个执行。同理压栈function2,就需要执行function4算出function2第二个参数的值。然后函数结束,先出栈fuction2、再出栈function1. 344 | 345 | ​ 所以顺序如下: 346 | 347 | * defer压栈function1,压栈函数地址、形参1、形参2(调用function3) --> 打印3 348 | * defer压栈function2,压栈函数地址、形参1、形参2(调用function4) --> 打印4 349 | * defer出栈function2, 调用function2 --> 打印2 350 | * defer出栈function1, 调用function1--> 打印1 351 | 352 | ```bash 353 | 3 354 | 4 355 | 2 356 | 1 357 | ``` 358 | 359 | --- 360 | 361 | ### 练习:defer面试真题 362 | 363 | 了解以上6个defer的知识点,我们来验证一下网上的真题吧。 364 | 365 | 下面代码输出什么? 366 | 367 | > test11.go 368 | 369 | ```go 370 | package main 371 | 372 | import "fmt" 373 | 374 | func DeferFunc1(i int) (t int) { 375 | t = i 376 | defer func() { 377 | t += 3 378 | }() 379 | return t 380 | } 381 | 382 | func DeferFunc2(i int) int { 383 | t := i 384 | defer func() { 385 | t += 3 386 | }() 387 | return t 388 | } 389 | 390 | func DeferFunc3(i int) (t int) { 391 | defer func() { 392 | t += i 393 | }() 394 | return 2 395 | } 396 | 397 | func DeferFunc4() (t int) { 398 | defer func(i int) { 399 | fmt.Println(i) 400 | fmt.Println(t) 401 | }(t) 402 | t = 1 403 | return 2 404 | } 405 | 406 | func main() { 407 | fmt.Println(DeferFunc1(1)) 408 | fmt.Println(DeferFunc2(1)) 409 | fmt.Println(DeferFunc3(1)) 410 | DeferFunc4() 411 | } 412 | 413 | ``` 414 | 415 | 416 | 417 | --- 418 | 419 | 420 | 421 | ### 练习题分析 422 | 423 | #### DeferFunc1 424 | 425 | ```go 426 | func DeferFunc1(i int) (t int) { 427 | t = i 428 | defer func() { 429 | t += 3 430 | }() 431 | return t 432 | } 433 | ``` 434 | 435 | 1. 将返回值t赋值为传入的i,此时t为1 436 | 2. 执行return语句将t赋值给t(等于啥也没做) 437 | 3. 执行defer方法,将t + 3 = 4 438 | 4. 函数返回 4 439 | 因为t的作用域为整个函数所以修改有效。 440 | 441 | #### DeferFunc2 442 | 443 | ```go 444 | func DeferFunc2(i int) int { 445 | t := i 446 | defer func() { 447 | t += 3 448 | }() 449 | return t 450 | } 451 | ``` 452 | 453 | 1. 创建变量t并赋值为1 454 | 2. 执行return语句,注意这里是将t赋值给返回值,此时返回值为1(这个返回值并不是t) 455 | 3. 执行defer方法,将t + 3 = 4 456 | 4. 函数返回返回值1 457 | 458 | 也可以按照如下代码理解 459 | 460 | ```go 461 | func DeferFunc2(i int) (result int) { 462 | t := i 463 | defer func() { 464 | t += 3 465 | }() 466 | return t 467 | } 468 | ``` 469 | 470 | 上面的代码return的时候相当于将t赋值给了result,当defer修改了t的值之后,对result是不会造成影响的。 471 | 472 | #### DeferFunc3 473 | 474 | ```go 475 | func DeferFunc3(i int) (t int) { 476 | defer func() { 477 | t += i 478 | }() 479 | return 2 480 | } 481 | ``` 482 | 483 | 1. 首先执行return将返回值t赋值为2 484 | 2. 执行defer方法将t + 1 485 | 3. 最后返回 3 486 | 487 | #### DeferFunc4 488 | 489 | ```go 490 | func DeferFunc4() (t int) { 491 | defer func(i int) { 492 | fmt.Println(i) 493 | fmt.Println(t) 494 | }(t) 495 | t = 1 496 | return 2 497 | } 498 | ``` 499 | 500 | 1. 初始化返回值t为零值 0 501 | 2. 首先执行defer的第一步,赋值defer中的func入参t为0 502 | 3. 执行defer的第二步,将defer压栈 503 | 4. 将t赋值为1 504 | 5. 执行return语句,将返回值t赋值为2 505 | 6. 执行defer的第三步,出栈并执行 506 | 因为在入栈时defer执行的func的入参已经赋值了,此时它作为的是一个形式参数,所以打印为0;相对应的因为最后已经将t的值修改为2,所以再打印一个2 507 | 508 | 509 | 510 | #### **结果** 511 | 512 | ```bash 513 | 4 514 | 1 515 | 3 516 | 0 517 | 2 518 | ``` 519 | 520 | 521 | 522 | -------------------------------------------------------------------------------- /7、一种实时动态保活的Worker工作池设计机制.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | ## 7、动态保活Worker工作池设计 3 | 4 | 5 | ### 一、我们如何知道一个Goroutine已经死亡? 6 | 实际上,Go语言并没有给我们暴露如何知道一个Goroutine是否存在的接口,如果要证明一个Go是否存在,可以在子Goroutine的业务中,定期写向一个keep live的Channel,然后主Goroutine来发现当前子Go的状态。Go语言在对于Go和Go之间没有像进程和线程一样有强烈的父子、兄弟等关系,每个Go实际上对于调度器都是一个独立的,平等的执行流程。 7 | 8 | >PS: 如果你是监控子线程、子进程的死亡状态,就没有这么简单了,这里也要感谢go的调度器给我们提供的方便,我们既然用Go,就要基于Go的调度器来实现该模式。 9 | 10 | 那么,我们如何做到一个Goroutine已经死亡了呢? 11 | 12 | #### 子Goroutine 13 | 可以通过给一个被监控的Goroutine添加一个`defer` ,然后`recover()` 捕获到当前Goroutine的异常状态,最后给主Goroutine发送一个死亡信号,通过`Channel`。 14 | 15 | #### 主Goroutine 16 | 在`主Goroutine`上,从这个`Channel`读取内容,当读到内容时,就重启这个`子Goroutine`,当然`主Goroutine`需要记录`子Goroutine`的`ID`,这样也就可以针对性的启动了。 17 | 18 | 19 | ### 二、代码实现 20 | 我们这里以一个工作池的场景来对上述方式进行实现。 21 | 22 | `WorkerManager`作为`主Goroutine`, `worker`作为子`Goroutine` 23 | 24 | > WorkerManager 25 | 26 | ```go 27 | type WorkerManager struct { 28 | //用来监控Worker是否已经死亡的缓冲Channel 29 | workerChan chan *worker 30 | // 一共要监控的worker数量 31 | nWorkers int 32 | } 33 | 34 | //创建一个WorkerManager对象 35 | func NewWorkerManager(nworkers int) *WorkerManager { 36 | return &WorkerManager{ 37 | nWorkers:nworkers, 38 | workerChan: make(chan *worker, nworkers), 39 | } 40 | } 41 | 42 | //启动worker池,并为每个Worker分配一个ID,让每个Worker进行工作 43 | func (wm *WorkerManager)StartWorkerPool() { 44 | //开启一定数量的Worker 45 | for i := 0; i < wm.nWorkers; i++ { 46 | i := i 47 | wk := &worker{id: i} 48 | go wk.work(wm.workerChan) 49 | } 50 | 51 | //启动保活监控 52 | wm.KeepLiveWorkers() 53 | } 54 | 55 | //保活监控workers 56 | func (wm *WorkerManager) KeepLiveWorkers() { 57 | //如果有worker已经死亡 workChan会得到具体死亡的worker然后 打出异常,然后重启 58 | for wk := range wm.workerChan { 59 | // log the error 60 | fmt.Printf("Worker %d stopped with err: [%v] \n", wk.id, wk.err) 61 | // reset err 62 | wk.err = nil 63 | // 当前这个wk已经死亡了,需要重新启动他的业务 64 | go wk.work(wm.workerChan) 65 | } 66 | } 67 | ``` 68 | 69 | 70 | >worker 71 | ```go 72 | type worker struct { 73 | id int 74 | err error 75 | } 76 | 77 | func (wk *worker) work(workerChan chan<- *worker) (err error) { 78 | // 任何Goroutine只要异常退出或者正常退出 都会调用defer 函数,所以在defer中想WorkerManager的WorkChan发送通知 79 | defer func() { 80 | //捕获异常信息,防止panic直接退出 81 | if r := recover(); r != nil { 82 | if err, ok := r.(error); ok { 83 | wk.err = err 84 | } else { 85 | wk.err = fmt.Errorf("Panic happened with [%v]", r) 86 | } 87 | } else { 88 | wk.err = err 89 | } 90 | 91 | //通知 主 Goroutine,当前子Goroutine已经死亡 92 | workerChan <- wk 93 | }() 94 | 95 | // do something 96 | fmt.Println("Start Worker...ID = ", wk.id) 97 | 98 | // 每个worker睡眠一定时间之后,panic退出或者 Goexit()退出 99 | for i := 0; i < 5; i++ { 100 | time.Sleep(time.Second*1) 101 | } 102 | 103 | panic("worker panic..") 104 | //runtime.Goexit() 105 | 106 | return err 107 | } 108 | ``` 109 | 110 | 111 | ### 三、测试 112 | 113 | >main 114 | ```go 115 | func main() { 116 | wm := NewWorkerManager(10) 117 | 118 | wm.StartWorkerPool() 119 | } 120 | ``` 121 | 122 | 结果: 123 | 124 | ```bash 125 | $ go run workmanager.go 126 | Start Worker...ID = 2 127 | Start Worker...ID = 1 128 | Start Worker...ID = 3 129 | Start Worker...ID = 4 130 | Start Worker...ID = 7 131 | Start Worker...ID = 6 132 | Start Worker...ID = 8 133 | Start Worker...ID = 9 134 | Start Worker...ID = 5 135 | Start Worker...ID = 0 136 | Worker 9 stopped with err: [Panic happened with [worker panic..]] 137 | Worker 1 stopped with err: [Panic happened with [worker panic..]] 138 | Worker 0 stopped with err: [Panic happened with [worker panic..]] 139 | Start Worker...ID = 9 140 | Start Worker...ID = 1 141 | Worker 2 stopped with err: [Panic happened with [worker panic..]] 142 | Worker 5 stopped with err: [Panic happened with [worker panic..]] 143 | Worker 4 stopped with err: [Panic happened with [worker panic..]] 144 | Start Worker...ID = 0 145 | Start Worker...ID = 2 146 | Start Worker...ID = 4 147 | Start Worker...ID = 5 148 | Worker 7 stopped with err: [Panic happened with [worker panic..]] 149 | Worker 8 stopped with err: [Panic happened with [worker panic..]] 150 | Worker 6 stopped with err: [Panic happened with [worker panic..]] 151 | Worker 3 stopped with err: [Panic happened with [worker panic..]] 152 | Start Worker...ID = 3 153 | Start Worker...ID = 6 154 | Start Worker...ID = 8 155 | Start Worker...ID = 7 156 | ... 157 | ... 158 | ``` 159 | 160 | 我们会发现,无论子Goroutine是因为 panic()异常退出,还是Goexit()退出,都会被主Goroutine监听到并且重启。主要我们就能够起到保活的功能了. 当然如果线程死亡?进程死亡?我们如何保证? 大家不用担心,我们用Go开发实际上是基于Go的调度器来开发的,进程、线程级别的死亡,会导致调度器死亡,那么我们的全部基础框架都将会塌陷。那么就要看线程、进程如何保活啦,不在我们Go开发的范畴之内了。 161 | -------------------------------------------------------------------------------- /8、精通Golang项目依赖Gomodules.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 8、精通Golang项目依赖Go modules 4 | 5 | ### 一、什么是Go Modules? 6 | 7 | Go modules 是 Go 语言的依赖解决方案,发布于 Go1.11,成长于 Go1.12,丰富于 Go1.13,正式于 Go1.14 推荐在生产上使用。 8 | 9 | 10 | 11 | Go moudles 目前集成在 Go 的工具链中,只要安装了 Go,自然而然也就可以使用 Go moudles 了,而 Go modules 的出现也解决了在 Go1.11 前的几个常见争议问题: 12 | 13 | 1. Go 语言长久以来的依赖管理问题。 14 | 2. “淘汰”现有的 GOPATH 的使用模式。 15 | 3. 统一社区中的其它的依赖管理工具(提供迁移功能)。 16 | 17 | 18 | 19 | ### 二、GOPATH的工作模式 20 | 21 | Go Modoules的目的之一就是淘汰GOPATH, 那么GOPATH是个什么? 22 | 23 | 为什么在 Go1.11 前就使用 GOPATH,而 Go1.11 后就开始逐步建议使用 Go modules,不再推荐 GOPATH 的模式了呢? 24 | 25 | #### (1) Wait is GOPATH? 26 | 27 | ```bash 28 | $ go env 29 | 30 | GOPATH="/home/itheima/go" 31 | ... 32 | 33 | ``` 34 | 35 | 36 | 37 | 我们输入`go env`命令行后可以查看到 GOPATH 变量的结果,我们进入到该目录下进行查看,如下: 38 | 39 | ```bash 40 | go 41 | ├── bin 42 | ├── pkg 43 | └── src 44 | ├── github.com 45 | ├── golang.org 46 | ├── google.golang.org 47 | ├── gopkg.in 48 | .... 49 | ``` 50 | 51 | 52 | 53 | GOPATH目录下一共包含了三个子目录,分别是: 54 | 55 | - bin:存储所编译生成的二进制文件。 56 | - pkg:存储预编译的目标文件,以加快程序的后续编译速度。 57 | - src:存储所有`.go`文件或源代码。在编写 Go 应用程序,程序包和库时,一般会以`$GOPATH/src/github.com/foo/bar`的路径进行存放。 58 | 59 | 因此在使用 GOPATH 模式下,我们需要将应用代码存放在固定的`$GOPATH/src`目录下,并且如果执行`go get`来拉取外部依赖会自动下载并安装到`$GOPATH`目录下。 60 | 61 | 62 | 63 | #### (2) GOPATH模式的弊端 64 | 65 | 在 GOPATH 的 `$GOPATH/src` 下进行 `.go` 文件或源代码的存储,我们可以称其为 GOPATH 的模式,这个模式拥有一些弊端. 66 | 67 | * **A. 无版本控制概念.** 在执行`go get`的时候,你无法传达任何的版本信息的期望,也就是说你也无法知道自己当前更新的是哪一个版本,也无法通过指定来拉取自己所期望的具体版本。 68 | 69 | - **B.无法同步一致第三方版本号.** 在运行 Go 应用程序的时候,你无法保证其它人与你所期望依赖的第三方库是相同的版本,也就是说在项目依赖库的管理上,你无法保证所有人的依赖版本都一致。 70 | - **C.无法指定当前项目引用的第三方版本号. ** 你没办法处理 v1、v2、v3 等等不同版本的引用问题,因为 GOPATH 模式下的导入路径都是一样的,都是`github.com/foo/bar`。 71 | 72 | 73 | 74 | ### 三、Go Modules模式 75 | 76 | 我们接下来用Go Modules的方式创建一个项目, 建议为了与GOPATH分开,不要将项目创建在`GOPATH/src`下. 77 | 78 | #### (1) go mod命令 79 | 80 | | 命令 | 作用 | 81 | | :-------------- | :------------------------------- | 82 | | go mod init | 生成 go.mod 文件 | 83 | | go mod download | 下载 go.mod 文件中指明的所有依赖 | 84 | | go mod tidy | 整理现有的依赖 | 85 | | go mod graph | 查看现有的依赖结构 | 86 | | go mod edit | 编辑 go.mod 文件 | 87 | | go mod vendor | 导出项目所有的依赖到vendor目录 | 88 | | go mod verify | 校验一个模块是否被篡改过 | 89 | | go mod why | 查看为什么需要依赖某模块 | 90 | 91 | #### (2) go mod环境变量 92 | 93 | 可以通过 `go env` 命令来进行查看 94 | 95 | ```bash 96 | $ go env 97 | GO111MODULE="auto" 98 | GOPROXY="https://proxy.golang.org,direct" 99 | GONOPROXY="" 100 | GOSUMDB="sum.golang.org" 101 | GONOSUMDB="" 102 | GOPRIVATE="" 103 | ... 104 | ``` 105 | 106 | 107 | 108 | ##### GO111MODULE 109 | 110 | Go语言提供了 `GO111MODULE `这个环境变量来作为 Go modules 的开关,其允许设置以下参数: 111 | 112 | - auto:只要项目包含了 go.mod 文件的话启用 Go modules,目前在 Go1.11 至 Go1.14 中仍然是默认值。 113 | - on:启用 Go modules,推荐设置,将会是未来版本中的默认值。 114 | - off:禁用 Go modules,不推荐设置。 115 | 116 | 可以通过来设置 117 | 118 | ```bash 119 | $ go env -w GO111MODULE=on 120 | ``` 121 | 122 | 123 | 124 | ##### GOPROXY 125 | 126 | 这个环境变量主要是用于设置 Go 模块代理(Go module proxy),其作用是用于使 Go 在后续拉取模块版本时直接通过镜像站点来快速拉取。 127 | 128 | GOPROXY 的默认值是:`https://proxy.golang.org,direct` 129 | 130 | `proxy.golang.org`国内访问不了,需要设置国内的代理. 131 | 132 | * 阿里云 133 | 134 | https://mirrors.aliyun.com/goproxy/ 135 | 136 | * 七牛云 137 | 138 | https://goproxy.cn,direct 139 | 140 | 141 | 142 | 如: 143 | 144 | ```bash 145 | $ go env -w GOPROXY=https://goproxy.cn,direct 146 | ``` 147 | 148 | GOPROXY 的值是一个以英文逗号 “,” 分割的 Go 模块代理列表,允许设置多个模块代理,假设你不想使用,也可以将其设置为 “off” ,这将会禁止 Go 在后续操作中使用任何 Go 模块代理。 149 | 150 | 如: 151 | 152 | ```bash 153 | $ go env -w GOPROXY=https://goproxy.cn,https://mirrors.aliyun.com/goproxy/,direct 154 | ``` 155 | 156 | 157 | 158 | > direct 159 | 160 | 而在刚刚设置的值中,我们可以发现值列表中有 “direct” 标识,它又有什么作用呢? 161 | 162 | 实际上 “direct” 是一个特殊指示符,用于指示 Go 回源到模块版本的源地址去抓取(比如 GitHub 等),场景如下:当值列表中上一个 Go 模块代理返回 404 或 410 错误时,Go 自动尝试列表中的下一个,遇见 “direct” 时回源,也就是回到源地址去抓取,而遇见 EOF 时终止并抛出类似 “invalid version: unknown revision...” 的错误。 163 | 164 | 165 | 166 | ##### GOSUMDB 167 | 168 | 它的值是一个 Go checksum database,用于在拉取模块版本时(无论是从源站拉取还是通过 Go module proxy 拉取)保证拉取到的模块版本数据未经过篡改,若发现不一致,也就是可能存在篡改,将会立即中止。 169 | 170 | GOSUMDB 的默认值为:`sum.golang.org`,在国内也是无法访问的,但是 GOSUMDB 可以被 Go 模块代理所代理(详见:Proxying a Checksum Database)。 171 | 172 | 因此我们可以通过设置 GOPROXY 来解决,而先前我们所设置的模块代理 `goproxy.cn` 就能支持代理 `sum.golang.org`,所以这一个问题在设置 GOPROXY 后,你可以不需要过度关心。 173 | 174 | 另外若对 GOSUMDB 的值有自定义需求,其支持如下格式: 175 | 176 | - 格式 1:`+`。 177 | - 格式 2:`+ `。 178 | 179 | 也可以将其设置为“off”,也就是禁止 Go 在后续操作中校验模块版本。 180 | 181 | 182 | 183 | ##### GONOPROXY/GONOSUMDB/GOPRIVATE 184 | 185 | 186 | 187 | 这三个环境变量都是用在当前项目依赖了私有模块,例如像是你公司的私有 git 仓库,又或是 github 中的私有库,都是属于私有模块,都是要进行设置的,否则会拉取失败。 188 | 189 | 更细致来讲,就是依赖了由 GOPROXY 指定的 Go 模块代理或由 GOSUMDB 指定 Go checksum database 都无法访问到的模块时的场景。 190 | 191 | 而一般**建议直接设置 GOPRIVATE,它的值将作为 GONOPROXY 和 GONOSUMDB 的默认值,所以建议的最佳姿势是直接使用 GOPRIVATE**。 192 | 193 | 并且它们的值都是一个以英文逗号 “,” 分割的模块路径前缀,也就是可以设置多个,例如: 194 | 195 | ```bash 196 | $ go env -w GOPRIVATE="git.example.com,github.com/eddycjy/mquote" 197 | ``` 198 | 199 | 设置后,前缀为 git.xxx.com 和 github.com/eddycjy/mquote 的模块都会被认为是私有模块。 200 | 201 | 如果不想每次都重新设置,我们也可以利用通配符,例如: 202 | 203 | ```bash 204 | $ go env -w GOPRIVATE="*.example.com" 205 | ``` 206 | 207 | 这样子设置的话,所有模块路径为 example.com 的子域名(例如:git.example.com)都将不经过 Go module proxy 和 Go checksum database,**需要注意的是不包括 example.com 本身**。 208 | 209 | 210 | 211 | ### 四、使用Go Modules初始化项目 212 | 213 | #### (1) 开启Go Modules 214 | 215 | ```bash 216 | $ go env -w GO111MODULE=on 217 | ``` 218 | 219 | 又或是可以通过直接设置系统环境变量(写入对应的~/.bash_profile 文件亦可)来实现这个目的: 220 | 221 | ```bash 222 | $ export GO111MODULE=on 223 | ``` 224 | 225 | 226 | 227 | #### (2) 初始化项目 228 | 229 | 创建项目目录 230 | 231 | ```bash 232 | $ mkdir -p $HOME/aceld/modules_test 233 | $ cd $HOME/aceld/modules_test 234 | ``` 235 | 236 | 执行Go modules 初始化 237 | 238 | ```bash 239 | $ go mod init github.com/aceld/modules_test 240 | go: creating new go.mod: module github.com/aceld/modules_test 241 | ``` 242 | 243 | ​ 在执行 `go mod init` 命令时,我们指定了模块导入路径为 `github.com/aceld/modules_test`。接下来我们在该项目根目录下创建 `main.go` 文件,如下: 244 | 245 | ```go 246 | package main 247 | 248 | import ( 249 | "fmt" 250 | "github.com/aceld/zinx/znet" 251 | "github.com/aceld/zinx/ziface" 252 | ) 253 | 254 | //ping test 自定义路由 255 | type PingRouter struct { 256 | znet.BaseRouter 257 | } 258 | 259 | //Ping Handle 260 | func (this *PingRouter) Handle(request ziface.IRequest) { 261 | //先读取客户端的数据 262 | fmt.Println("recv from client : msgId=", request.GetMsgID(), 263 | ", data=", string(request.GetData())) 264 | 265 | //再回写ping...ping...ping 266 | err := request.GetConnection().SendBuffMsg(0, []byte("ping...ping...ping")) 267 | if err != nil { 268 | fmt.Println(err) 269 | } 270 | } 271 | 272 | func main() { 273 | //1 创建一个server句柄 274 | s := znet.NewServer() 275 | 276 | //2 配置路由 277 | s.AddRouter(0, &PingRouter{}) 278 | 279 | //3 开启服务 280 | s.Serve() 281 | } 282 | 283 | ``` 284 | 285 | OK, 我们先不要关注代码本身,我们看当前的main.go也就是我们的`aceld/modules_test`项目,是依赖一个叫`github.com/aceld/zinx`库的. `znet`和`ziface`只是`zinx`的两个模块. 286 | 287 | 接下来我们在`$HOME/aceld/modules_test`,本项目的根目录执行 288 | 289 | ```bash 290 | $ go get github.com/aceld/zinx/znet 291 | 292 | go: downloading github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100 293 | go: found github.com/aceld/zinx/znet in github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100 294 | 295 | ``` 296 | 297 | 我们会看到 我们的`go.mod`被修改,同时多了一个`go.sum`文件. 298 | 299 | 300 | 301 | #### (3) 查看go.mod文件 302 | 303 | > aceld/modules_test/go.mod 304 | 305 | ```go 306 | module github.com/aceld/modules_test 307 | 308 | go 1.14 309 | 310 | require github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100 // indirect 311 | ``` 312 | 313 | 我们来简单看一下这里面的关键字 314 | 315 | `module`: 用于定义当前项目的模块路径 316 | 317 | `go`:标识当前Go版本.即初始化版本 318 | 319 | `require`: 当前项目依赖的一个特定的必须版本 320 | 321 | `// indirect`: 示该模块为间接依赖,也就是在当前应用程序中的 import 语句中,并没有发现这个模块的明确引用,有可能是你先手动 `go get` 拉取下来的,也有可能是你所依赖的模块所依赖的.我们的代码很明显是依赖的`"github.com/aceld/zinx/znet"`和`"github.com/aceld/zinx/ziface"`,所以就间接的依赖了`github.com/aceld/zinx` 322 | 323 | 324 | 325 | #### (4) 查看go.sum文件 326 | 327 | 在第一次拉取模块依赖后,会发现多出了一个 go.sum 文件,其详细罗列了当前项目直接或间接依赖的所有模块版本,并写明了那些模块版本的 SHA-256 哈希值以备 Go 在今后的操作中保证项目所依赖的那些模块版本不会被篡改。 328 | 329 | ```bash 330 | github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100 h1:Ez5iM6cKGMtqvIJ8nvR9h74Ln8FvFDgfb7bJIbrKv54= 331 | github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100/go.mod h1:bMiERrPdR8FzpBOo86nhWWmeHJ1cCaqVvWKCGcDVJ5M= 332 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 333 | ``` 334 | 335 | 我们可以看到一个模块路径可能有如下两种: 336 | 337 | h1:hash情况 338 | 339 | ```bash 340 | github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100 h1:Ez5iM6cKGMtqvIJ8nvR9h74Ln8FvFDgfb7bJIbrKv54= 341 | ``` 342 | 343 | go.mod hash情况 344 | 345 | ```bash 346 | github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100/go.mod h1:bMiERrPdR8FzpBOo86nhWWmeHJ1cCaqVvWKCGcDVJ5M= 347 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 348 | ``` 349 | 350 | h1 hash 是 Go modules 将目标模块版本的 zip 文件开包后,针对所有包内文件依次进行 hash,然后再把它们的 hash 结果按照固定格式和算法组成总的 hash 值。 351 | 352 | 而 h1 hash 和 go.mod hash 两者,要不就是同时存在,要不就是只存在 go.mod hash。那什么情况下会不存在 h1 hash 呢,就是当 Go 认为肯定用不到某个模块版本的时候就会省略它的 h1 hash,就会出现不存在 h1 hash,只存在 go.mod hash 的情况。 353 | 354 | 355 | 356 | ### 五、修改模块的版本依赖关系 357 | 358 | ​ 为了作尝试,假定我们现在都zinx版本作了升级, 由`zinx v0.0.0-20200221135252-8a8954e75100` 升级到 `zinx v0.0.0-20200306023939-bc416543ae24` (注意zinx是一个没有打版本tag打第三方库,如果有的版本号是有tag的,那么可以直接对应v后面的版本号即可) 359 | 360 | ​ 那么,我们是怎么知道zinx做了升级呢, 我们又是如何知道的最新的`zinx`版本号是多少呢? 361 | 362 | ​ 先回到`$HOME/aceld/modules_test`,本项目的根目录执行 363 | 364 | ```bash 365 | $ go get github.com/aceld/zinx/znet 366 | go: downloading github.com/aceld/zinx v0.0.0-20200306023939-bc416543ae24 367 | go: found github.com/aceld/zinx/znet in github.com/aceld/zinx v0.0.0-20200306023939-bc416543ae24 368 | go: github.com/aceld/zinx upgrade => v0.0.0-20200306023939-bc416543ae24 369 | ``` 370 | 371 | 这样我们,下载了最新的zinx, 版本是`v0.0.0-20200306023939-bc416543ae24` 372 | 373 | ​ 然后,我么看一下go.mod 374 | 375 | ```go 376 | module github.com/aceld/modules_test 377 | 378 | go 1.14 379 | 380 | require github.com/aceld/zinx v0.0.0-20200306023939-bc416543ae24 // indirect 381 | ``` 382 | 383 | 384 | 385 | 我们会看到,当我们执行`go get` 的时候, 会自动的将本地将当前项目的`require`更新了.变成了最新的依赖. 386 | 387 | 388 | 389 | 好了, 现在我们就要做另外一件事,就是,我们想用一个旧版本的zinx. 来修改当前`zinx`模块的依赖版本号. 390 | 391 | 目前我们在`$GOPATH/pkg/mod/github.com/aceld`下,已经有了两个版本的zinx库 392 | 393 | ```bash 394 | /go/pkg/mod/github.com/aceld$ ls 395 | zinx@v0.0.0-20200221135252-8a8954e75100 396 | zinx@v0.0.0-20200306023939-bc416543ae24 397 | ``` 398 | 399 | ​ 目前,我们`/aceld/modules_test`依赖的是`zinx@v0.0.0-20200306023939-bc416543ae24` 这个是最新版, 我们要改成之前的版本`zinx@v0.0.0-20200306023939-bc416543ae24`. 400 | 401 | ​ 回到`/aceld/modules_test`项目目录下,执行 402 | 403 | ```bash 404 | $ go mod edit -replace=zinx@v0.0.0-20200306023939-bc416543ae24=zinx@v0.0.0-20200221135252-8a8954e75100 405 | ``` 406 | 407 | ​ 然后我们打开go.mod查看一下 408 | 409 | ```go 410 | module github.com/aceld/modules_test 411 | 412 | go 1.14 413 | 414 | require github.com/aceld/zinx v0.0.0-20200306023939-bc416543ae24 // indirect 415 | 416 | replace zinx v0.0.0-20200306023939-bc416543ae24 => zinx v0.0.0-20200221135252-8a8954e75100 417 | ``` 418 | 419 | 420 | 421 | ​ 这里出现了`replace`关键字.用于将一个模块版本替换为另外一个模块版本。 422 | 423 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### 出版计算机图书 4 | 5 | ![深入理解Go语言](https://github.com/aceld/golang/assets/7778936/2ec54a86-449d-4a6f-b820-fab37d00c9ee) 6 | 7 | 8 | | 平台 | 链接 | 9 | | --- | --- | 10 | | 当当自营 | http://product.dangdang.com/29569696.html | 11 | | 京东自营 | https://item.jd.com/13736143.html?bbtf=1 | 12 | | 京东 | 搜索 **“深入理解Go语言”** | 13 | | 天猫 | 搜索 **“深入理解Go语言”** | 14 | | 当当 | 搜索 **“深入理解Go语言”** | 15 | | 淘宝 | 搜索 **“深入理解Go语言”** | 16 | 17 | | 线下书店 | 18 | | --- | 19 | | 全国**新华书店** | 20 | 21 | 22 | 23 | 24 | 25 | --- 26 | 27 | ![](images/Golang修养之路封面.jpg) 28 | 本书针对Golang专题性热门技术深入理解,修养在Golang领域深入话题,脱胎换骨。 29 | 30 | 主要内容涉及: 31 | * 深入理解GMP全场景分析 32 | * 深入理解GC三色标记与混合写屏障 33 | * Golang技术性能调优 34 | 35 | ## [语雀阅读版](https://yuque.com/aceld) 36 | 37 | 38 | ## 开源地址: 39 | 40 | Github:https://github.com/aceld/golang 41 | 42 | Gitee:https://gitee.com/Aceld/golang 43 | 44 | 45 | 46 | > 作者:刘丹冰Aceld 47 | > 48 | > ![](images/0-作者公众号刘丹冰Aceld.jpg) 49 | --- 50 | 51 | 52 | * [封面](default.md) 53 | * [第一篇:Golang修养必经之路](第一篇:Golang修养必经之路.md) 54 | * [1、最常用的调试 golang 的 bug 以及性能问题的实践方法?](1、最常用的调试golang的bug以及性能问题的实践方法?.md) 55 | * [2、Golang的协程调度器原理及GMP设计思想?](2、Golang的协程调度器原理及GMP设计思想?.md) 56 | * [3、Golang中逃逸现象, 变量“何时栈?何时堆?”](3、Golang中逃逸现象,变量“何时栈何时堆”.md) 57 | * [4、Golang中make与new有何区别?](4、Golang中make与new有何区别?.md) 58 | * [5、Golang三色标记+混合写屏障GC模式全分析](5、Golang三色标记+混合写屏障GC模式全分析.md) 59 | * [6、面向对象的编程思维理解interface](6、面向对象的编程思维理解interface.md) 60 | * [7、Golang中的Defer必掌握的7知识点](7、Golang中的Defer必掌握的7知识点.md) 61 | * [8、精通Golang项目依赖Go modules](8、精通Golang项目依赖Gomodules.md) 62 | * [第二篇:Golang面试之路](第二篇:Golang面试之路.md) 63 | * [1、数据定义](1、数据定义.md) 64 | * [2、数组和切片](2、数组和切片.md) 65 | * [3、Map](3、Map.md) 66 | * [4、interface](4、interface.md) 67 | * [5、channel](5、channel.md) 68 | * [6、WaitGroup](6、WaitGroup.md) 69 | * [第三篇、Golang编程设计与通用之路](第三篇、Golang编程设计与通用之路.md) 70 | * [1、流?I/O操作?阻塞?epoll?](1、流?I-O操作?阻塞?epoll.md) 71 | * [2、分布式从ACID、CAP、BASE的理论推进](2、分布式从ACID、CAP、BASE的理论推进.md) 72 | * [3、对于操作系统而言进程、线程以及Goroutine协程的区别](3、对于操作系统而言进程、线程以及Goroutine协程的区别.md) 73 | * [4、Go是否可以无限go? 如何限定数量?](4、Go是否可以无限go?如何限定数量?.md) 74 | * [5、单点Server的N种并发模型汇总](5、单点Server的N种并发模型汇总.md) 75 | * [6、TCP中TIME_WAIT状态意义详解](6、TCP中TIME_WAIT状态意义详解.md) 76 | * [7、动态保活Worker工作池设计](7、一种实时动态保活的Worker工作池设计机制.md) 77 | 78 | 本书包括重点章节教学视频 79 | 80 | ## 📺视频一、《Golang深入理解GPM模型》 81 | | platform | link | 82 | | ---- | ---- | 83 | ||https://www.bilibili.com/video/BV19r4y1w7Nx| 84 | ||https://www.douyin.com/video/7135998503377046820 | 85 | 86 | 87 | ## 📺视频二、《Golang深入理解GC三色标记与混合写屏障》 88 | | platform | link | 89 | | ---- | ---- | 90 | ||https://www.bilibili.com/video/BV1wz4y1y7Kd| 91 | ||https://www.douyin.com/video/6981827730933419271| 92 | ||https://www.youtube.com/watch?v=QqblICkRBKA&list=PL_GrAPKmuajz6T5EBXGbEgx9LciuuryHD| 93 | 94 | 95 | ## 📺视频三、《Linux深入理解IO复用并发模型》 96 | | platform | link | 97 | | ---- | ---- | 98 | ||https://www.bilibili.com/video/BV1jK4y1N7ST| 99 | ||https://www.douyin.com/video/7142675640242769159| 100 | 101 | --- 102 | 103 | ## 原创作品 104 | 105 | | 刘丹冰Aceld | ![image.png](https://static.golangjob.cn/221008/b4d7cb9e6382f5facbc2bd707d91f5ad.png)| 106 | | ---- | ---- | 107 | |技术知识库|https://www.yuque.com/aceld| 108 | |---|---| 109 | ||[《Golang修养之路》](https://www.yuque.com/aceld/golang/ithv8f)| 110 | ||[《8小时转职Golang工程师》](https://www.yuque.com/aceld/mo95lb/dsk886)| 111 | ||[《zinx-Golang轻量级Tcp服务器框架》](https://www.yuque.com/aceld/npyr8s/bgftov)| 112 | |代表作品|[《Lars-基于C++负载均衡远程服务器调度系统》](https://www.yuque.com/aceld/wbs5h3/ggzqva)| 113 | ||[《libevent深入浅出》](https://www.yuque.com/aceld/vwi2dk/sss79n)| 114 | ||[《Nginx中文入门手册》](https://www.yuque.com/aceld/fpknid/pzxaev)| 115 | ||[《Linux上Lua应用实战与人工智能》](https://www.yuque.com/aceld/pxbsur/sg3adg)| 116 | |---|---| 117 | |github|https://github.com/aceld| 118 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | * [封面](default.md) 2 | * [第一篇:Golang修养必经之路](第一篇:Golang修养必经之路.md) 3 | * [1、最常用的调试 golang 的 bug 以及性能问题的实践方法?](1、最常用的调试golang的bug以及性能问题的实践方法?.md) 4 | * [2、Golang的协程调度器原理及GMP设计思想?](2、Golang的协程调度器原理及GMP设计思想?.md) 5 | * [3、Golang中逃逸现象, 变量“何时栈?何时堆?”](3、Golang中逃逸现象,变量“何时栈何时堆”.md) 6 | * [4、Golang中make与new有何区别?](4、Golang中make与new有何区别?.md) 7 | * [5、Golang三色标记+混合写屏障GC模式全分析](5、Golang三色标记+混合写屏障GC模式全分析.md) 8 | * [6、面向对象的编程思维理解interface](6、面向对象的编程思维理解interface.md) 9 | * [7、Golang中的Defer必掌握的7知识点](7、Golang中的Defer必掌握的7知识点.md) 10 | * [8、精通Golang项目依赖Go modules](8、精通Golang项目依赖Gomodules.md) 11 | * [第二篇:Golang面试之路](第二篇:Golang面试之路.md) 12 | * [1、数据定义](1、数据定义.md) 13 | * [2、数组和切片](2、数组和切片.md) 14 | * [3、Map](3、Map.md) 15 | * [4、interface](4、interface.md) 16 | * [5、channel](5、channel.md) 17 | * [6、WaitGroup](6、WaitGroup.md) 18 | * [第三篇、Golang编程设计与通用之路](第三篇、Golang编程设计与通用之路.md) 19 | * [1、流?I/O操作?阻塞?epoll?](1、流?I-O操作?阻塞?epoll.md) 20 | * [2、分布式从ACID、CAP、BASE的理论推进](2、分布式从ACID、CAP、BASE的理论推进.md) 21 | * [3、对于操作系统而言进程、线程以及Goroutine协程的区别](3、对于操作系统而言进程、线程以及Goroutine协程的区别.md) 22 | * [4、Go是否可以无限go? 如何限定数量?](4、Go是否可以无限go?如何限定数量?.md) 23 | * [5、单点Server的N种并发模型汇总](5、单点Server的N种并发模型汇总.md) 24 | * [6、TCP中TIME_WAIT状态意义详解](6、TCP中TIME_WAIT状态意义详解.md) 25 | * [7、动态保活Worker工作池设计](7、一种实时动态保活的Worker工作池设计机制.md) 26 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "highlight" 4 | ] 5 | } -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/cover.jpg -------------------------------------------------------------------------------- /default.md: -------------------------------------------------------------------------------- 1 | ![](images/Golang修养之路封面.jpg) 2 | 3 | 4 | 本书针对Golang专题性热门技术深入理解,修养在Golang领域深入话题,脱胎换骨。 5 | 6 | 主要内容涉及: 7 | * 深入理解GMP全场景分析 8 | * 深入理解GC三色标记与混合写屏障 9 | * Golang技术性能调优 10 | 11 | --- 12 | > ### 《Golang修养之路》内容将不断更新 13 | > 感谢持续"关注"... 14 | --- 15 | 16 | > 作者:刘丹冰Aceld 17 | > ![](images/0-作者公众号刘丹冰Aceld.jpg) 18 | 19 | * [封面](default.md) 20 | * [第一篇:Golang修养必经之路](第一篇:Golang修养必经之路.md) 21 | * [1、最常用的调试 golang 的 bug 以及性能问题的实践方法?](1、最常用的调试golang的bug以及性能问题的实践方法?.md) 22 | * [2、Golang的协程调度器原理及GMP设计思想?](2、Golang的协程调度器原理及GMP设计思想?.md) 23 | * [3、Golang中逃逸现象, 变量“何时栈?何时堆?”](3、Golang中逃逸现象,变量“何时栈何时堆”.md) 24 | * [4、Golang中make与new有何区别?](4、Golang中make与new有何区别?.md) 25 | * [5、Golang三色标记+混合写屏障GC模式全分析](5、Golang三色标记+混合写屏障GC模式全分析.md) 26 | * [6、面向对象的编程思维理解interface](6、面向对象的编程思维理解interface.md) 27 | * [7、Golang中的Defer必掌握的7知识点](7、Golang中的Defer必掌握的7知识点.md) 28 | * [8、精通Golang项目依赖Go modules](8、精通Golang项目依赖Gomodules.md) 29 | * [第二篇:Golang面试之路](第二篇:Golang面试之路.md) 30 | * [1、数据定义](1、数据定义.md) 31 | * [2、数组和切片](2、数组和切片.md) 32 | * [3、Map](3、Map.md) 33 | * [4、interface](4、interface.md) 34 | * [5、channel](5、channel.md) 35 | * [6、WaitGroup](6、WaitGroup.md) 36 | * [第三篇、Golang编程设计与通用之路](第三篇、Golang编程设计与通用之路.md) 37 | * [1、流?I/O操作?阻塞?epoll?](1、流?I-O操作?阻塞?epoll.md) 38 | * [2、分布式从ACID、CAP、BASE的理论推进](2、分布式从ACID、CAP、BASE的理论推进.md) 39 | * [3、对于操作系统而言进程、线程以及Goroutine协程的区别](3、对于操作系统而言进程、线程以及Goroutine协程的区别.md) 40 | * [4、Go是否可以无限go? 如何限定数量?](4、Go是否可以无限go?如何限定数量?.md) 41 | * [5、单点Server的N种并发模型汇总](5、单点Server的N种并发模型汇总.md) 42 | * [6、TCP中TIME_WAIT状态意义详解](6、TCP中TIME_WAIT状态意义详解.md) 43 | * [7、动态保活Worker工作池设计](7、一种实时动态保活的Worker工作池设计机制.md) 44 | -------------------------------------------------------------------------------- /images/0-作者公众号刘丹冰Aceld.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/0-作者公众号刘丹冰Aceld.jpg -------------------------------------------------------------------------------- /images/10-N-1关系.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/10-N-1关系.png -------------------------------------------------------------------------------- /images/100-epoll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/100-epoll.png -------------------------------------------------------------------------------- /images/101-epoll3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/101-epoll3.png -------------------------------------------------------------------------------- /images/102-epoll4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/102-epoll4.png -------------------------------------------------------------------------------- /images/103-epoll水平触发1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/103-epoll水平触发1.png -------------------------------------------------------------------------------- /images/104-epoll水平触发2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/104-epoll水平触发2.png -------------------------------------------------------------------------------- /images/105-epoll边缘触发1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/105-epoll边缘触发1.png -------------------------------------------------------------------------------- /images/105-epoll边缘触发2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/105-epoll边缘触发2.png -------------------------------------------------------------------------------- /images/107-内存4区.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/107-内存4区.jpeg -------------------------------------------------------------------------------- /images/108-数据变量.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/108-数据变量.png -------------------------------------------------------------------------------- /images/109-foreach.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/109-foreach.jpeg -------------------------------------------------------------------------------- /images/11-1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/11-1-1.png -------------------------------------------------------------------------------- /images/110-foreach2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/110-foreach2.jpeg -------------------------------------------------------------------------------- /images/111-defer1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/111-defer1.png -------------------------------------------------------------------------------- /images/112-defer2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/112-defer2.jpeg -------------------------------------------------------------------------------- /images/113-defer3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/113-defer3.jpeg -------------------------------------------------------------------------------- /images/114-defer4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/114-defer4.jpeg -------------------------------------------------------------------------------- /images/115-interface1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/115-interface1.jpeg -------------------------------------------------------------------------------- /images/116-interface2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/116-interface2.jpeg -------------------------------------------------------------------------------- /images/117-interface3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/117-interface3.jpeg -------------------------------------------------------------------------------- /images/118-interface4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/118-interface4.jpeg -------------------------------------------------------------------------------- /images/119-interface5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/119-interface5.jpeg -------------------------------------------------------------------------------- /images/12-m-n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/12-m-n.png -------------------------------------------------------------------------------- /images/120-interface6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/120-interface6.jpeg -------------------------------------------------------------------------------- /images/121-interface7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/121-interface7.jpeg -------------------------------------------------------------------------------- /images/126-分布式5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/126-分布式5.jpeg -------------------------------------------------------------------------------- /images/127-分布式6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/127-分布式6.jpeg -------------------------------------------------------------------------------- /images/128-分布式7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/128-分布式7.jpeg -------------------------------------------------------------------------------- /images/129-CAP1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/129-CAP1.jpeg -------------------------------------------------------------------------------- /images/13-gm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/13-gm.png -------------------------------------------------------------------------------- /images/130-CAP2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/130-CAP2.jpeg -------------------------------------------------------------------------------- /images/131-CAP3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/131-CAP3.jpeg -------------------------------------------------------------------------------- /images/132-CAP4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/132-CAP4.jpeg -------------------------------------------------------------------------------- /images/133-CAP5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/133-CAP5.jpeg -------------------------------------------------------------------------------- /images/134-CAP6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/134-CAP6.jpeg -------------------------------------------------------------------------------- /images/135-CAP7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/135-CAP7.jpeg -------------------------------------------------------------------------------- /images/136-CAP8.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/136-CAP8.jpeg -------------------------------------------------------------------------------- /images/137-CAP9.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/137-CAP9.jpeg -------------------------------------------------------------------------------- /images/138-CAP10.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/138-CAP10.jpeg -------------------------------------------------------------------------------- /images/139-CAP11.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/139-CAP11.jpeg -------------------------------------------------------------------------------- /images/14-old调度器.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/14-old调度器.png -------------------------------------------------------------------------------- /images/140-CAP12.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/140-CAP12.jpeg -------------------------------------------------------------------------------- /images/141-CAP13.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/141-CAP13.jpeg -------------------------------------------------------------------------------- /images/143-Base2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/143-Base2.jpeg -------------------------------------------------------------------------------- /images/144-Base2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/144-Base2.jpeg -------------------------------------------------------------------------------- /images/145-Base3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/145-Base3.jpeg -------------------------------------------------------------------------------- /images/146-Cap0.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/146-Cap0.jpeg -------------------------------------------------------------------------------- /images/149-goroutines3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/149-goroutines3.jpeg -------------------------------------------------------------------------------- /images/15-gmp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/15-gmp.png -------------------------------------------------------------------------------- /images/151-goroutines5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/151-goroutines5.jpeg -------------------------------------------------------------------------------- /images/152-进程线程1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/152-进程线程1.jpeg -------------------------------------------------------------------------------- /images/153-进程线程2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/153-进程线程2.jpeg -------------------------------------------------------------------------------- /images/154-进程线程3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/154-进程线程3.jpeg -------------------------------------------------------------------------------- /images/155-进程线程4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/155-进程线程4.jpeg -------------------------------------------------------------------------------- /images/156-进程线程5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/156-进程线程5.jpeg -------------------------------------------------------------------------------- /images/157-进程线程6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/157-进程线程6.jpeg -------------------------------------------------------------------------------- /images/158-并发模型1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/158-并发模型1.jpeg -------------------------------------------------------------------------------- /images/159-并发模型2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/159-并发模型2.jpeg -------------------------------------------------------------------------------- /images/16-GMP-调度.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/16-GMP-调度.png -------------------------------------------------------------------------------- /images/160-并发模型3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/160-并发模型3.jpeg -------------------------------------------------------------------------------- /images/161-并发模型4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/161-并发模型4.jpeg -------------------------------------------------------------------------------- /images/162-并发模型5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/162-并发模型5.jpeg -------------------------------------------------------------------------------- /images/163-并发模型6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/163-并发模型6.jpeg -------------------------------------------------------------------------------- /images/164-并发模型7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/164-并发模型7.jpeg -------------------------------------------------------------------------------- /images/165-timewait1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/165-timewait1.png -------------------------------------------------------------------------------- /images/166-timewait2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/166-timewait2.png -------------------------------------------------------------------------------- /images/167-timewait3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/167-timewait3.png -------------------------------------------------------------------------------- /images/168-timewait4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/168-timewait4.png -------------------------------------------------------------------------------- /images/169-channel异常情况总结.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/169-channel异常情况总结.png -------------------------------------------------------------------------------- /images/17-pic-go调度器生命周期.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/17-pic-go调度器生命周期.png -------------------------------------------------------------------------------- /images/18-go-func调度周期.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/18-go-func调度周期.jpeg -------------------------------------------------------------------------------- /images/19-go-trace1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/19-go-trace1.png -------------------------------------------------------------------------------- /images/20-go-trace2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/20-go-trace2.png -------------------------------------------------------------------------------- /images/20-go-trace3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/20-go-trace3.png -------------------------------------------------------------------------------- /images/22-go-trace4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/22-go-trace4.png -------------------------------------------------------------------------------- /images/23-go-trace5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/23-go-trace5.png -------------------------------------------------------------------------------- /images/24-go-trace6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/24-go-trace6.png -------------------------------------------------------------------------------- /images/25-go-trace7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/25-go-trace7.png -------------------------------------------------------------------------------- /images/26-gmp场景1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/26-gmp场景1.png -------------------------------------------------------------------------------- /images/27-gmp场景2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/27-gmp场景2.png -------------------------------------------------------------------------------- /images/28-gmp场景3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/28-gmp场景3.png -------------------------------------------------------------------------------- /images/29-gmp场景4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/29-gmp场景4.png -------------------------------------------------------------------------------- /images/30-gmp场景5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/30-gmp场景5.png -------------------------------------------------------------------------------- /images/31-gmp场景6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/31-gmp场景6.png -------------------------------------------------------------------------------- /images/32-gmp场景7.001.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/32-gmp场景7.001.jpeg -------------------------------------------------------------------------------- /images/33-gmp场景8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/33-gmp场景8.png -------------------------------------------------------------------------------- /images/34-gmp场景9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/34-gmp场景9.png -------------------------------------------------------------------------------- /images/35-gmp场景10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/35-gmp场景10.png -------------------------------------------------------------------------------- /images/36-gmp场景11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/36-gmp场景11.png -------------------------------------------------------------------------------- /images/37-godebug1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/37-godebug1.png -------------------------------------------------------------------------------- /images/38-pprof-profile-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/38-pprof-profile-web.png -------------------------------------------------------------------------------- /images/39-pprof001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/39-pprof001.png -------------------------------------------------------------------------------- /images/40-平铺设计.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/40-平铺设计.png -------------------------------------------------------------------------------- /images/41-开闭设计.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/41-开闭设计.png -------------------------------------------------------------------------------- /images/42-GC2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/42-GC2.png -------------------------------------------------------------------------------- /images/42-混乱的依赖关系.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/42-混乱的依赖关系.png -------------------------------------------------------------------------------- /images/43-依赖倒转设计.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/43-依赖倒转设计.png -------------------------------------------------------------------------------- /images/44-GC1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/44-GC1.png -------------------------------------------------------------------------------- /images/45-GC3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/45-GC3.png -------------------------------------------------------------------------------- /images/46-GC4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/46-GC4.png -------------------------------------------------------------------------------- /images/47-GC5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/47-GC5.jpeg -------------------------------------------------------------------------------- /images/48-GC6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/48-GC6.jpeg -------------------------------------------------------------------------------- /images/49-GC7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/49-GC7.jpeg -------------------------------------------------------------------------------- /images/5-单进程操作系统.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/5-单进程操作系统.png -------------------------------------------------------------------------------- /images/50-GC8.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/50-GC8.jpeg -------------------------------------------------------------------------------- /images/51-GC9.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/51-GC9.jpeg -------------------------------------------------------------------------------- /images/52-GC10.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/52-GC10.jpeg -------------------------------------------------------------------------------- /images/53-STW1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/53-STW1.png -------------------------------------------------------------------------------- /images/54-STW2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/54-STW2.png -------------------------------------------------------------------------------- /images/55-三色标记问题1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/55-三色标记问题1.jpeg -------------------------------------------------------------------------------- /images/56-三色标记问题2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/56-三色标记问题2.jpeg -------------------------------------------------------------------------------- /images/57-三色标记问题3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/57-三色标记问题3.jpeg -------------------------------------------------------------------------------- /images/58-三色标记问题4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/58-三色标记问题4.jpeg -------------------------------------------------------------------------------- /images/59-三色标记问题5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/59-三色标记问题5.jpeg -------------------------------------------------------------------------------- /images/6-多进程操作系统.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/6-多进程操作系统.png -------------------------------------------------------------------------------- /images/60-三色标记问题6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/60-三色标记问题6.jpeg -------------------------------------------------------------------------------- /images/61-三色标记问题7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/61-三色标记问题7.jpeg -------------------------------------------------------------------------------- /images/62-三色标记插入写屏障1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/62-三色标记插入写屏障1.jpeg -------------------------------------------------------------------------------- /images/63-三色标记插入写屏障2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/63-三色标记插入写屏障2.jpeg -------------------------------------------------------------------------------- /images/64-三色标记插入写屏障3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/64-三色标记插入写屏障3.jpeg -------------------------------------------------------------------------------- /images/65-三色标记插入写屏障4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/65-三色标记插入写屏障4.jpeg -------------------------------------------------------------------------------- /images/66-三色标记插入写屏障5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/66-三色标记插入写屏障5.jpeg -------------------------------------------------------------------------------- /images/67-三色标记插入写屏障6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/67-三色标记插入写屏障6.jpeg -------------------------------------------------------------------------------- /images/68-三色标记插入写屏障7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/68-三色标记插入写屏障7.jpeg -------------------------------------------------------------------------------- /images/69-三色标记插入写屏障9.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/69-三色标记插入写屏障9.jpeg -------------------------------------------------------------------------------- /images/7-cpu切换浪费成本.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/7-cpu切换浪费成本.png -------------------------------------------------------------------------------- /images/70-三色标记插入写屏障10.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/70-三色标记插入写屏障10.jpeg -------------------------------------------------------------------------------- /images/71-三色标记插入写屏障11.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/71-三色标记插入写屏障11.jpeg -------------------------------------------------------------------------------- /images/72-三色标记删除写屏障1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/72-三色标记删除写屏障1.jpeg -------------------------------------------------------------------------------- /images/73-三色标记删除写屏障2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/73-三色标记删除写屏障2.jpeg -------------------------------------------------------------------------------- /images/74-三色标记删除写屏障3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/74-三色标记删除写屏障3.jpeg -------------------------------------------------------------------------------- /images/75-三色标记删除写屏障4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/75-三色标记删除写屏障4.jpeg -------------------------------------------------------------------------------- /images/76-三色标记删除写屏障5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/76-三色标记删除写屏障5.jpeg -------------------------------------------------------------------------------- /images/77-三色标记删除写屏障6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/77-三色标记删除写屏障6.jpeg -------------------------------------------------------------------------------- /images/78-三色标记删除写屏障7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/78-三色标记删除写屏障7.jpeg -------------------------------------------------------------------------------- /images/79-三色标记混合写屏障1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/79-三色标记混合写屏障1.jpeg -------------------------------------------------------------------------------- /images/8-线程的内核和用户态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/8-线程的内核和用户态.png -------------------------------------------------------------------------------- /images/80-三色标记混合写屏障2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/80-三色标记混合写屏障2.jpeg -------------------------------------------------------------------------------- /images/81-三色标记混合写屏障3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/81-三色标记混合写屏障3.jpeg -------------------------------------------------------------------------------- /images/82-三色标记混合写屏障4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/82-三色标记混合写屏障4.jpeg -------------------------------------------------------------------------------- /images/83-三色标记混合写屏障5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/83-三色标记混合写屏障5.jpeg -------------------------------------------------------------------------------- /images/84-三色标记混合写屏障6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/84-三色标记混合写屏障6.jpeg -------------------------------------------------------------------------------- /images/85-三色标记混合写屏障7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/85-三色标记混合写屏障7.jpeg -------------------------------------------------------------------------------- /images/86-三色标记混合写屏障8.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/86-三色标记混合写屏障8.jpeg -------------------------------------------------------------------------------- /images/87-三色标记混合写屏障9.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/87-三色标记混合写屏障9.jpeg -------------------------------------------------------------------------------- /images/88-三色标记混合写屏障10.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/88-三色标记混合写屏障10.jpeg -------------------------------------------------------------------------------- /images/89-三色标记混合写屏障11.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/89-三色标记混合写屏障11.jpeg -------------------------------------------------------------------------------- /images/9-协程和线程.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/9-协程和线程.png -------------------------------------------------------------------------------- /images/90-三色标记混合写屏障12.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/90-三色标记混合写屏障12.jpeg -------------------------------------------------------------------------------- /images/91-三色标记混合写屏障13.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/91-三色标记混合写屏障13.jpeg -------------------------------------------------------------------------------- /images/92-io-阻塞1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/92-io-阻塞1.png -------------------------------------------------------------------------------- /images/93-io-阻塞2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/93-io-阻塞2.png -------------------------------------------------------------------------------- /images/94-io-阻塞3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/94-io-阻塞3.png -------------------------------------------------------------------------------- /images/95-io-阻塞4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/95-io-阻塞4.png -------------------------------------------------------------------------------- /images/96-io-阻塞5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/96-io-阻塞5.png -------------------------------------------------------------------------------- /images/97-非阻塞忙轮询.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/97-非阻塞忙轮询.png -------------------------------------------------------------------------------- /images/98-select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/98-select.png -------------------------------------------------------------------------------- /images/99-epoll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/99-epoll.png -------------------------------------------------------------------------------- /images/GPM封面.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/GPM封面.png -------------------------------------------------------------------------------- /images/Golang修养之路封面.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/Golang修养之路封面.jpg -------------------------------------------------------------------------------- /images/标题.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/标题.jpeg -------------------------------------------------------------------------------- /images/深入理解Go语言.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceld/golang/a2da9f901146acd69219e8bc7a15bec634e6bc2e/images/深入理解Go语言.jpg --------------------------------------------------------------------------------