diff --git a/ch1/ch1-02.html b/ch1/ch1-02.html index e643689..ceb62bc 100644 --- a/ch1/ch1-02.html +++ b/ch1/ch1-02.html @@ -238,7 +238,7 @@
用哪种不用哪种,为什么呢?第一种形式,是一条短变量声明,最简洁,但只能用在函数内部,而不能用于包变量。第二种形式依赖于字符串的默认初始化零值机制,被初始化为 ""
。第三种形式用得很少,除非同时声明多个变量。第四种形式显式地标明变量的类型,当变量类型与初值类型相同时,类型冗余,但如果两者类型不同,变量类型就必须了。实践中一般使用前两种形式中的某个,初始值重要的话就显式地指定变量的类型,否则使用隐式初始化。
用哪种不用哪种,为什么呢?第一种形式,是一条短变量声明,最简洁,但只能用在函数内部,而不能用于包变量。第二种形式依赖于字符串的默认初始化零值机制,被初始化为 ""
。第三种形式用得很少,除非同时声明多个变量。第四种形式显式地标明变量的类型,当变量类型与初值类型相同时,类型冗余,但如果两者类型不同,变量类型就必须了。实践中一般使用前两种形式中的某个,初始值重要的话就显式地指定变量的值,否则指定类型使用隐式初始化。
如前文所述,每次循环迭代字符串 s
的内容都会更新。+=
连接原字符串、空格和下个参数,产生新字符串,并把它赋值给 s
。s
原来的内容已经不再使用,将在适当时机对它进行垃圾回收。
如果连接涉及的数据量很大,这种方式代价高昂。一种简单且高效的解决方案是使用 strings
包的 Join
函数:
gopl.io/ch1/echo3
diff --git a/print.html b/print.html index d0411ff..775ce9e 100644 --- a/print.html +++ b/print.html @@ -369,7 +369,7 @@用哪种不用哪种,为什么呢?第一种形式,是一条短变量声明,最简洁,但只能用在函数内部,而不能用于包变量。第二种形式依赖于字符串的默认初始化零值机制,被初始化为 ""
。第三种形式用得很少,除非同时声明多个变量。第四种形式显式地标明变量的类型,当变量类型与初值类型相同时,类型冗余,但如果两者类型不同,变量类型就必须了。实践中一般使用前两种形式中的某个,初始值重要的话就显式地指定变量的类型,否则使用隐式初始化。
用哪种不用哪种,为什么呢?第一种形式,是一条短变量声明,最简洁,但只能用在函数内部,而不能用于包变量。第二种形式依赖于字符串的默认初始化零值机制,被初始化为 ""
。第三种形式用得很少,除非同时声明多个变量。第四种形式显式地标明变量的类型,当变量类型与初值类型相同时,类型冗余,但如果两者类型不同,变量类型就必须了。实践中一般使用前两种形式中的某个,初始值重要的话就显式地指定变量的值,否则指定类型使用隐式初始化。
如前文所述,每次循环迭代字符串 s
的内容都会更新。+=
连接原字符串、空格和下个参数,产生新字符串,并把它赋值给 s
。s
原来的内容已经不再使用,将在适当时机对它进行垃圾回收。
如果连接涉及的数据量很大,这种方式代价高昂。一种简单且高效的解决方案是使用 strings
包的 Join
函数:
gopl.io/ch1/echo3
diff --git a/searchindex.js b/searchindex.js index bed0eba..add4ad3 100644 --- a/searchindex.js +++ b/searchindex.js @@ -1 +1 @@ -Object.assign(window.search, {"doc_urls":["index.html#go语言圣经中文版","preface-zh.html#译者序","preface.html#前言","preface.html#go语言起源","preface.html#go语言项目","preface.html#本书的组织","preface.html#更多的信息","preface.html#致谢","ch1/ch1.html#第1章-入门","ch1/ch1-01.html#11-hello-world","ch1/ch1-02.html#12-命令行参数","ch1/ch1-03.html#13-查找重复的行","ch1/ch1-04.html#14-gif动画","ch1/ch1-05.html#15-获取url","ch1/ch1-06.html#16-并发获取多个url","ch1/ch1-07.html#17-web服务","ch1/ch1-08.html#18-本章要点","ch2/ch2.html#第2章-程序结构","ch2/ch2-01.html#21-命名","ch2/ch2-02.html#22-声明","ch2/ch2-03.html#23-变量","ch2/ch2-03.html#231-简短变量声明","ch2/ch2-03.html#232-指针","ch2/ch2-03.html#233-new函数","ch2/ch2-03.html#234-变量的生命周期","ch2/ch2-04.html#24-赋值","ch2/ch2-04.html#241-元组赋值","ch2/ch2-04.html#242-可赋值性","ch2/ch2-05.html#25-类型","ch2/ch2-06.html#26-包和文件","ch2/ch2-06.html#261-导入包","ch2/ch2-06.html#262-包的初始化","ch2/ch2-07.html#27-作用域","ch3/ch3.html#第3章-基础数据类型","ch3/ch3-01.html#31-整型","ch3/ch3-02.html#32-浮点数","ch3/ch3-03.html#33-复数","ch3/ch3-04.html#34-布尔型","ch3/ch3-05.html#35-字符串","ch3/ch3-05.html#351-字符串面值","ch3/ch3-05.html#352-unicode","ch3/ch3-05.html#353-utf-8","ch3/ch3-05.html#354-字符串和byte切片","ch3/ch3-05.html#355-字符串和数字的转换","ch3/ch3-06.html#36-常量","ch3/ch3-06.html#361-iota-常量生成器","ch3/ch3-06.html#362-无类型常量","ch4/ch4.html#第4章-复合数据类型","ch4/ch4-01.html#41-数组","ch4/ch4-02.html#42-slice","ch4/ch4-02.html#421-append函数","ch4/ch4-02.html#422-slice内存技巧","ch4/ch4-03.html#43-map","ch4/ch4-04.html#44-结构体","ch4/ch4-04.html#441-结构体字面值","ch4/ch4-04.html#442-结构体比较","ch4/ch4-04.html#443-结构体嵌入和匿名成员","ch4/ch4-05.html#45-json","ch4/ch4-06.html#46-文本和html模板","ch5/ch5.html#第5章-函数","ch5/ch5-01.html#51-函数声明","ch5/ch5-02.html#52-递归","ch5/ch5-03.html#53-多返回值","ch5/ch5-04.html#54-错误","ch5/ch5-04.html#541-错误处理策略","ch5/ch5-04.html#542-文件结尾错误eof","ch5/ch5-05.html#55-函数值","ch5/ch5-06.html#56-匿名函数","ch5/ch5-06.html#561-警告捕获迭代变量","ch5/ch5-07.html#57-可变参数","ch5/ch5-08.html#58-deferred函数","ch5/ch5-09.html#59-panic异常","ch5/ch5-10.html#510-recover捕获异常","ch6/ch6.html#第6章-方法","ch6/ch6-01.html#61-方法声明","ch6/ch6-02.html#62-基于指针对象的方法","ch6/ch6-02.html#621-nil也是一个合法的接收器类型","ch6/ch6-03.html#63-通过嵌入结构体来扩展类型","ch6/ch6-04.html#64-方法值和方法表达式","ch6/ch6-05.html#65-示例-bit数组","ch6/ch6-06.html#66-封装","ch7/ch7.html#第7章-接口","ch7/ch7-01.html#71-接口约定","ch7/ch7-02.html#72-接口类型","ch7/ch7-03.html#73-实现接口的条件","ch7/ch7-04.html#74-flagvalue接口","ch7/ch7-05.html#75--接口值","ch7/ch7-05.html#751--警告一个包含nil指针的接口不是nil接口","ch7/ch7-06.html#76-sortinterface接口","ch7/ch7-07.html#77-httphandler接口","ch7/ch7-08.html#78-error接口","ch7/ch7-09.html#79-示例-表达式求值","ch7/ch7-10.html#710-类型断言","ch7/ch7-11.html#711-基于类型断言区别错误类型","ch7/ch7-12.html#712-通过类型断言询问行为","ch7/ch7-13.html#713-类型分支","ch7/ch7-14.html#714-示例-基于标记的xml解码","ch7/ch7-15.html#715-一些建议","ch8/ch8.html#第8章-goroutines和channels","ch8/ch8-01.html#81-goroutines","ch8/ch8-02.html#82-示例-并发的clock服务","ch8/ch8-03.html#83-示例-并发的echo服务","ch8/ch8-04.html#84-channels","ch8/ch8-04.html#841-不带缓存的channels","ch8/ch8-04.html#842-串联的channelspipeline","ch8/ch8-04.html#843-单方向的channel","ch8/ch8-04.html#844-带缓存的channels","ch8/ch8-05.html#85-并发的循环","ch8/ch8-06.html#86-示例-并发的web爬虫","ch8/ch8-07.html#87-基于select的多路复用","ch8/ch8-08.html#88-示例-并发的目录遍历","ch8/ch8-09.html#89-并发的退出","ch8/ch8-10.html#810-示例-聊天服务","ch9/ch9.html#第9章-基于共享变量的并发","ch9/ch9-01.html#91-竞争条件","ch9/ch9-02.html#92-syncmutex互斥锁","ch9/ch9-03.html#93-syncrwmutex读写锁","ch9/ch9-04.html#94-内存同步","ch9/ch9-05.html#95-synconce惰性初始化","ch9/ch9-06.html#96-竞争条件检测","ch9/ch9-07.html#97-示例-并发的非阻塞缓存","ch9/ch9-08.html#98-goroutines和线程","ch9/ch9-08.html#981-动态栈","ch9/ch9-08.html#982-goroutine调度","ch9/ch9-08.html#983-gomaxprocs","ch9/ch9-08.html#984-goroutine没有id号","ch10/ch10.html#第10章-包和工具","ch10/ch10-01.html#101-包简介","ch10/ch10-02.html#102-导入路径","ch10/ch10-03.html#103-包声明","ch10/ch10-04.html#104-导入声明","ch10/ch10-05.html#105-包的匿名导入","ch10/ch10-06.html#106-包和命名","ch10/ch10-07.html#107-工具","ch10/ch10-07.html#1071-工作区结构","ch10/ch10-07.html#1072-下载包","ch10/ch10-07.html#1073-构建包","ch10/ch10-07.html#1074-包文档","ch10/ch10-07.html#1075-内部包","ch10/ch10-07.html#1076-查询包","ch11/ch11.html#第11章-测试","ch11/ch11-01.html#111-go-test","ch11/ch11-02.html#112-测试函数","ch11/ch11-02.html#1121-随机测试","ch11/ch11-02.html#1122-测试一个命令","ch11/ch11-02.html#1123-白盒测试","ch11/ch11-02.html#1124-外部测试包","ch11/ch11-02.html#1125-编写有效的测试","ch11/ch11-02.html#1126-避免脆弱的测试","ch11/ch11-03.html#113-测试覆盖率","ch11/ch11-04.html#114-基准测试","ch11/ch11-05.html#115-剖析","ch11/ch11-06.html#116-示例函数","ch12/ch12.html#第12章-反射","ch12/ch12-01.html#121-为何需要反射","ch12/ch12-02.html#122-reflecttype-和-reflectvalue","ch12/ch12-03.html#123-display一个递归的值打印器","ch12/ch12-04.html#124-示例-编码为s表达式","ch12/ch12-05.html#125-通过reflectvalue修改值","ch12/ch12-06.html#126-示例-解码s表达式","ch12/ch12-07.html#127-获取结构体字段标签","ch12/ch12-08.html#128-显示一个类型的方法集","ch12/ch12-09.html#129-几点忠告","ch13/ch13.html#第13章-底层编程","ch13/ch13-01.html#131-unsafesizeof-alignof-和-offsetof","ch13/ch13-02.html#132-unsafepointer","ch13/ch13-03.html#133-示例-深度相等判断","ch13/ch13-04.html#134-通过cgo调用c代码","ch13/ch13-05.html#135-几点忠告","appendix/appendix.html#附录","appendix/appendix-a-errata.html#附录a-原文勘误","appendix/appendix-b-author.html#附录b作者译者","appendix/appendix-b-author.html#英文作者","appendix/appendix-b-author.html#中文译者","appendix/appendix-c-cpoyright.html#附录c译文授权","appendix/appendix-d-translations.html#附录d其它语言"],"index":{"documentStore":{"docInfo":{"0":{"body":40,"breadcrumbs":2,"title":1},"1":{"body":133,"breadcrumbs":0,"title":0},"10":{"body":205,"breadcrumbs":1,"title":1},"100":{"body":205,"breadcrumbs":4,"title":2},"101":{"body":135,"breadcrumbs":4,"title":2},"102":{"body":54,"breadcrumbs":4,"title":2},"103":{"body":57,"breadcrumbs":4,"title":2},"104":{"body":93,"breadcrumbs":4,"title":2},"105":{"body":75,"breadcrumbs":4,"title":2},"106":{"body":66,"breadcrumbs":4,"title":2},"107":{"body":316,"breadcrumbs":2,"title":1},"108":{"body":257,"breadcrumbs":4,"title":2},"109":{"body":154,"breadcrumbs":4,"title":2},"11":{"body":311,"breadcrumbs":1,"title":1},"110":{"body":307,"breadcrumbs":2,"title":1},"111":{"body":106,"breadcrumbs":2,"title":1},"112":{"body":181,"breadcrumbs":2,"title":1},"113":{"body":2,"breadcrumbs":1,"title":1},"114":{"body":279,"breadcrumbs":1,"title":1},"115":{"body":175,"breadcrumbs":3,"title":2},"116":{"body":26,"breadcrumbs":3,"title":2},"117":{"body":43,"breadcrumbs":1,"title":1},"118":{"body":130,"breadcrumbs":3,"title":2},"119":{"body":13,"breadcrumbs":1,"title":1},"12":{"body":169,"breadcrumbs":3,"title":2},"120":{"body":617,"breadcrumbs":1,"title":1},"121":{"body":1,"breadcrumbs":3,"title":2},"122":{"body":4,"breadcrumbs":2,"title":1},"123":{"body":7,"breadcrumbs":3,"title":2},"124":{"body":21,"breadcrumbs":3,"title":2},"125":{"body":9,"breadcrumbs":3,"title":2},"126":{"body":8,"breadcrumbs":1,"title":1},"127":{"body":2,"breadcrumbs":1,"title":1},"128":{"body":13,"breadcrumbs":1,"title":1},"129":{"body":17,"breadcrumbs":1,"title":1},"13":{"body":96,"breadcrumbs":3,"title":2},"130":{"body":27,"breadcrumbs":1,"title":1},"131":{"body":158,"breadcrumbs":1,"title":1},"132":{"body":46,"breadcrumbs":1,"title":1},"133":{"body":66,"breadcrumbs":1,"title":1},"134":{"body":34,"breadcrumbs":1,"title":1},"135":{"body":61,"breadcrumbs":1,"title":1},"136":{"body":117,"breadcrumbs":1,"title":1},"137":{"body":128,"breadcrumbs":1,"title":1},"138":{"body":6,"breadcrumbs":1,"title":1},"139":{"body":156,"breadcrumbs":1,"title":1},"14":{"body":120,"breadcrumbs":3,"title":2},"140":{"body":8,"breadcrumbs":1,"title":1},"141":{"body":9,"breadcrumbs":5,"title":3},"142":{"body":331,"breadcrumbs":1,"title":1},"143":{"body":74,"breadcrumbs":1,"title":1},"144":{"body":173,"breadcrumbs":1,"title":1},"145":{"body":225,"breadcrumbs":1,"title":1},"146":{"body":40,"breadcrumbs":1,"title":1},"147":{"body":55,"breadcrumbs":1,"title":1},"148":{"body":1,"breadcrumbs":1,"title":1},"149":{"body":173,"breadcrumbs":1,"title":1},"15":{"body":234,"breadcrumbs":3,"title":2},"150":{"body":138,"breadcrumbs":1,"title":1},"151":{"body":147,"breadcrumbs":1,"title":1},"152":{"body":22,"breadcrumbs":1,"title":1},"153":{"body":2,"breadcrumbs":1,"title":1},"154":{"body":47,"breadcrumbs":1,"title":1},"155":{"body":216,"breadcrumbs":4,"title":3},"156":{"body":376,"breadcrumbs":3,"title":2},"157":{"body":314,"breadcrumbs":3,"title":2},"158":{"body":152,"breadcrumbs":3,"title":2},"159":{"body":256,"breadcrumbs":3,"title":2},"16":{"body":83,"breadcrumbs":1,"title":1},"160":{"body":246,"breadcrumbs":1,"title":1},"161":{"body":69,"breadcrumbs":1,"title":1},"162":{"body":11,"breadcrumbs":1,"title":1},"163":{"body":8,"breadcrumbs":1,"title":1},"164":{"body":129,"breadcrumbs":7,"title":4},"165":{"body":70,"breadcrumbs":3,"title":2},"166":{"body":217,"breadcrumbs":1,"title":1},"167":{"body":408,"breadcrumbs":3,"title":2},"168":{"body":4,"breadcrumbs":1,"title":1},"169":{"body":1,"breadcrumbs":0,"title":0},"17":{"body":2,"breadcrumbs":1,"title":1},"170":{"body":497,"breadcrumbs":0,"title":0},"171":{"body":0,"breadcrumbs":2,"title":1},"172":{"body":85,"breadcrumbs":1,"title":0},"173":{"body":14,"breadcrumbs":1,"title":0},"174":{"body":3,"breadcrumbs":2,"title":1},"175":{"body":44,"breadcrumbs":2,"title":1},"18":{"body":66,"breadcrumbs":1,"title":1},"19":{"body":79,"breadcrumbs":1,"title":1},"2":{"body":0,"breadcrumbs":0,"title":0},"20":{"body":35,"breadcrumbs":1,"title":1},"21":{"body":62,"breadcrumbs":1,"title":1},"22":{"body":144,"breadcrumbs":1,"title":1},"23":{"body":49,"breadcrumbs":2,"title":2},"24":{"body":54,"breadcrumbs":1,"title":1},"25":{"body":25,"breadcrumbs":1,"title":1},"26":{"body":99,"breadcrumbs":1,"title":1},"27":{"body":14,"breadcrumbs":1,"title":1},"28":{"body":169,"breadcrumbs":1,"title":1},"29":{"body":93,"breadcrumbs":1,"title":1},"3":{"body":29,"breadcrumbs":1,"title":1},"30":{"body":74,"breadcrumbs":1,"title":1},"31":{"body":94,"breadcrumbs":1,"title":1},"32":{"body":203,"breadcrumbs":1,"title":1},"33":{"body":2,"breadcrumbs":1,"title":1},"34":{"body":230,"breadcrumbs":1,"title":1},"35":{"body":309,"breadcrumbs":1,"title":1},"36":{"body":149,"breadcrumbs":1,"title":1},"37":{"body":50,"breadcrumbs":1,"title":1},"38":{"body":63,"breadcrumbs":1,"title":1},"39":{"body":26,"breadcrumbs":1,"title":1},"4":{"body":9,"breadcrumbs":1,"title":1},"40":{"body":8,"breadcrumbs":2,"title":2},"41":{"body":160,"breadcrumbs":3,"title":3},"42":{"body":212,"breadcrumbs":2,"title":2},"43":{"body":38,"breadcrumbs":1,"title":1},"44":{"body":68,"breadcrumbs":1,"title":1},"45":{"body":135,"breadcrumbs":2,"title":2},"46":{"body":191,"breadcrumbs":1,"title":1},"47":{"body":2,"breadcrumbs":1,"title":1},"48":{"body":161,"breadcrumbs":1,"title":1},"49":{"body":182,"breadcrumbs":3,"title":2},"5":{"body":28,"breadcrumbs":0,"title":0},"50":{"body":244,"breadcrumbs":3,"title":2},"51":{"body":153,"breadcrumbs":3,"title":2},"52":{"body":347,"breadcrumbs":3,"title":2},"53":{"body":187,"breadcrumbs":1,"title":1},"54":{"body":96,"breadcrumbs":1,"title":1},"55":{"body":32,"breadcrumbs":1,"title":1},"56":{"body":165,"breadcrumbs":1,"title":1},"57":{"body":466,"breadcrumbs":3,"title":2},"58":{"body":191,"breadcrumbs":3,"title":2},"59":{"body":1,"breadcrumbs":1,"title":1},"6":{"body":24,"breadcrumbs":0,"title":0},"60":{"body":106,"breadcrumbs":1,"title":1},"61":{"body":238,"breadcrumbs":1,"title":1},"62":{"body":195,"breadcrumbs":1,"title":1},"63":{"body":24,"breadcrumbs":1,"title":1},"64":{"body":168,"breadcrumbs":1,"title":1},"65":{"body":35,"breadcrumbs":2,"title":2},"66":{"body":180,"breadcrumbs":1,"title":1},"67":{"body":383,"breadcrumbs":1,"title":1},"68":{"body":78,"breadcrumbs":1,"title":1},"69":{"body":95,"breadcrumbs":1,"title":1},"7":{"body":52,"breadcrumbs":0,"title":0},"70":{"body":368,"breadcrumbs":3,"title":2},"71":{"body":140,"breadcrumbs":3,"title":2},"72":{"body":119,"breadcrumbs":3,"title":2},"73":{"body":19,"breadcrumbs":1,"title":1},"74":{"body":128,"breadcrumbs":1,"title":1},"75":{"body":80,"breadcrumbs":1,"title":1},"76":{"body":122,"breadcrumbs":2,"title":2},"77":{"body":160,"breadcrumbs":1,"title":1},"78":{"body":150,"breadcrumbs":1,"title":1},"79":{"body":210,"breadcrumbs":3,"title":2},"8":{"body":2,"breadcrumbs":1,"title":1},"80":{"body":121,"breadcrumbs":1,"title":1},"81":{"body":2,"breadcrumbs":1,"title":1},"82":{"body":178,"breadcrumbs":1,"title":1},"83":{"body":65,"breadcrumbs":1,"title":1},"84":{"body":233,"breadcrumbs":1,"title":1},"85":{"body":212,"breadcrumbs":3,"title":2},"86":{"body":86,"breadcrumbs":1,"title":1},"87":{"body":58,"breadcrumbs":2,"title":2},"88":{"body":472,"breadcrumbs":3,"title":2},"89":{"body":280,"breadcrumbs":3,"title":2},"9":{"body":111,"breadcrumbs":5,"title":3},"90":{"body":102,"breadcrumbs":3,"title":2},"91":{"body":566,"breadcrumbs":1,"title":1},"92":{"body":86,"breadcrumbs":1,"title":1},"93":{"body":115,"breadcrumbs":1,"title":1},"94":{"body":123,"breadcrumbs":1,"title":1},"95":{"body":146,"breadcrumbs":1,"title":1},"96":{"body":268,"breadcrumbs":3,"title":2},"97":{"body":6,"breadcrumbs":1,"title":1},"98":{"body":5,"breadcrumbs":3,"title":2},"99":{"body":63,"breadcrumbs":4,"title":2}},"docs":{"0":{"body":"Go语言圣经 《The Go Programming Language》 中文版本,仅供学习交流之用。对于希望学习CGO、Go汇编语言等高级用法的同学,我们推荐 《Go语言高级编程》 开源图书。如果希望深入学习Go语言语法树结构,可以参考 《Go语法树入门——开启自制编程语言和编译器之旅》 。如果想从头实现一个玩具Go语言可以参考 《从头实现µGo语言》 (µGo 是 凹语言 阶段的产物)。 在线阅读: https://gopl-zh.github.io 在线阅读: https://golang-china.github.io/gopl-zh 项目主页: https://github.com/gopl-zh 项目主页(旧): http://github.com/golang-china/gopl-zh 原版官网: http://gopl.io 译者信息: 译者:柴树杉,Github @chai2010 ,Twitter @chaishushan 译者:Xargin, https://github.com/cch123 译者:CrazySssst, https://github.com/CrazySssst 译者:foreversmart, https://github.com/foreversmart njutree@gmail.com Go 语言中国: Go 语言中国: https://github.com/golang-china Go 语言中国论坛: https://github.com/golang-china/main.go/discussions","breadcrumbs":"Go语言圣经 » Go语言圣经(中文版)","id":"0","title":"Go语言圣经(中文版)"},"1":{"body":"在上个世纪70年代,贝尔实验室的 Ken Thompson 和 Dennis M. Ritchie 合作发明了 UNIX 操作系统,同时 Dennis M. Ritchie 为了解决 UNIX 系统的移植性问题而发明了 C 语言,贝尔实验室的 UNIX 和 C 语言两大发明奠定了整个现代IT行业最重要的软件基础(目前的三大桌面操作系统的中 Linux 和 Mac OS X 都是源于 UNIX 系统,两大移动平台的操作系统 iOS 和 Android 也都是源于 UNIX 系统。C 系家族的编程语言占据统治地位达几十年之久)。在 UNIX 和 C 语言发明40年之后,目前已经在 Google 工作的 Ken Thompson 和 Rob Pike (他们在贝尔实验室时就是同事)、还有 Robert Griesemer (设计了 V8 引擎和 HotSpot 虚拟机)一起合作,为了解决在21世纪多核和网络化环境下越来越复杂的编程问题而发明了 Go 语言。从 Go 语言库早期代码库日志可以看出它的演化历程( Git 用 git log --before={2008-03-03} --reverse 命令查看): 从早期提交日志中也可以看出,Go 语言是从 Ken Thompson 发明的 B 语言、 Dennis M. Ritchie 发明的 C 语言逐步演化过来的,是 C 语言家族的成员,因此很多人将 Go 语言称为 21 世纪的 C 语言。纵观这几年来的发展趋势,Go 语言已经成为云计算、云存储时代最重要的基础编程语言。 在 C 语言发明之后约5年的时间之后(1978年), Brian W. Kernighan 和 Dennis M. Ritchie 合作编写出版了C语言方面的经典教材《 The C Programming Language 》,该书被誉为 C 语言程序员的圣经,作者也被大家亲切地称为 K&R 。同样在 Go 语言正式发布(2009 年)约 5 年之后(2014 年开始写作,2015 年出版),由 Go 语言核心团队成员 Alan A. A. Donovan 和 K&R 中的 Brian W. Kernighan 合作编写了Go语言方面的经典教材《 The Go Programming Language 》。Go 语言被誉为 21 世纪的 C 语言,如果说 K&R 所著的是圣经的旧约,那么 D&K 所著的必将成为圣经的新约。该书介绍了 Go 语言几乎全部特性,并且随着语言的深入层层递进,对每个细节都解读得非常细致,每一节内容都精彩不容错过,是广大 Gopher 的必读书目。大部分 Go 语言核心团队的成员都参与了该书校对工作,因此该书的质量是可以完全放心的。 同时,单凭阅读和学习其语法结构并不能真正地掌握一门编程语言,必须进行足够多的编程实践——亲自编写一些程序并研究学习别人写的程序。要从利用 Go 语言良好的特性使得程序模块化,充分利用 Go 的标准函数库以 Go 语言自己的风格来编写程序。书中包含了上百个精心挑选的习题,希望大家能先用自己的方式尝试完成习题,然后再参考官方给出的解决方案。 该书英文版约从 2015 年 10 月开始公开发售,其中日文版本最早参与翻译和审校(参考致谢部分)。在 2015 年 10 月,我们并不知道中文版是否会及时引进、将由哪家出版社引进、引进将由何人来翻译、何时能出版,这些信息都成了一个秘密。中国的 Go 语言社区是全球最大的Go语言社区,我们从一开始就始终紧跟着 Go 语言的发展脚步。我们应该也完全有能力以中国 Go 语言社区的力量同步完成 Go 语言圣经中文版的翻译工作。与此同时,国内有很多 Go 语言爱好者也在积极关注该书(本人也在第一时间购买了纸质版本, 亚马逊价格314人民币 。补充:国内也即将出版英文版, 价格79元 )。为了 Go 语言的学习和交流,大家决定合作免费翻译该书。 翻译工作从 2015 年 11 月 20 日前后开始,到 2016 年 1 月底初步完成,前后历时约 2 个月时间(在其它语言版本中,全球第一个完成翻译的,基本做到和原版同步)。其中, chai2010 翻译了前言、第2 ~ 4章、第10 ~ 13章, Xargin 翻译了第1章、第6章、第8 ~ 9章, CrazySssst 翻译了第5章, foreversmart 翻译了第7章,大家共同参与了基本的校验工作,还有其他一些朋友提供了积极的反馈建议。如果大家还有任何问题或建议,可以直接到中文版项目页面提交 Issue ,如果发现英文版原文在 勘误 中未提到的任何错误,可以直接去 英文版项目 提交。 最后,希望这本书能够帮助大家用Go语言快乐地编程。 2016年 1月 于 武汉","breadcrumbs":"译者序 » 译者序","id":"1","title":"译者序"},"10":{"body":"大多数的程序都是处理输入,产生输出;这也正是“计算”的定义。但是,程序如何获取要处理的输入数据呢?一些程序生成自己的数据,但通常情况下,输入来自于程序外部:文件、网络连接、其它程序的输出、敲键盘的用户、命令行参数或其它类似输入源。下面几个例子会讨论其中几个输入源,首先是命令行参数。 os 包以跨平台的方式,提供了一些与操作系统交互的函数和变量。程序的命令行参数可从 os 包的 Args 变量获取;os 包外部使用 os.Args 访问该变量。 os.Args 变量是一个字符串(string)的 切片 (slice)(译注:slice 和 Python 语言中的切片类似,是一个简版的动态数组),切片是 Go 语言的基础概念,稍后详细介绍。现在先把切片 s 当作数组元素序列,序列的长度动态变化,用 s[i] 访问单个元素,用 s[m:n] 获取子序列(译注:和 Python 里的语法差不多)。序列的元素数目为 len(s)。和大多数编程语言类似,区间索引时,Go 语言里也采用左闭右开形式,即,区间包括第一个索引元素,不包括最后一个,因为这样可以简化逻辑。(译注:比如 a=[1,2,3,4,5], a[0:3]=[1,2,3],不包含最后一个元素)。比如 s[m:n] 这个切片,0≤m≤n≤len(s),包含 n-m 个元素。 os.Args 的第一个元素:os.Args[0],是命令本身的名字;其它的元素则是程序启动时传给它的参数。s[m:n] 形式的切片表达式,产生从第 m 个元素到第 n-1 个元素的切片,下个例子用到的元素包含在 os.Args[1:len(os.Args)] 切片中。如果省略切片表达式的 m 或 n,会默认传入 0 或 len(s),因此前面的切片可以简写成 os.Args[1:]。 下面是 Unix 里 echo 命令的一份实现,echo 把它的命令行参数打印成一行。程序导入了两个包,用括号把它们括起来写成列表形式,而没有分开写成独立的 import 声明。两种形式都合法,列表形式习惯上用得多。包导入顺序并不重要;gofmt 工具格式化时按照字母顺序对包名排序。(示例有多个版本时,我们会对示例编号,这样可以明确当前正在讨论的是哪个。) gopl.io/ch1/echo1 // Echo1 prints its command-line arguments.\npackage main import ( \"fmt\" \"os\"\n) func main() { var s, sep string for i := 1; i < len(os.Args); i++ { s += sep + os.Args[i] sep = \" \" } fmt.Println(s)\n} 注释语句以 // 开头。对于程序员来说,// 之后到行末之间所有的内容都是注释,被编译器忽略。按照惯例,我们在每个包的包声明前添加注释;对于 main package,注释包含一句或几句话,从整体角度对程序做个描述。 var 声明定义了两个 string 类型的变量 s 和 sep。变量会在声明时直接初始化。如果变量没有显式初始化,则被隐式地赋予其类型的 零值 (zero value),数值类型是 0,字符串类型是空字符串 \"\"。这个例子里,声明把 s 和 sep 隐式地初始化成空字符串。第 2 章再来详细地讲解变量和声明。 对数值类型,Go 语言提供了常规的数值和逻辑运算符。而对 string 类型,+ 运算符连接字符串(译注:和 C++ 或者 JavaScript 是一样的)。所以表达式:sep + os.Args[i] 表示连接字符串 sep 和 os.Args[i]。程序中使用的语句:s+=sep+os.Args[i] 是一条 赋值语句 ,将 s 的旧值跟 sep 与 os.Args[i] 连接后赋值回 s,等价于:s=s+sep+os.Args[i]。 运算符 += 是赋值运算符(assignment operator),每种数值运算符或逻辑运算符,如 + 或 *,都有对应的赋值运算符。 echo 程序可以每循环一次输出一个参数,这个版本却是不断地把新文本追加到末尾来构造字符串。字符串 s 开始为空,即值为 \"\",每次循环会添加一些文本;第一次迭代之后,还会再插入一个空格,因此循环结束时每个参数中间都有一个空格。这是一种二次加工(quadratic process),当参数数量庞大时,开销很大,但是对于 echo,这种情形不大可能出现。本章会介绍 echo 的若干改进版,下一章解决低效问题。 循环索引变量 i 在 for 循环的第一部分中定义。符号 := 是 短变量声明 (short variable declaration)的一部分,这是定义一个或多个变量并根据它们的初始值为这些变量赋予适当类型的语句。下一章有这方面更多说明。 自增语句 i++ 给 i 加 1;这和 i+=1 以及 i=i+1 都是等价的。对应的还有 i-- 给 i 减 1。它们是语句,而不像 C 系的其它语言那样是表达式。所以 j=i++ 非法,而且 ++ 和 -- 都只能放在变量名后面,因此 --i 也非法。 Go 语言只有 for 循环这一种循环语句。for 循环有多种形式,其中一种如下所示: for initialization; condition; post { // zero or more statements\n} for 循环三个部分不需括号包围。大括号强制要求,左大括号必须和 post 语句在同一行。 initialization 语句是可选的,在循环开始前执行。 initalization 如果存在,必须是一条 简单语句 (simple statement),即,短变量声明、自增语句、赋值语句或函数调用。condition 是一个布尔表达式(boolean expression),其值在每次循环迭代开始时计算。如果为 true 则执行循环体语句。post 语句在循环体执行结束后执行,之后再次对 condition 求值。condition 值为 false 时,循环结束。 for 循环的这三个部分每个都可以省略,如果省略 initialization 和 post,分号也可以省略: // a traditional \"while\" loop\nfor condition { // ...\n} 如果连 condition 也省略了,像下面这样: // a traditional infinite loop\nfor { // ...\n} 这就变成一个无限循环,尽管如此,还可以用其他方式终止循环,如一条 break 或 return 语句。 for 循环的另一种形式,在某种数据类型的区间(range)上遍历,如字符串或切片。echo 的第二版本展示了这种形式: gopl.io/ch1/echo2 // Echo2 prints its command-line arguments.\npackage main import ( \"fmt\" \"os\"\n) func main() { s, sep := \"\", \"\" for _, arg := range os.Args[1:] { s += sep + arg sep = \" \" } fmt.Println(s)\n} 每次循环迭代,range 产生一对值;索引以及在该索引处的元素值。这个例子不需要索引,但 range 的语法要求,要处理元素,必须处理索引。一种思路是把索引赋值给一个临时变量(如 temp)然后忽略它的值,但 Go 语言不允许使用无用的局部变量(local variables),因为这会导致编译错误。 Go 语言中这种情况的解决方法是用 空标识符 (blank identifier),即 _(也就是下划线)。空标识符可用于在任何语法需要变量名但程序逻辑不需要的时候(如:在循环里)丢弃不需要的循环索引,并保留元素值。大多数的 Go 程序员都会像上面这样使用 range 和 _ 写 echo 程序,因为隐式地而非显式地索引 os.Args,容易写对。 echo 的这个版本使用一条短变量声明来声明并初始化 s 和 seps,也可以将这两个变量分开声明,声明一个变量有好几种方式,下面这些都等价: s := \"\"\nvar s string\nvar s = \"\"\nvar s string = \"\" 用哪种不用哪种,为什么呢?第一种形式,是一条短变量声明,最简洁,但只能用在函数内部,而不能用于包变量。第二种形式依赖于字符串的默认初始化零值机制,被初始化为 \"\"。第三种形式用得很少,除非同时声明多个变量。第四种形式显式地标明变量的类型,当变量类型与初值类型相同时,类型冗余,但如果两者类型不同,变量类型就必须了。实践中一般使用前两种形式中的某个,初始值重要的话就显式地指定变量的类型,否则使用隐式初始化。 如前文所述,每次循环迭代字符串 s 的内容都会更新。+= 连接原字符串、空格和下个参数,产生新字符串,并把它赋值给 s。s 原来的内容已经不再使用,将在适当时机对它进行垃圾回收。 如果连接涉及的数据量很大,这种方式代价高昂。一种简单且高效的解决方案是使用 strings 包的 Join 函数: gopl.io/ch1/echo3 func main() { fmt.Println(strings.Join(os.Args[1:], \" \"))\n} 最后,如果不关心输出格式,只想看看输出值,或许只是为了调试,可以用 Println 为我们格式化输出。 fmt.Println(os.Args[1:]) 这条语句的输出结果跟 strings.Join 得到的结果很像,只是被放到了一对方括号里。切片都会被打印成这种格式。 练习 1.1: 修改 echo 程序,使其能够打印 os.Args[0],即被执行命令本身的名字。 练习 1.2: 修改 echo 程序,使其打印每个参数的索引和值,每个一行。 练习 1.3: 做实验测量潜在低效的版本和使用了 strings.Join 的版本的运行时间差异。( 1.6 节 讲解了部分 time 包, 11.4 节 展示了如何写标准测试程序,以得到系统性的性能评测。)","breadcrumbs":"入门 » 命令行参数 » 1.2. 命令行参数","id":"10","title":"1.2. 命令行参数"},"100":{"body":"网络编程是并发大显身手的一个领域,由于服务器是最典型的需要同时处理很多连接的程序,这些连接一般来自于彼此独立的客户端。在本小节中,我们会讲解go语言的net包,这个包提供编写一个网络客户端或者服务器程序的基本组件,无论两者间通信是使用TCP、UDP或者Unix domain sockets。在第一章中我们使用过的net/http包里的方法,也算是net包的一部分。 我们的第一个例子是一个顺序执行的时钟服务器,它会每隔一秒钟将当前时间写到客户端: gopl.io/ch8/clock1 // Clock1 is a TCP server that periodically writes the time.\npackage main import ( \"io\" \"log\" \"net\" \"time\"\n) func main() { listener, err := net.Listen(\"tcp\", \"localhost:8000\") if err != nil { log.Fatal(err) } for { conn, err := listener.Accept() if err != nil { log.Print(err) // e.g., connection aborted continue } handleConn(conn) // handle one connection at a time }\n} func handleConn(c net.Conn) { defer c.Close() for { _, err := io.WriteString(c, time.Now().Format(\"15:04:05\\n\")) if err != nil { return // e.g., client disconnected } time.Sleep(1 * time.Second) }\n} Listen函数创建了一个net.Listener的对象,这个对象会监听一个网络端口上到来的连接,在这个例子里我们用的是TCP的localhost:8000端口。listener对象的Accept方法会直接阻塞,直到一个新的连接被创建,然后会返回一个net.Conn对象来表示这个连接。 handleConn函数会处理一个完整的客户端连接。在一个for死循环中,用time.Now()获取当前时刻,然后写到客户端。由于net.Conn实现了io.Writer接口,我们可以直接向其写入内容。这个死循环会一直执行,直到写入失败。最可能的原因是客户端主动断开连接。这种情况下handleConn函数会用defer调用关闭服务器侧的连接,然后返回到主函数,继续等待下一个连接请求。 time.Time.Format方法提供了一种格式化日期和时间信息的方式。它的参数是一个格式化模板,标识如何来格式化时间,而这个格式化模板限定为Mon Jan 2 03:04:05PM 2006 UTC-0700。有8个部分(周几、月份、一个月的第几天……)。可以以任意的形式来组合前面这个模板;出现在模板中的部分会作为参考来对时间格式进行输出。在上面的例子中我们只用到了小时、分钟和秒。time包里定义了很多标准时间格式,比如time.RFC1123。在进行格式化的逆向操作time.Parse时,也会用到同样的策略。(译注:这是go语言和其它语言相比比较奇葩的一个地方。你需要记住格式化字符串是1月2日下午3点4分5秒零六年UTC-0700,而不像其它语言那样Y-m-d H:i:s一样,当然了这里可以用1234567的方式来记忆,倒是也不麻烦。) 为了连接例子里的服务器,我们需要一个客户端程序,比如netcat这个工具(nc命令),这个工具可以用来执行网络连接操作。 $ go build gopl.io/ch8/clock1\n$ ./clock1 &\n$ nc localhost 8000\n13:58:54\n13:58:55\n13:58:56\n13:58:57\n^C 客户端将服务器发来的时间显示了出来,我们用Control+C来中断客户端的执行,在Unix系统上,你会看到^C这样的响应。如果你的系统没有装nc这个工具,你可以用telnet来实现同样的效果,或者也可以用我们下面的这个用go写的简单的telnet程序,用net.Dial就可以简单地创建一个TCP连接: gopl.io/ch8/netcat1 // Netcat1 is a read-only TCP client.\npackage main import ( \"io\" \"log\" \"net\" \"os\"\n) func main() { conn, err := net.Dial(\"tcp\", \"localhost:8000\") if err != nil { log.Fatal(err) } defer conn.Close() mustCopy(os.Stdout, conn)\n} func mustCopy(dst io.Writer, src io.Reader) { if _, err := io.Copy(dst, src); err != nil { log.Fatal(err) }\n} 这个程序会从连接中读取数据,并将读到的内容写到标准输出中,直到遇到end of file的条件或者发生错误。mustCopy这个函数我们在本节的几个例子中都会用到。让我们同时运行两个客户端来进行一个测试,这里可以开两个终端窗口,下面左边的是其中的一个的输出,右边的是另一个的输出: $ go build gopl.io/ch8/netcat1\n$ ./netcat1\n13:58:54 $ ./netcat1\n13:58:55\n13:58:56\n^C 13:58:57 13:58:58 13:58:59 ^C\n$ killall clock1 killall命令是一个Unix命令行工具,可以用给定的进程名来杀掉所有名字匹配的进程。 第二个客户端必须等待第一个客户端完成工作,这样服务端才能继续向后执行;因为我们这里的服务器程序同一时间只能处理一个客户端连接。我们这里对服务端程序做一点小改动,使其支持并发:在handleConn函数调用的地方增加go关键字,让每一次handleConn的调用都进入一个独立的goroutine。 gopl.io/ch8/clock2 for { conn, err := listener.Accept() if err != nil { log.Print(err) // e.g., connection aborted continue } go handleConn(conn) // handle connections concurrently\n} 现在多个客户端可以同时接收到时间了: $ go build gopl.io/ch8/clock2\n$ ./clock2 &\n$ go build gopl.io/ch8/netcat1\n$ ./netcat1\n14:02:54 $ ./netcat1\n14:02:55 14:02:55\n14:02:56 14:02:56\n14:02:57 ^C\n14:02:58\n14:02:59 $ ./netcat1\n14:03:00 14:03:00\n14:03:01 14:03:01\n^C 14:03:02 ^C\n$ killall clock2 练习 8.1: 修改clock2来支持传入参数作为端口号,然后写一个clockwall的程序,这个程序可以同时与多个clock服务器通信,从多个服务器中读取时间,并且在一个表格中一次显示所有服务器传回的结果,类似于你在某些办公室里看到的时钟墙。如果你有地理学上分布式的服务器可以用的话,让这些服务器跑在不同的机器上面;或者在同一台机器上跑多个不同的实例,这些实例监听不同的端口,假装自己在不同的时区。像下面这样: $ TZ=US/Eastern ./clock2 -port 8010 &\n$ TZ=Asia/Tokyo ./clock2 -port 8020 &\n$ TZ=Europe/London ./clock2 -port 8030 &\n$ clockwall NewYork=localhost:8010 Tokyo=localhost:8020 London=localhost:8030 练习 8.2: 实现一个并发FTP服务器。服务器应该解析客户端发来的一些命令,比如cd命令来切换目录,ls来列出目录内文件,get和send来传输文件,close来关闭连接。你可以用标准的ftp命令来作为客户端,或者也可以自己实现一个。","breadcrumbs":"Goroutines和Channels » 示例: 并发的Clock服务 » 8.2. 示例: 并发的Clock服务","id":"100","title":"8.2. 示例: 并发的Clock服务"},"101":{"body":"clock服务器每一个连接都会起一个goroutine。在本节中我们会创建一个echo服务器,这个服务在每个连接中会有多个goroutine。大多数echo服务仅仅会返回他们读取到的内容,就像下面这个简单的handleConn函数所做的一样: func handleConn(c net.Conn) { io.Copy(c, c) // NOTE: ignoring errors c.Close()\n} 一个更有意思的echo服务应该模拟一个实际的echo的“回响”,并且一开始要用大写HELLO来表示“声音很大”,之后经过一小段延迟返回一个有所缓和的Hello,然后一个全小写字母的hello表示声音渐渐变小直至消失,像下面这个版本的handleConn(译注:笑看作者脑洞大开): gopl.io/ch8/reverb1 func echo(c net.Conn, shout string, delay time.Duration) { fmt.Fprintln(c, \"\\t\", strings.ToUpper(shout)) time.Sleep(delay) fmt.Fprintln(c, \"\\t\", shout) time.Sleep(delay) fmt.Fprintln(c, \"\\t\", strings.ToLower(shout))\n} func handleConn(c net.Conn) { input := bufio.NewScanner(c) for input.Scan() { echo(c, input.Text(), 1*time.Second) } // NOTE: ignoring potential errors from input.Err() c.Close()\n} 我们需要升级我们的客户端程序,这样它就可以发送终端的输入到服务器,并把服务端的返回输出到终端上,这使我们有了使用并发的另一个好机会: gopl.io/ch8/netcat2 func main() { conn, err := net.Dial(\"tcp\", \"localhost:8000\") if err != nil { log.Fatal(err) } defer conn.Close() go mustCopy(os.Stdout, conn) mustCopy(conn, os.Stdin)\n} 当main goroutine从标准输入流中读取内容并将其发送给服务器时,另一个goroutine会读取并打印服务端的响应。当main goroutine碰到输入终止时,例如,用户在终端中按了Control-D(^D),在windows上是Control-Z,这时程序就会被终止,尽管其它goroutine中还有进行中的任务。(在8.4.1中引入了channels后我们会明白如何让程序等待两边都结束。) 下面这个会话中,客户端的输入是左对齐的,服务端的响应会用缩进来区别显示。 客户端会向服务器“喊三次话”: $ go build gopl.io/ch8/reverb1\n$ ./reverb1 &\n$ go build gopl.io/ch8/netcat2\n$ ./netcat2\nHello? HELLO? Hello? hello?\nIs there anybody there? IS THERE ANYBODY THERE?\nYooo-hooo! Is there anybody there? is there anybody there? YOOO-HOOO! Yooo-hooo! yooo-hooo!\n^D\n$ killall reverb1 注意客户端的第三次shout在前一个shout处理完成之前一直没有被处理,这貌似看起来不是特别“现实”。真实世界里的回响应该是会由三次shout的回声组合而成的。为了模拟真实世界的回响,我们需要更多的goroutine来做这件事情。这样我们就再一次地需要go这个关键词了,这次我们用它来调用echo: gopl.io/ch8/reverb2 func handleConn(c net.Conn) { input := bufio.NewScanner(c) for input.Scan() { go echo(c, input.Text(), 1*time.Second) } // NOTE: ignoring potential errors from input.Err() c.Close()\n} go后跟的函数的参数会在go语句自身执行时被求值;因此input.Text()会在main goroutine中被求值。 现在回响是并发并且会按时间来覆盖掉其它响应了: $ go build gopl.io/ch8/reverb2\n$ ./reverb2 &\n$ ./netcat2\nIs there anybody there? IS THERE ANYBODY THERE?\nYooo-hooo! Is there anybody there? YOOO-HOOO! is there anybody there? Yooo-hooo! yooo-hooo!\n^D\n$ killall reverb2 让服务使用并发不只是处理多个客户端的请求,甚至在处理单个连接时也可能会用到,就像我们上面的两个go关键词的用法。然而在我们使用go关键词的同时,需要慎重地考虑net.Conn中的方法在并发地调用时是否安全,事实上对于大多数类型来说也确实不安全。我们会在下一章中详细地探讨并发安全性。","breadcrumbs":"Goroutines和Channels » 示例: 并发的Echo服务 » 8.3. 示例: 并发的Echo服务","id":"101","title":"8.3. 示例: 并发的Echo服务"},"102":{"body":"如果说goroutine是Go语言程序的并发体的话,那么channels则是它们之间的通信机制。一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int。 使用内置的make函数,我们可以创建一个channel: ch := make(chan int) // ch has type 'chan int' 和map类似,channel也对应一个make创建的底层数据结构的引用。当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。 两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相同的对象,那么比较的结果为真。一个channel也可以和nil进行比较。 一个channel有发送和接受两个主要操作,都是通信行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine。发送和接收两个操作都使用<-运算符。在发送语句中,<-运算符分割channel和要发送的值。在接收语句中,<-运算符写在channel对象之前。一个不使用接收结果的接收操作也是合法的。 ch <- x // a send statement\nx = <-ch // a receive expression in an assignment statement\n<-ch // a receive statement; result is discarded Channel还支持close操作,用于关闭channel,随后对基于该channel的任何发送操作都将导致panic异常。对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话将产生一个零值的数据。 使用内置的close函数就可以关闭一个channel: close(ch) 以最简单方式调用make函数创建的是一个无缓存的channel,但是我们也可以指定第二个整型参数,对应channel的容量。如果channel的容量大于零,那么该channel就是带缓存的channel。 ch = make(chan int) // unbuffered channel\nch = make(chan int, 0) // unbuffered channel\nch = make(chan int, 3) // buffered channel with capacity 3 我们将先讨论无缓存的channel,然后在8.4.4节讨论带缓存的channel。","breadcrumbs":"Goroutines和Channels » Channels » 8.4. Channels","id":"102","title":"8.4. Channels"},"103":{"body":"一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。 基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。因为这个原因,无缓存Channels有时候也被称为同步Channels。当通过一个无缓存Channels发送数据时,接收者收到数据发生在再次唤醒发送者goroutine之前(译注: happens before ,这是Go语言并发内存模型的一个关键术语!)。 在讨论并发编程时,当我们说x事件在y事件之前发生( happens before ),我们并不是说x事件在时间上比y时间更早;我们要表达的意思是要保证在此之前的事件都已经完成了,例如在此之前的更新某些变量的操作已经完成,你可以放心依赖这些已完成的事件了。 当我们说x事件既不是在y事件之前发生也不是在y事件之后发生,我们就说x事件和y事件是并发的。这并不是意味着x事件和y事件就一定是同时发生的,我们只是不能确定这两个事件发生的先后顺序。在下一章中我们将看到,当两个goroutine并发访问了相同的变量时,我们有必要保证某些事件的执行顺序,以避免出现某些并发问题。 在8.3节的客户端程序,它在主goroutine中(译注:就是执行main函数的goroutine)将标准输入复制到server,因此当客户端程序关闭标准输入时,后台goroutine可能依然在工作。我们需要让主goroutine等待后台goroutine完成工作后再退出,我们使用了一个channel来同步两个goroutine: gopl.io/ch8/netcat3 func main() { conn, err := net.Dial(\"tcp\", \"localhost:8000\") if err != nil { log.Fatal(err) } done := make(chan struct{}) go func() { io.Copy(os.Stdout, conn) // NOTE: ignoring errors log.Println(\"done\") done <- struct{}{} // signal the main goroutine }() mustCopy(conn, os.Stdin) conn.Close() <-done // wait for background goroutine to finish\n} 当用户关闭了标准输入,主goroutine中的mustCopy函数调用将返回,然后调用conn.Close()关闭读和写方向的网络连接。关闭网络连接中的写方向的连接将导致server程序收到一个文件(end-of-file)结束的信号。关闭网络连接中读方向的连接将导致后台goroutine的io.Copy函数调用返回一个“read from closed connection”(“从关闭的连接读”)类似的错误,因此我们临时移除了错误日志语句;在练习8.3将会提供一个更好的解决方案。(需要注意的是go语句调用了一个函数字面量,这是Go语言中启动goroutine常用的形式。) 在后台goroutine返回之前,它先打印一个日志信息,然后向done对应的channel发送一个值。主goroutine在退出前先等待从done对应的channel接收一个值。因此,总是可以在程序退出前正确输出“done”消息。 基于channels发送消息有两个重要方面。首先每个消息都有一个值,但是有时候通讯的事实和发生的时刻也同样重要。当我们更希望强调通讯发生的时刻时,我们将它称为 消息事件 。有些消息事件并不携带额外的信息,它仅仅是用作两个goroutine之间的同步,这时候我们可以用struct{}空结构体作为channels元素的类型,虽然也可以使用bool或int类型实现同样的功能,done <- 1语句也比done <- struct{}{}更短。 练习 8.3: 在netcat3例子中,conn虽然是一个interface类型的值,但是其底层真实类型是*net.TCPConn,代表一个TCP连接。一个TCP连接有读和写两个部分,可以使用CloseRead和CloseWrite方法分别关闭它们。修改netcat3的主goroutine代码,只关闭网络连接中写的部分,这样的话后台goroutine可以在标准输入被关闭后继续打印从reverb1服务器传回的数据。(要在reverb2服务器也完成同样的功能是比较困难的;参考 练习 8.4 。)","breadcrumbs":"Goroutines和Channels » Channels » 8.4.1. 不带缓存的Channels","id":"103","title":"8.4.1. 不带缓存的Channels"},"104":{"body":"Channels也可以用于将多个goroutine连接在一起,一个Channel的输出作为下一个Channel的输入。这种串联的Channels就是所谓的管道(pipeline)。下面的程序用两个channels将三个goroutine串联起来,如图8.1所示。 第一个goroutine是一个计数器,用于生成0、1、2、……形式的整数序列,然后通过channel将该整数序列发送给第二个goroutine;第二个goroutine是一个求平方的程序,对收到的每个整数求平方,然后将平方后的结果通过第二个channel发送给第三个goroutine;第三个goroutine是一个打印程序,打印收到的每个整数。为了保持例子清晰,我们有意选择了非常简单的函数,当然三个goroutine的计算很简单,在现实中确实没有必要为如此简单的运算构建三个goroutine。 gopl.io/ch8/pipeline1 func main() { naturals := make(chan int) squares := make(chan int) // Counter go func() { for x := 0; ; x++ { naturals <- x } }() // Squarer go func() { for { x := <-naturals squares <- x * x } }() // Printer (in main goroutine) for { fmt.Println(<-squares) }\n} 如您所料,上面的程序将生成0、1、4、9、……形式的无穷数列。像这样的串联Channels的管道(Pipelines)可以用在需要长时间运行的服务中,每个长时间运行的goroutine可能会包含一个死循环,在不同goroutine的死循环内部使用串联的Channels来通信。但是,如果我们希望通过Channels只发送有限的数列该如何处理呢? 如果发送者知道,没有更多的值需要发送到channel的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭channel实现: close(naturals) 当一个channel被关闭后,再向该channel发送数据将导致panic异常。当一个被关闭的channel中已经发送的数据都被成功接收后,后续的接收操作将不再阻塞,它们会立即返回一个零值。关闭上面例子中的naturals变量对应的channel并不能终止循环,它依然会收到一个永无休止的零值序列,然后将它们发送给打印者goroutine。 没有办法直接测试一个channel是否被关闭,但是接收操作有一个变体形式:它多接收一个结果,多接收的第二个结果是一个布尔值ok,ture表示成功从channels接收到值,false表示channels已经被关闭并且里面没有值可接收。使用这个特性,我们可以修改squarer函数中的循环代码,当naturals对应的channel被关闭并没有值可接收时跳出循环,并且也关闭squares对应的channel. // Squarer\ngo func() { for { x, ok := <-naturals if !ok { break // channel was closed and drained } squares <- x * x } close(squares)\n}() 因为上面的语法是笨拙的,而且这种处理模式很常见,因此Go语言的range循环可直接在channels上面迭代。使用range循环是上面处理模式的简洁语法,它依次从channel接收数据,当channel被关闭并且没有值可接收时跳出循环。 在下面的改进中,我们的计数器goroutine只生成100个含数字的序列,然后关闭naturals对应的channel,这将导致计算平方数的squarer对应的goroutine可以正常终止循环并关闭squares对应的channel。(在一个更复杂的程序中,可以通过defer语句关闭对应的channel。)最后,主goroutine也可以正常终止循环并退出程序。 gopl.io/ch8/pipeline2 func main() { naturals := make(chan int) squares := make(chan int) // Counter go func() { for x := 0; x < 100; x++ { naturals <- x } close(naturals) }() // Squarer go func() { for x := range naturals { squares <- x * x } close(squares) }() // Printer (in main goroutine) for x := range squares { fmt.Println(x) }\n} 其实你并不需要关闭每一个channel。只有当需要告诉接收者goroutine,所有的数据已经全部发送时才需要关闭channel。不管一个channel是否被关闭,当它没有被引用时将会被Go语言的垃圾自动回收器回收。(不要将关闭一个打开文件的操作和关闭一个channel操作混淆。对于每个打开的文件,都需要在不使用的时候调用对应的Close方法来关闭文件。) 试图重复关闭一个channel将导致panic异常,试图关闭一个nil值的channel也将导致panic异常。关闭一个channels还会触发一个广播机制,我们将在8.9节讨论。","breadcrumbs":"Goroutines和Channels » Channels » 8.4.2. 串联的Channels(Pipeline)","id":"104","title":"8.4.2. 串联的Channels(Pipeline)"},"105":{"body":"随着程序的增长,人们习惯于将大的函数拆分为小的函数。我们前面的例子中使用了三个goroutine,然后用两个channels来连接它们,它们都是main函数的局部变量。将三个goroutine拆分为以下三个函数是自然的想法: func counter(out chan int)\nfunc squarer(out, in chan int)\nfunc printer(in chan int) 其中计算平方的squarer函数在两个串联Channels的中间,因此拥有两个channel类型的参数,一个用于输入一个用于输出。两个channel都拥有相同的类型,但是它们的使用方式相反:一个只用于接收,另一个只用于发送。参数的名字in和out已经明确表示了这个意图,但是并无法保证squarer函数向一个in参数对应的channel发送数据或者从一个out参数对应的channel接收数据。 这种场景是典型的。当一个channel作为一个函数参数时,它一般总是被专门用于只发送或者只接收。 为了表明这种意图并防止被滥用,Go语言的类型系统提供了单方向的channel类型,分别用于只发送或只接收的channel。类型chan<- int表示一个只发送int的channel,只能发送不能接收。相反,类型<-chan int表示一个只接收int的channel,只能接收不能发送。(箭头<-和关键字chan的相对位置表明了channel的方向。)这种限制将在编译期检测。 因为关闭操作只用于断言不再向channel发送新的数据,所以只有在发送者所在的goroutine才会调用close函数,因此对一个只接收的channel调用close将是一个编译错误。 这是改进的版本,这一次参数使用了单方向channel类型: gopl.io/ch8/pipeline3 func counter(out chan<- int) { for x := 0; x < 100; x++ { out <- x } close(out)\n} func squarer(out chan<- int, in <-chan int) { for v := range in { out <- v * v } close(out)\n} func printer(in <-chan int) { for v := range in { fmt.Println(v) }\n} func main() { naturals := make(chan int) squares := make(chan int) go counter(naturals) go squarer(squares, naturals) printer(squares)\n} 调用counter(naturals)时,naturals的类型将隐式地从chan int转换成chan<- int。调用printer(squares)也会导致相似的隐式转换,这一次是转换为<-chan int类型只接收型的channel。任何双向channel向单向channel变量的赋值操作都将导致该隐式转换。这里并没有反向转换的语法:也就是不能将一个类似chan<- int类型的单向型的channel转换为chan int类型的双向型的channel。","breadcrumbs":"Goroutines和Channels » Channels » 8.4.3. 单方向的Channel","id":"105","title":"8.4.3. 单方向的Channel"},"106":{"body":"带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的。下面的语句创建了一个可以持有三个字符串元素的带缓存Channel。图8.2是ch变量对应的channel的图形表示形式。 ch = make(chan string, 3) 向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。 我们可以在无阻塞的情况下连续向新创建的channel发送三个值: ch <- \"A\"\nch <- \"B\"\nch <- \"C\" 此刻,channel的内部缓存队列将是满的(图8.3),如果有第四个发送操作将发生阻塞。 如果我们接收一个值, fmt.Println(<-ch) // \"A\" 那么channel的缓存队列将不是满的也不是空的(图8.4),因此对该channel执行的发送或接收操作都不会发生阻塞。通过这种方式,channel的缓存队列解耦了接收和发送的goroutine。 在某些特殊情况下,程序可能需要知道channel内部缓存的容量,可以用内置的cap函数获取: fmt.Println(cap(ch)) // \"3\" 同样,对于内置的len函数,如果传入的是channel,那么将返回channel内部缓存队列中有效元素的个数。因为在并发程序中该信息会随着接收操作而失效,但是它对某些故障诊断和性能优化会有帮助。 fmt.Println(len(ch)) // \"2\" 在继续执行两次接收操作后channel内部的缓存队列将又成为空的,如果有第四个接收操作将发生阻塞: fmt.Println(<-ch) // \"B\"\nfmt.Println(<-ch) // \"C\" 在这个例子中,发送和接收操作都发生在同一个goroutine中,但是在真实的程序中它们一般由不同的goroutine执行。Go语言新手有时候会将一个带缓存的channel当作同一个goroutine中的队列使用,虽然语法看似简单,但实际上这是一个错误。Channel和goroutine的调度器机制是紧密相连的,如果没有其他goroutine从channel接收,发送者——或许是整个程序——将会面临永远阻塞的风险。如果你只是需要一个简单的队列,使用slice就可以了。 下面的例子展示了一个使用了带缓存channel的应用。它并发地向三个镜像站点发出请求,三个镜像站点分散在不同的地理位置。它们分别将收到的响应发送到带缓存channel,最后接收者只接收第一个收到的响应,也就是最快的那个响应。因此mirroredQuery函数可能在另外两个响应慢的镜像站点响应之前就返回了结果。(顺便说一下,多个goroutines并发地向同一个channel发送数据,或从同一个channel接收数据都是常见的用法。) func mirroredQuery() string { responses := make(chan string, 3) go func() { responses <- request(\"asia.gopl.io\") }() go func() { responses <- request(\"europe.gopl.io\") }() go func() { responses <- request(\"americas.gopl.io\") }() return <-responses // return the quickest response\n} func request(hostname string) (response string) { /* ... */ } 如果我们使用了无缓存的channel,那么两个慢的goroutines将会因为没有人接收而被永远卡住。这种情况,称为goroutines泄漏,这将是一个BUG。和垃圾变量不同,泄漏的goroutines并不会被自动回收,因此确保每个不再需要的goroutine能正常退出是重要的。 关于无缓存或带缓存channels之间的选择,或者是带缓存channels的容量大小的选择,都可能影响程序的正确性。无缓存channel更强地保证了每个发送操作与相应的同步接收操作;但是对于带缓存channel,这些操作是解耦的。同样,即使我们知道将要发送到一个channel的信息的数量上限,创建一个对应容量大小的带缓存channel也是不现实的,因为这要求在执行任何接收操作之前缓存所有已经发送的值。如果未能分配足够的缓存将导致程序死锁。 Channel的缓存也可能影响程序的性能。想象一家蛋糕店有三个厨师,一个烘焙,一个上糖衣,还有一个将每个蛋糕传递到它下一个厨师的生产线。在狭小的厨房空间环境,每个厨师在完成蛋糕后必须等待下一个厨师已经准备好接受它;这类似于在一个无缓存的channel上进行沟通。 如果在每个厨师之间有一个放置一个蛋糕的额外空间,那么每个厨师就可以将一个完成的蛋糕临时放在那里而马上进入下一个蛋糕的制作中;这类似于将channel的缓存队列的容量设置为1。只要每个厨师的平均工作效率相近,那么其中大部分的传输工作将是迅速的,个体之间细小的效率差异将在交接过程中弥补。如果厨师之间有更大的额外空间——也是就更大容量的缓存队列——将可以在不停止生产线的前提下消除更大的效率波动,例如一个厨师可以短暂地休息,然后再加快赶上进度而不影响其他人。 另一方面,如果生产线的前期阶段一直快于后续阶段,那么它们之间的缓存在大部分时间都将是满的。相反,如果后续阶段比前期阶段更快,那么它们之间的缓存在大部分时间都将是空的。对于这类场景,额外的缓存并没有带来任何好处。 生产线的隐喻对于理解channels和goroutines的工作机制是很有帮助的。例如,如果第二阶段是需要精心制作的复杂操作,一个厨师可能无法跟上第一个厨师的进度,或者是无法满足第三阶段厨师的需求。要解决这个问题,我们可以再雇佣另一个厨师来帮助完成第二阶段的工作,他执行相同的任务但是独立工作。这类似于基于相同的channels创建另一个独立的goroutine。 我们没有太多的空间展示全部细节,但是gopl.io/ch8/cake包模拟了这个蛋糕店,可以通过不同的参数调整。它还对上面提到的几种场景提供对应的基准测试(§11.4) 。","breadcrumbs":"Goroutines和Channels » Channels » 8.4.4. 带缓存的Channels","id":"106","title":"8.4.4. 带缓存的Channels"},"107":{"body":"本节中,我们会探索一些用来在并行时循环迭代的常见并发模型。我们会探究从全尺寸图片生成一些缩略图的问题。gopl.io/ch8/thumbnail包提供了ImageFile函数来帮我们拉伸图片。我们不会说明这个函数的实现,只需要从gopl.io下载它。 gopl.io/ch8/thumbnail package thumbnail // ImageFile reads an image from infile and writes\n// a thumbnail-size version of it in the same directory.\n// It returns the generated file name, e.g., \"foo.thumb.jpg\".\nfunc ImageFile(infile string) (string, error) 下面的程序会循环迭代一些图片文件名,并为每一张图片生成一个缩略图: gopl.io/ch8/thumbnail // makeThumbnails makes thumbnails of the specified files.\nfunc makeThumbnails(filenames []string) { for _, f := range filenames { if _, err := thumbnail.ImageFile(f); err != nil { log.Println(err) } }\n} 显然我们处理文件的顺序无关紧要,因为每一个图片的拉伸操作和其它图片的处理操作都是彼此独立的。像这种子问题都是完全彼此独立的问题被叫做易并行问题(译注:embarrassingly parallel,直译的话更像是尴尬并行)。易并行问题是最容易被实现成并行的一类问题(废话),并且最能够享受到并发带来的好处,能够随着并行的规模线性地扩展。 下面让我们并行地执行这些操作,从而将文件IO的延迟隐藏掉,并用上多核cpu的计算能力来拉伸图像。我们的第一个并发程序只是使用了一个go关键字。这里我们先忽略掉错误,之后再进行处理。 // NOTE: incorrect!\nfunc makeThumbnails2(filenames []string) { for _, f := range filenames { go thumbnail.ImageFile(f) // NOTE: ignoring errors }\n} 这个版本运行的实在有点太快,实际上,由于它比最早的版本使用的时间要短得多,即使当文件名的slice中只包含有一个元素。这就有点奇怪了,如果程序没有并发执行的话,那为什么一个并发的版本还是要快呢?答案其实是makeThumbnails在它还没有完成工作之前就已经返回了。它启动了所有的goroutine,每一个文件名对应一个,但没有等待它们一直到执行完毕。 没有什么直接的办法能够等待goroutine完成,但是我们可以改变goroutine里的代码让其能够将完成情况报告给外部的goroutine知晓,使用的方式是向一个共享的channel中发送事件。因为我们已经确切地知道有len(filenames)个内部goroutine,所以外部的goroutine只需要在返回之前对这些事件计数。 // makeThumbnails3 makes thumbnails of the specified files in parallel.\nfunc makeThumbnails3(filenames []string) { ch := make(chan struct{}) for _, f := range filenames { go func(f string) { thumbnail.ImageFile(f) // NOTE: ignoring errors ch <- struct{}{} }(f) } // Wait for goroutines to complete. for range filenames { <-ch }\n} 注意我们将f的值作为一个显式的变量传给了函数,而不是在循环的闭包中声明: for _, f := range filenames { go func() { thumbnail.ImageFile(f) // NOTE: incorrect! // ... }()\n} 回忆一下之前在5.6.1节中,匿名函数中的循环变量快照问题。上面这个单独的变量f是被所有的匿名函数值所共享,且会被连续的循环迭代所更新的。当新的goroutine开始执行字面函数时,for循环可能已经更新了f并且开始了另一轮的迭代或者(更有可能的)已经结束了整个循环,所以当这些goroutine开始读取f的值时,它们所看到的值已经是slice的最后一个元素了。显式地添加这个参数,我们能够确保使用的f是当go语句执行时的“当前”那个f。 如果我们想要从每一个worker goroutine往主goroutine中返回值时该怎么办呢?当我们调用thumbnail.ImageFile创建文件失败的时候,它会返回一个错误。下一个版本的makeThumbnails会返回其在做拉伸操作时接收到的第一个错误: // makeThumbnails4 makes thumbnails for the specified files in parallel.\n// It returns an error if any step failed.\nfunc makeThumbnails4(filenames []string) error { errors := make(chan error) for _, f := range filenames { go func(f string) { _, err := thumbnail.ImageFile(f) errors <- err }(f) } for range filenames { if err := <-errors; err != nil { return err // NOTE: incorrect: goroutine leak! } } return nil\n} 这个程序有一个微妙的bug。当它遇到第一个非nil的error时会直接将error返回到调用方,使得没有一个goroutine去排空errors channel。这样剩下的worker goroutine在向这个channel中发送值时,都会永远地阻塞下去,并且永远都不会退出。这种情况叫做goroutine泄露(§8.4.4),可能会导致整个程序卡住或者跑出out of memory的错误。 最简单的解决办法就是用一个具有合适大小的buffered channel,这样这些worker goroutine向channel中发送错误时就不会被阻塞。(一个可选的解决办法是创建一个另外的goroutine,当main goroutine返回第一个错误的同时去排空channel。) 下一个版本的makeThumbnails使用了一个buffered channel来返回生成的图片文件的名字,附带生成时的错误。 // makeThumbnails5 makes thumbnails for the specified files in parallel.\n// It returns the generated file names in an arbitrary order,\n// or an error if any step failed.\nfunc makeThumbnails5(filenames []string) (thumbfiles []string, err error) { type item struct { thumbfile string err error } ch := make(chan item, len(filenames)) for _, f := range filenames { go func(f string) { var it item it.thumbfile, it.err = thumbnail.ImageFile(f) ch <- it }(f) } for range filenames { it := <-ch if it.err != nil { return nil, it.err } thumbfiles = append(thumbfiles, it.thumbfile) } return thumbfiles, nil\n} 我们最后一个版本的makeThumbnails返回了新文件们的大小总计数(bytes)。和前面的版本都不一样的一点是我们在这个版本里没有把文件名放在slice里,而是通过一个string的channel传过来,所以我们无法对循环的次数进行预测。 为了知道最后一个goroutine什么时候结束(最后一个结束并不一定是最后一个开始),我们需要一个递增的计数器,在每一个goroutine启动时加一,在goroutine退出时减一。这需要一种特殊的计数器,这个计数器需要在多个goroutine操作时做到安全并且提供在其减为零之前一直等待的一种方法。这种计数类型被称为sync.WaitGroup,下面的代码就用到了这种方法: // makeThumbnails6 makes thumbnails for each file received from the channel.\n// It returns the number of bytes occupied by the files it creates.\nfunc makeThumbnails6(filenames <-chan string) int64 { sizes := make(chan int64) var wg sync.WaitGroup // number of working goroutines for f := range filenames { wg.Add(1) // worker go func(f string) { defer wg.Done() thumb, err := thumbnail.ImageFile(f) if err != nil { log.Println(err) return } info, _ := os.Stat(thumb) // OK to ignore error sizes <- info.Size() }(f) } // closer go func() { wg.Wait() close(sizes) }() var total int64 for size := range sizes { total += size } return total\n} 注意Add和Done方法的不对称。Add是为计数器加一,必须在worker goroutine开始之前调用,而不是在goroutine中;否则的话我们没办法确定Add是在\"closer\" goroutine调用Wait之前被调用。并且Add还有一个参数,但Done却没有任何参数;其实它和Add(-1)是等价的。我们使用defer来确保计数器即使是在出错的情况下依然能够正确地被减掉。上面的程序代码结构是当我们使用并发循环,但又不知道迭代次数时很通常而且很地道的写法。 sizes channel携带了每一个文件的大小到main goroutine,在main goroutine中使用了range loop来计算总和。观察一下我们是怎样创建一个closer goroutine,并让其在所有worker goroutine们结束之后再关闭sizes channel的。两步操作:wait和close,必须是基于sizes的循环的并发。考虑一下另一种方案:如果等待操作被放在了main goroutine中,在循环之前,这样的话就永远都不会结束了,如果在循环之后,那么又变成了不可达的部分,因为没有任何东西去关闭这个channel,这个循环就永远都不会终止。 图8.5 表明了makethumbnails6函数中事件的序列。纵列表示goroutine。窄线段代表sleep,粗线段代表活动。斜线箭头代表用来同步两个goroutine的事件。时间向下流动。注意main goroutine是如何大部分的时间被唤醒执行其range循环,等待worker发送值或者closer来关闭channel的。 练习 8.4: 修改reverb2服务器,在每一个连接中使用sync.WaitGroup来计数活跃的echo goroutine。当计数减为零时,关闭TCP连接的写入,像练习8.3中一样。验证一下你的修改版netcat3客户端会一直等待所有的并发“喊叫”完成,即使是在标准输入流已经关闭的情况下。 练习 8.5: 使用一个已有的CPU绑定的顺序程序,比如在3.3节中我们写的Mandelbrot程序或者3.2节中的3-D surface计算程序,并将他们的主循环改为并发形式,使用channel来进行通信。在多核计算机上这个程序得到了多少速度上的改进?使用多少个goroutine是最合适的呢?","breadcrumbs":"Goroutines和Channels » 并发的循环 » 8.5. 并发的循环","id":"107","title":"8.5. 并发的循环"},"108":{"body":"在5.6节中,我们做了一个简单的web爬虫,用bfs(广度优先)算法来抓取整个网站。在本节中,我们会让这个爬虫并行化,这样每一个彼此独立的抓取命令可以并行进行IO,最大化利用网络资源。crawl函数和gopl.io/ch5/findlinks3中的是一样的。 gopl.io/ch8/crawl1 func crawl(url string) []string { fmt.Println(url) list, err := links.Extract(url) if err != nil { log.Print(err) } return list\n} 主函数和5.6节中的breadthFirst(广度优先)类似。像之前一样,一个worklist是一个记录了需要处理的元素的队列,每一个元素都是一个需要抓取的URL列表,不过这一次我们用channel代替slice来做这个队列。每一个对crawl的调用都会在他们自己的goroutine中进行并且会把他们抓到的链接发送回worklist。 func main() { worklist := make(chan []string) // Start with the command-line arguments. go func() { worklist <- os.Args[1:] }() // Crawl the web concurrently. seen := make(map[string]bool) for list := range worklist { for _, link := range list { if !seen[link] { seen[link] = true go func(link string) { worklist <- crawl(link) }(link) } } }\n} 注意这里的crawl所在的goroutine会将link作为一个显式的参数传入,来避免“循环变量快照”的问题(在5.6.1中有讲解)。另外注意这里将命令行参数传入worklist也是在一个另外的goroutine中进行的,这是为了避免channel两端的main goroutine与crawler goroutine都尝试向对方发送内容,却没有一端接收内容时发生死锁。当然,这里我们也可以用buffered channel来解决问题,这里不再赘述。 现在爬虫可以高并发地运行起来,并且可以产生一大坨的URL了,不过还是会有俩问题。一个问题是在运行一段时间后可能会出现在log的错误信息里的: $ go build gopl.io/ch8/crawl1\n$ ./crawl1 http://gopl.io/\nhttp://gopl.io/\nhttps://golang.org/help/\nhttps://golang.org/doc/\nhttps://golang.org/blog/\n...\n2015/07/15 18:22:12 Get ...: dial tcp: lookup blog.golang.org: no such host\n2015/07/15 18:22:12 Get ...: dial tcp 23.21.222.120:443: socket: too many open files\n... 最初的错误信息是一个让人莫名的DNS查找失败,即使这个域名是完全可靠的。而随后的错误信息揭示了原因:这个程序一次性创建了太多网络连接,超过了每一个进程的打开文件数限制,既而导致了在调用net.Dial像DNS查找失败这样的问题。 这个程序实在是太他妈并行了。无穷无尽地并行化并不是什么好事情,因为不管怎么说,你的系统总是会有一些个限制因素,比如CPU核心数会限制你的计算负载,比如你的硬盘转轴和磁头数限制了你的本地磁盘IO操作频率,比如你的网络带宽限制了你的下载速度上限,或者是你的一个web服务的服务容量上限等等。为了解决这个问题,我们可以限制并发程序所使用的资源来使之适应自己的运行环境。对于我们的例子来说,最简单的方法就是限制对links.Extract在同一时间最多不会有超过n次调用,这里的n一般小于文件描述符的上限值,比如20。这和一个夜店里限制客人数目是一个道理,只有当有客人离开时,才会允许新的客人进入店内。 我们可以用一个有容量限制的buffered channel来控制并发,这类似于操作系统里的计数信号量概念。从概念上讲,channel里的n个空槽代表n个可以处理内容的token(通行证),从channel里接收一个值会释放其中的一个token,并且生成一个新的空槽位。这样保证了在没有接收介入时最多有n个发送操作。(这里可能我们拿channel里填充的槽来做token更直观一些,不过还是这样吧。)由于channel里的元素类型并不重要,我们用一个零值的struct{}来作为其元素。 让我们重写crawl函数,将对links.Extract的调用操作用获取、释放token的操作包裹起来,来确保同一时间对其只有20个调用。信号量数量和其能操作的IO资源数量应保持接近。 gopl.io/ch8/crawl2 // tokens is a counting semaphore used to\n// enforce a limit of 20 concurrent requests.\nvar tokens = make(chan struct{}, 20) func crawl(url string) []string { fmt.Println(url) tokens <- struct{}{} // acquire a token list, err := links.Extract(url) <-tokens // release the token if err != nil { log.Print(err) } return list\n} 第二个问题是这个程序永远都不会终止,即使它已经爬到了所有初始链接衍生出的链接。(当然,除非你慎重地选择了合适的初始化URL或者已经实现了练习8.6中的深度限制,你应该还没有意识到这个问题。)为了使这个程序能够终止,我们需要在worklist为空或者没有crawl的goroutine在运行时退出主循环。 func main() { worklist := make(chan []string) var n int // number of pending sends to worklist // Start with the command-line arguments. n++ go func() { worklist <- os.Args[1:] }() // Crawl the web concurrently. seen := make(map[string]bool) for ; n > 0; n-- { list := <-worklist for _, link := range list { if !seen[link] { seen[link] = true n++ go func(link string) { worklist <- crawl(link) }(link) } } }\n} 这个版本中,计数器n对worklist的发送操作数量进行了限制。每一次我们发现有元素需要被发送到worklist时,我们都会对n进行++操作,在向worklist中发送初始的命令行参数之前,我们也进行过一次++操作。这里的操作++是在每启动一个crawler的goroutine之前。主循环会在n减为0时终止,这时候说明没活可干了。 现在这个并发爬虫会比5.6节中的深度优先搜索版快上20倍,而且不会出什么错,并且在其完成任务时也会正确地终止。 下面的程序是避免过度并发的另一种思路。这个版本使用了原来的crawl函数,但没有使用计数信号量,取而代之用了20个常驻的crawler goroutine,这样来保证最多20个HTTP请求在并发。 func main() { worklist := make(chan []string) // lists of URLs, may have duplicates unseenLinks := make(chan string) // de-duplicated URLs // Add command-line arguments to worklist. go func() { worklist <- os.Args[1:] }() // Create 20 crawler goroutines to fetch each unseen link. for i := 0; i < 20; i++ { go func() { for link := range unseenLinks { foundLinks := crawl(link) go func() { worklist <- foundLinks }() } }() } // The main goroutine de-duplicates worklist items // and sends the unseen ones to the crawlers. seen := make(map[string]bool) for list := range worklist { for _, link := range list { if !seen[link] { seen[link] = true unseenLinks <- link } } }\n} 所有的爬虫goroutine现在都是被同一个channel - unseenLinks喂饱的了。主goroutine负责拆分它从worklist里拿到的元素,然后把没有抓过的经由unseenLinks channel发送给一个爬虫的goroutine。 seen这个map被限定在main goroutine中;也就是说这个map只能在main goroutine中进行访问。类似于其它的信息隐藏方式,这样的约束可以让我们从一定程度上保证程序的正确性。例如,内部变量不能够在函数外部被访问到;变量(§2.3.4)在没有发生变量逃逸(译注:局部变量被全局变量引用地址导致变量被分配在堆上)的情况下是无法在函数外部访问的;一个对象的封装字段无法被该对象的方法以外的方法访问到。在所有的情况下,信息隐藏都可以帮助我们约束我们的程序,使其不发生意料之外的情况。 crawl函数爬到的链接在一个专有的goroutine中被发送到worklist中来避免死锁。为了节省篇幅,这个例子的终止问题我们先不进行详细阐述了。 练习 8.6: 为并发爬虫增加深度限制。也就是说,如果用户设置了depth=3,那么只有从首页跳转三次以内能够跳到的页面才能被抓取到。 练习 8.7: 完成一个并发程序来创建一个线上网站的本地镜像,把该站点的所有可达的页面都抓取到本地硬盘。为了省事,我们这里可以只取出现在该域下的所有页面(比如golang.org开头,译注:外链的应该就不算了。)当然了,出现在页面里的链接你也需要进行一些处理,使其能够在你的镜像站点上进行跳转,而不是指向原始的链接。 译注: 拓展阅读 Handling 1 Million Requests per Minute with Go 。","breadcrumbs":"Goroutines和Channels » 示例: 并发的Web爬虫 » 8.6. 示例: 并发的Web爬虫","id":"108","title":"8.6. 示例: 并发的Web爬虫"},"109":{"body":"下面的程序会进行火箭发射的倒计时。time.Tick函数返回一个channel,程序会周期性地像一个节拍器一样向这个channel发送事件。每一个事件的值是一个时间戳,不过更有意思的是其传送方式。 gopl.io/ch8/countdown1 func main() { fmt.Println(\"Commencing countdown.\") tick := time.Tick(1 * time.Second) for countdown := 10; countdown > 0; countdown-- { fmt.Println(countdown) <-tick } launch()\n} 现在我们让这个程序支持在倒计时中,用户按下return键时直接中断发射流程。首先,我们启动一个goroutine,这个goroutine会尝试从标准输入中读入一个单独的byte并且,如果成功了,会向名为abort的channel发送一个值。 gopl.io/ch8/countdown2 abort := make(chan struct{})\ngo func() { os.Stdin.Read(make([]byte, 1)) // read a single byte abort <- struct{}{}\n}() 现在每一次计数循环的迭代都需要等待两个channel中的其中一个返回事件了:当一切正常时的ticker channel(就像NASA jorgon的\"nominal\",译注:这梗估计我们是不懂了)或者异常时返回的abort事件。我们无法做到从每一个channel中接收信息,如果我们这么做的话,如果第一个channel中没有事件发过来那么程序就会立刻被阻塞,这样我们就无法收到第二个channel中发过来的事件。这时候我们需要多路复用(multiplex)这些操作了,为了能够多路复用,我们使用了select语句。 select {\ncase <-ch1: // ...\ncase x := <-ch2: // ...use x...\ncase ch3 <- y: // ...\ndefault: // ...\n} 上面是select语句的一般形式。和switch语句稍微有点相似,也会有几个case和最后的default选择分支。每一个case代表一个通信操作(在某个channel上进行发送或者接收),并且会包含一些语句组成的一个语句块。一个接收表达式可能只包含接收表达式自身(译注:不把接收到的值赋值给变量什么的),就像上面的第一个case,或者包含在一个简短的变量声明中,像第二个case里一样;第二种形式让你能够引用接收到的值。 select会等待case中有能够执行的case时去执行。当条件满足时,select才会去通信并执行case之后的语句;这时候其它通信是不会执行的。一个没有任何case的select语句写作select{},会永远地等待下去。 让我们回到我们的火箭发射程序。time.After函数会立即返回一个channel,并起一个新的goroutine在经过特定的时间后向该channel发送一个独立的值。下面的select语句会一直等待直到两个事件中的一个到达,无论是abort事件或者一个10秒经过的事件。如果10秒经过了还没有abort事件进入,那么火箭就会发射。 func main() { // ...create abort channel... fmt.Println(\"Commencing countdown. Press return to abort.\") select { case <-time.After(10 * time.Second): // Do nothing. case <-abort: fmt.Println(\"Launch aborted!\") return } launch()\n} 下面这个例子更微妙。ch这个channel的buffer大小是1,所以会交替的为空或为满,所以只有一个case可以进行下去,无论i是奇数或者偶数,它都会打印0 2 4 6 8。 ch := make(chan int, 1)\nfor i := 0; i < 10; i++ { select { case x := <-ch: fmt.Println(x) // \"0\" \"2\" \"4\" \"6\" \"8\" case ch <- i: }\n} 如果多个case同时就绪时,select会随机地选择一个执行,这样来保证每一个channel都有平等的被select的机会。增加前一个例子的buffer大小会使其输出变得不确定,因为当buffer既不为满也不为空时,select语句的执行情况就像是抛硬币的行为一样是随机的。 下面让我们的发射程序打印倒计时。这里的select语句会使每次循环迭代等待一秒来执行退出操作。 gopl.io/ch8/countdown3 func main() { // ...create abort channel... fmt.Println(\"Commencing countdown. Press return to abort.\") tick := time.Tick(1 * time.Second) for countdown := 10; countdown > 0; countdown-- { fmt.Println(countdown) select { case <-tick: // Do nothing. case <-abort: fmt.Println(\"Launch aborted!\") return } } launch()\n} time.Tick函数表现得好像它创建了一个在循环中调用time.Sleep的goroutine,每次被唤醒时发送一个事件。当countdown函数返回时,它会停止从tick中接收事件,但是ticker这个goroutine还依然存活,继续徒劳地尝试向channel中发送值,然而这时候已经没有其它的goroutine会从该channel中接收值了——这被称为goroutine泄露(§8.4.4)。 Tick函数挺方便,但是只有当程序整个生命周期都需要这个时间时我们使用它才比较合适。否则的话,我们应该使用下面的这种模式: ticker := time.NewTicker(1 * time.Second)\n<-ticker.C // receive from the ticker's channel\nticker.Stop() // cause the ticker's goroutine to terminate 有时候我们希望能够从channel中发送或者接收值,并避免因为发送或者接收导致的阻塞,尤其是当channel没有准备好写或者读时。select语句就可以实现这样的功能。select会有一个default来设置当其它的操作都不能够马上被处理时程序需要执行哪些逻辑。 下面的select语句会在abort channel中有值时,从其中接收值;无值时什么都不做。这是一个非阻塞的接收操作;反复地做这样的操作叫做“轮询channel”。 select {\ncase <-abort: fmt.Printf(\"Launch aborted!\\n\") return\ndefault: // do nothing\n} channel的零值是nil。也许会让你觉得比较奇怪,nil的channel有时候也是有一些用处的。因为对一个nil的channel发送和接收操作会永远阻塞,在select语句中操作nil的channel永远都不会被select到。 这使得我们可以用nil来激活或者禁用case,来达成处理其它输入或输出事件时超时和取消的逻辑。我们会在下一节中看到一个例子。 练习 8.8: 使用select来改造8.3节中的echo服务器,为其增加超时,这样服务器可以在客户端10秒中没有任何喊话时自动断开连接。","breadcrumbs":"Goroutines和Channels » 基于select的多路复用 » 8.7. 基于select的多路复用","id":"109","title":"8.7. 基于select的多路复用"},"11":{"body":"对文件做拷贝、打印、搜索、排序、统计或类似事情的程序都有一个差不多的程序结构:一个处理输入的循环,在每个元素上执行计算处理,在处理的同时或最后产生输出。我们会展示一个名为 dup 的程序的三个版本;灵感来自于 Unix 的 uniq 命令,其寻找相邻的重复行。该程序使用的结构和包是个参考范例,可以方便地修改。 dup 的第一个版本打印标准输入中多次出现的行,以重复次数开头。该程序将引入 if 语句,map 数据类型以及 bufio 包。 gopl.io/ch1/dup1 // Dup1 prints the text of each line that appears more than\n// once in the standard input, preceded by its count.\npackage main import ( \"bufio\" \"fmt\" \"os\"\n) func main() { counts := make(map[string]int) input := bufio.NewScanner(os.Stdin) for input.Scan() { counts[input.Text()]++ } // NOTE: ignoring potential errors from input.Err() for line, n := range counts { if n > 1 { fmt.Printf(\"%d\\t%s\\n\", n, line) } }\n} 正如 for 循环一样,if 语句条件两边也不加括号,但是主体部分需要加。if 语句的 else 部分是可选的,在 if 的条件为 false 时执行。 map 存储了键/值(key/value)的集合,对集合元素,提供常数时间的存、取或测试操作。键可以是任意类型,只要其值能用 == 运算符比较,最常见的例子是字符串;值则可以是任意类型。这个例子中的键是字符串,值是整数。内置函数 make 创建空 map,此外,它还有别的作用。4.3 节讨论 map。 (译注:从功能和实现上说,Go 的 map 类似于 Java 语言中的 HashMap,Python 语言中的 dict,Lua 语言中的 table,通常使用 hash 实现。遗憾的是,对于该词的翻译并不统一,数学界术语为 映射 ,而计算机界众说纷纭莫衷一是。为了防止对读者造成误解,保留不译。) 每次 dup 读取一行输入,该行被当做键存入 map,其对应的值递增。counts[input.Text()]++ 语句等价下面两句: line := input.Text()\ncounts[line] = counts[line] + 1 map 中不含某个键时不用担心,首次读到新行时,等号右边的表达式 counts[line] 的值将被计算为其类型的零值,对于 int 即 0。 为了打印结果,我们使用了基于 range 的循环,并在 counts 这个 map 上迭代。跟之前类似,每次迭代得到两个结果,键和其在 map 中对应的值。map 的迭代顺序并不确定,从实践来看,该顺序随机,每次运行都会变化。这种设计是有意为之的,因为能防止程序依赖特定遍历顺序,而这是无法保证的。(译注:具体可以参见这里 https://stackoverflow.com/questions/11853396/google-go-lang-assignment-order ) 继续来看 bufio 包,它使处理输入和输出方便又高效。Scanner 类型是该包最有用的特性之一,它读取输入并将其拆成行或单词;通常是处理行形式的输入最简单的方法。 程序使用短变量声明创建 bufio.Scanner 类型的变量 input。 input := bufio.NewScanner(os.Stdin) 该变量从程序的标准输入中读取内容。每次调用 input.Scan(),即读入下一行,并移除行末的换行符;读取的内容可以调用 input.Text() 得到。Scan 函数在读到一行时返回 true,不再有输入时返回 false。 类似于 C 或其它语言里的 printf 函数,fmt.Printf 函数对一些表达式产生格式化输出。该函数的首个参数是个格式字符串,指定后续参数被如何格式化。各个参数的格式取决于“转换字符”(conversion character),形式为百分号后跟一个字母。举个例子,%d 表示以十进制形式打印一个整型操作数,而 %s 则表示把字符串型操作数的值展开。 Printf 有一大堆这种转换,Go程序员称之为 动词(verb) 。下面的表格虽然远不是完整的规范,但展示了可用的很多特性: %d 十进制整数\n%x, %o, %b 十六进制,八进制,二进制整数。\n%f, %g, %e 浮点数: 3.141593 3.141592653589793 3.141593e+00\n%t 布尔:true或false\n%c 字符(rune) (Unicode码点)\n%s 字符串\n%q 带双引号的字符串\"abc\"或带单引号的字符'c'\n%v 变量的自然形式(natural format)\n%T 变量的类型\n%% 字面上的百分号标志(无操作数) dup1 的格式字符串中还含有制表符\\t和换行符\\n。字符串字面上可能含有这些代表不可见字符的 转义字符(escape sequences) 。默认情况下,Printf 不会换行。按照惯例,以字母 f 结尾的格式化函数,如 log.Printf 和 fmt.Errorf,都采用 fmt.Printf 的格式化准则。而以 ln 结尾的格式化函数,则遵循 Println 的方式,以跟 %v 差不多的方式格式化参数,并在最后添加一个换行符。(译注:后缀 f 指 format,ln 指 line。) 很多程序要么从标准输入中读取数据,如上面的例子所示,要么从一系列具名文件中读取数据。dup 程序的下个版本读取标准输入或是使用 os.Open 打开各个具名文件,并操作它们。 gopl.io/ch1/dup2 // Dup2 prints the count and text of lines that appear more than once\n// in the input. It reads from stdin or from a list of named files.\npackage main import ( \"bufio\" \"fmt\" \"os\"\n) func main() { counts := make(map[string]int) files := os.Args[1:] if len(files) == 0 { countLines(os.Stdin, counts) } else { for _, arg := range files { f, err := os.Open(arg) if err != nil { fmt.Fprintf(os.Stderr, \"dup2: %v\\n\", err) continue } countLines(f, counts) f.Close() } } for line, n := range counts { if n > 1 { fmt.Printf(\"%d\\t%s\\n\", n, line) } }\n} func countLines(f *os.File, counts map[string]int) { input := bufio.NewScanner(f) for input.Scan() { counts[input.Text()]++ } // NOTE: ignoring potential errors from input.Err()\n} os.Open 函数返回两个值。第一个值是被打开的文件(*os.File),其后被 Scanner 读取。 os.Open 返回的第二个值是内置 error 类型的值。如果 err 等于内置值nil(译注:相当于其它语言里的 NULL),那么文件被成功打开。读取文件,直到文件结束,然后调用 Close 关闭该文件,并释放占用的所有资源。相反的话,如果 err 的值不是 nil,说明打开文件时出错了。这种情况下,错误值描述了所遇到的问题。我们的错误处理非常简单,只是使用 Fprintf 与表示任意类型默认格式值的动词 %v,向标准错误流打印一条信息,然后 dup 继续处理下一个文件;continue 语句直接跳到 for 循环的下个迭代开始执行。 为了使示例代码保持合理的大小,本书开始的一些示例有意简化了错误处理,显而易见的是,应该检查 os.Open 返回的错误值,然而,使用 input.Scan 读取文件过程中,不大可能出现错误,因此我们忽略了错误处理。我们会在跳过错误检查的地方做说明。5.4 节中深入介绍错误处理。 注意 countLines 函数在其声明前被调用。函数和包级别的变量(package-level entities)可以任意顺序声明,并不影响其被调用。(译注:最好还是遵循一定的规范) map 是一个由 make 函数创建的数据结构的引用。map 作为参数传递给某函数时,该函数接收这个引用的一份拷贝(copy,或译为副本),被调用函数对 map 底层数据结构的任何修改,调用者函数都可以通过持有的 map 引用看到。在我们的例子中,countLines 函数向 counts 插入的值,也会被 main 函数看到。(译注:类似于 C++ 里的引用传递,实际上指针是另一个指针了,但内部存的值指向同一块内存) dup 的前两个版本以\"流”模式读取输入,并根据需要拆分成多个行。理论上,这些程序可以处理任意数量的输入数据。还有另一个方法,就是一口气把全部输入数据读到内存中,一次分割为多行,然后处理它们。下面这个版本,dup3,就是这么操作的。这个例子引入了 ReadFile 函数(来自于io/ioutil包),其读取指定文件的全部内容,strings.Split 函数把字符串分割成子串的切片。(Split 的作用与前文提到的 strings.Join 相反。) 我们略微简化了 dup3。首先,由于 ReadFile 函数需要文件名作为参数,因此只读指定文件,不读标准输入。其次,由于行计数代码只在一处用到,故将其移回 main 函数。 gopl.io/ch1/dup3 package main import ( \"fmt\" \"io/ioutil\" \"os\" \"strings\"\n) func main() { counts := make(map[string]int) for _, filename := range os.Args[1:] { data, err := ioutil.ReadFile(filename) if err != nil { fmt.Fprintf(os.Stderr, \"dup3: %v\\n\", err) continue } for _, line := range strings.Split(string(data), \"\\n\") { counts[line]++ } } for line, n := range counts { if n > 1 { fmt.Printf(\"%d\\t%s\\n\", n, line) } }\n} ReadFile 函数返回一个字节切片(byte slice),必须把它转换为 string,才能用 strings.Split 分割。我们会在3.5.4 节详细讲解字符串和字节切片。 实现上,bufio.Scanner、ioutil.ReadFile 和 ioutil.WriteFile 都使用 *os.File 的 Read 和 Write 方法,但是,大多数程序员很少需要直接调用那些低级(lower-level)函数。高级(higher-level)函数,像 bufio 和 io/ioutil 包中所提供的那些,用起来要容易点。 练习 1.4: 修改 dup2,出现重复的行时打印文件名称。","breadcrumbs":"入门 » 查找重复的行 » 1.3. 查找重复的行","id":"11","title":"1.3. 查找重复的行"},"110":{"body":"在本小节中,我们会创建一个程序来生成指定目录的硬盘使用情况报告,这个程序和Unix里的du工具比较相似。大多数工作用下面这个walkDir函数来完成,这个函数使用dirents函数来枚举一个目录下的所有入口。 gopl.io/ch8/du1 // walkDir recursively walks the file tree rooted at dir\n// and sends the size of each found file on fileSizes.\nfunc walkDir(dir string, fileSizes chan<- int64) { for _, entry := range dirents(dir) { if entry.IsDir() { subdir := filepath.Join(dir, entry.Name()) walkDir(subdir, fileSizes) } else { fileSizes <- entry.Size() } }\n} // dirents returns the entries of directory dir.\nfunc dirents(dir string) []os.FileInfo { entries, err := ioutil.ReadDir(dir) if err != nil { fmt.Fprintf(os.Stderr, \"du1: %v\\n\", err) return nil } return entries\n} ioutil.ReadDir函数会返回一个os.FileInfo类型的slice,os.FileInfo类型也是os.Stat这个函数的返回值。对每一个子目录而言,walkDir会递归地调用其自身,同时也在递归里获取每一个文件的信息。walkDir函数会向fileSizes这个channel发送一条消息。这条消息包含了文件的字节大小。 下面的主函数,用了两个goroutine。后台的goroutine调用walkDir来遍历命令行给出的每一个路径并最终关闭fileSizes这个channel。主goroutine会对其从channel中接收到的文件大小进行累加,并输出其和。 package main import ( \"flag\" \"fmt\" \"io/ioutil\" \"os\" \"path/filepath\"\n) func main() { // Determine the initial directories. flag.Parse() roots := flag.Args() if len(roots) == 0 { roots = []string{\".\"} } // Traverse the file tree. fileSizes := make(chan int64) go func() { for _, root := range roots { walkDir(root, fileSizes) } close(fileSizes) }() // Print the results. var nfiles, nbytes int64 for size := range fileSizes { nfiles++ nbytes += size } printDiskUsage(nfiles, nbytes)\n} func printDiskUsage(nfiles, nbytes int64) { fmt.Printf(\"%d files %.1f GB\\n\", nfiles, float64(nbytes)/1e9)\n} 这个程序会在打印其结果之前卡住很长时间。 $ go build gopl.io/ch8/du1\n$ ./du1 $HOME /usr /bin /etc\n213201 files 62.7 GB 如果在运行的时候能够让我们知道处理进度的话想必更好。但是,如果简单地把printDiskUsage函数调用移动到循环里会导致其打印出成百上千的输出。 下面这个du的变种会间歇打印内容,不过只有在调用时提供了-v的flag才会显示程序进度信息。在roots目录上循环的后台goroutine在这里保持不变。主goroutine现在使用了计时器来每500ms生成事件,然后用select语句来等待文件大小的消息来更新总大小数据,或者一个计时器的事件来打印当前的总大小数据。如果-v的flag在运行时没有传入的话,tick这个channel会保持为nil,这样在select里的case也就相当于被禁用了。 gopl.io/ch8/du2 var verbose = flag.Bool(\"v\", false, \"show verbose progress messages\") func main() { // ...start background goroutine... // Print the results periodically. var tick <-chan time.Time if *verbose { tick = time.Tick(500 * time.Millisecond) } var nfiles, nbytes int64\nloop: for { select { case size, ok := <-fileSizes: if !ok { break loop // fileSizes was closed } nfiles++ nbytes += size case <-tick: printDiskUsage(nfiles, nbytes) } } printDiskUsage(nfiles, nbytes) // final totals\n} 由于我们的程序不再使用range循环,第一个select的case必须显式地判断fileSizes的channel是不是已经被关闭了,这里可以用到channel接收的二值形式。如果channel已经被关闭了的话,程序会直接退出循环。这里的break语句用到了标签break,这样可以同时终结select和for两个循环;如果没有用标签就break的话只会退出内层的select循环,而外层的for循环会使之进入下一轮select循环。 现在程序会悠闲地为我们打印更新流: $ go build gopl.io/ch8/du2\n$ ./du2 -v $HOME /usr /bin /etc\n28608 files 8.3 GB\n54147 files 10.3 GB\n93591 files 15.1 GB\n127169 files 52.9 GB\n175931 files 62.2 GB\n213201 files 62.7 GB 然而这个程序还是会花上很长时间才会结束。完全可以并发调用walkDir,从而发挥磁盘系统的并行性能。下面这个第三个版本的du,会对每一个walkDir的调用创建一个新的goroutine。它使用sync.WaitGroup(§8.5)来对仍旧活跃的walkDir调用进行计数,另一个goroutine会在计数器减为零的时候将fileSizes这个channel关闭。 gopl.io/ch8/du3 func main() { // ...determine roots... // Traverse each root of the file tree in parallel. fileSizes := make(chan int64) var n sync.WaitGroup for _, root := range roots { n.Add(1) go walkDir(root, &n, fileSizes) } go func() { n.Wait() close(fileSizes) }() // ...select loop...\n} func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) { defer n.Done() for _, entry := range dirents(dir) { if entry.IsDir() { n.Add(1) subdir := filepath.Join(dir, entry.Name()) go walkDir(subdir, n, fileSizes) } else { fileSizes <- entry.Size() } }\n} 由于这个程序在高峰期会创建成百上千的goroutine,我们需要修改dirents函数,用计数信号量来阻止他同时打开太多的文件,就像我们在8.7节中的并发爬虫一样: // sema is a counting semaphore for limiting concurrency in dirents.\nvar sema = make(chan struct{}, 20) // dirents returns the entries of directory dir.\nfunc dirents(dir string) []os.FileInfo { sema <- struct{}{} // acquire token defer func() { <-sema }() // release token // ... 这个版本比之前那个快了好几倍,尽管其具体效率还是和你的运行环境,机器配置相关。 练习 8.9: 编写一个du工具,每隔一段时间将root目录下的目录大小计算并显示出来。","breadcrumbs":"Goroutines和Channels » 示例: 并发的目录遍历 » 8.8. 示例: 并发的目录遍历","id":"110","title":"8.8. 示例: 并发的目录遍历"},"111":{"body":"有时候我们需要通知goroutine停止它正在干的事情,比如一个正在执行计算的web服务,然而它的客户端已经断开了和服务端的连接。 Go语言并没有提供在一个goroutine中终止另一个goroutine的方法,由于这样会导致goroutine之间的共享变量落在未定义的状态上。在8.7节中的rocket launch程序中,我们往名字叫abort的channel里发送了一个简单的值,在countdown的goroutine中会把这个值理解为自己的退出信号。但是如果我们想要退出两个或者任意多个goroutine怎么办呢? 一种可能的手段是向abort的channel里发送和goroutine数目一样多的事件来退出它们。如果这些goroutine中已经有一些自己退出了,那么会导致我们的channel里的事件数比goroutine还多,这样导致我们的发送直接被阻塞。另一方面,如果这些goroutine又生成了其它的goroutine,我们的channel里的数目又太少了,所以有些goroutine可能会无法接收到退出消息。一般情况下我们是很难知道在某一个时刻具体有多少个goroutine在运行着的。另外,当一个goroutine从abort channel中接收到一个值的时候,他会消费掉这个值,这样其它的goroutine就没法看到这条信息。为了能够达到我们退出goroutine的目的,我们需要更靠谱的策略,来通过一个channel把消息广播出去,这样goroutine们能够看到这条事件消息,并且在事件完成之后,可以知道这件事已经发生过了。 回忆一下我们关闭了一个channel并且被消费掉了所有已发送的值,操作channel之后的代码可以立即被执行,并且会产生零值。我们可以将这个机制扩展一下,来作为我们的广播机制:不要向channel发送值,而是用关闭一个channel来进行广播。 只要一些小修改,我们就可以把退出逻辑加入到前一节的du程序。首先,我们创建一个退出的channel,不需要向这个channel发送任何值,但其所在的闭包内要写明程序需要退出。我们同时还定义了一个工具函数,cancelled,这个函数在被调用的时候会轮询退出状态。 gopl.io/ch8/du4 var done = make(chan struct{}) func cancelled() bool { select { case <-done: return true default: return false }\n} 下面我们创建一个从标准输入流中读取内容的goroutine,这是一个比较典型的连接到终端的程序。每当有输入被读到(比如用户按了回车键),这个goroutine就会把取消消息通过关闭done的channel广播出去。 // Cancel traversal when input is detected.\ngo func() { os.Stdin.Read(make([]byte, 1)) // read a single byte close(done)\n}() 现在我们需要使我们的goroutine来对取消进行响应。在main goroutine中,我们添加了select的第三个case语句,尝试从done channel中接收内容。如果这个case被满足的话,在select到的时候即会返回,但在结束之前我们需要把fileSizes channel中的内容“排”空,在channel被关闭之前,舍弃掉所有值。这样可以保证对walkDir的调用不要被向fileSizes发送信息阻塞住,可以正确地完成。 for { select { case <-done: // Drain fileSizes to allow existing goroutines to finish. for range fileSizes { // Do nothing. } return case size, ok := <-fileSizes: // ... }\n} walkDir这个goroutine一启动就会轮询取消状态,如果取消状态被设置的话会直接返回,并且不做额外的事情。这样我们将所有在取消事件之后创建的goroutine改变为无操作。 func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) { defer n.Done() if cancelled() { return } for _, entry := range dirents(dir) { // ... }\n} 在walkDir函数的循环中我们对取消状态进行轮询可以带来明显的益处,可以避免在取消事件发生时还去创建goroutine。取消本身是有一些代价的;想要快速的响应需要对程序逻辑进行侵入式的修改。确保在取消发生之后不要有代价太大的操作可能会需要修改你代码里的很多地方,但是在一些重要的地方去检查取消事件也确实能带来很大的好处。 对这个程序的一个简单的性能分析可以揭示瓶颈在dirents函数中获取一个信号量。下面的select可以让这种操作可以被取消,并且可以将取消时的延迟从几百毫秒降低到几十毫秒。 func dirents(dir string) []os.FileInfo { select { case sema <- struct{}{}: // acquire token case <-done: return nil // cancelled } defer func() { <-sema }() // release token // ...read directory...\n} 现在当取消发生时,所有后台的goroutine都会迅速停止并且主函数会返回。当然,当主函数返回时,一个程序会退出,而我们又无法在主函数退出的时候确认其已经释放了所有的资源(译注:因为程序都退出了,你的代码都没法执行了)。这里有一个方便的窍门我们可以一用:取代掉直接从主函数返回,我们调用一个panic,然后runtime会把每一个goroutine的栈dump下来。如果main goroutine是唯一一个剩下的goroutine的话,他会清理掉自己的一切资源。但是如果还有其它的goroutine没有退出,他们可能没办法被正确地取消掉,也有可能被取消但是取消操作会很花时间;所以这里的一个调研还是很有必要的。我们用panic来获取到足够的信息来验证我们上面的判断,看看最终到底是什么样的情况。 练习 8.10: HTTP请求可能会因http.Request结构体中Cancel channel的关闭而取消。修改8.6节中的web crawler来支持取消http请求。(提示:http.Get并没有提供方便地定制一个请求的方法。你可以用http.NewRequest来取而代之,设置它的Cancel字段,然后用http.DefaultClient.Do(req)来进行这个http请求。) 练习 8.11: 紧接着8.4.4中的mirroredQuery流程,实现一个并发请求url的fetch的变种。当第一个请求返回时,直接取消其它的请求。","breadcrumbs":"Goroutines和Channels » 并发的退出 » 8.9. 并发的退出","id":"111","title":"8.9. 并发的退出"},"112":{"body":"我们用一个聊天服务器来终结本章节的内容,这个程序可以让一些用户通过服务器向其它所有用户广播文本消息。这个程序中有四种goroutine。main和broadcaster各自是一个goroutine实例,每一个客户端的连接都会有一个handleConn和clientWriter的goroutine。broadcaster是select用法的不错的样例,因为它需要处理三种不同类型的消息。 下面演示的main goroutine的工作,是listen和accept(译注:网络编程里的概念)从客户端过来的连接。对每一个连接,程序都会建立一个新的handleConn的goroutine,就像我们在本章开头的并发的echo服务器里所做的那样。 gopl.io/ch8/chat func main() { listener, err := net.Listen(\"tcp\", \"localhost:8000\") if err != nil { log.Fatal(err) } go broadcaster() for { conn, err := listener.Accept() if err != nil { log.Print(err) continue } go handleConn(conn) }\n} 然后是broadcaster的goroutine。他的内部变量clients会记录当前建立连接的客户端集合。其记录的内容是每一个客户端的消息发出channel的“资格”信息。 type client chan<- string // an outgoing message channel var ( entering = make(chan client) leaving = make(chan client) messages = make(chan string) // all incoming client messages\n) func broadcaster() { clients := make(map[client]bool) // all connected clients for { select { case msg := <-messages: // Broadcast incoming message to all // clients' outgoing message channels. for cli := range clients { cli <- msg } case cli := <-entering: clients[cli] = true case cli := <-leaving: delete(clients, cli) close(cli) } }\n} broadcaster监听来自全局的entering和leaving的channel来获知客户端的到来和离开事件。当其接收到其中的一个事件时,会更新clients集合,当该事件是离开行为时,它会关闭客户端的消息发送channel。broadcaster也会监听全局的消息channel,所有的客户端都会向这个channel中发送消息。当broadcaster接收到什么消息时,就会将其广播至所有连接到服务端的客户端。 现在让我们看看每一个客户端的goroutine。handleConn函数会为它的客户端创建一个消息发送channel并通过entering channel来通知客户端的到来。然后它会读取客户端发来的每一行文本,并通过全局的消息channel来将这些文本发送出去,并为每条消息带上发送者的前缀来标明消息身份。当客户端发送完毕后,handleConn会通过leaving这个channel来通知客户端的离开并关闭连接。 func handleConn(conn net.Conn) { ch := make(chan string) // outgoing client messages go clientWriter(conn, ch) who := conn.RemoteAddr().String() ch <- \"You are \" + who messages <- who + \" has arrived\" entering <- ch input := bufio.NewScanner(conn) for input.Scan() { messages <- who + \": \" + input.Text() } // NOTE: ignoring potential errors from input.Err() leaving <- ch messages <- who + \" has left\" conn.Close()\n} func clientWriter(conn net.Conn, ch <-chan string) { for msg := range ch { fmt.Fprintln(conn, msg) // NOTE: ignoring network errors }\n} 另外,handleConn为每一个客户端创建了一个clientWriter的goroutine,用来接收向客户端发送消息的channel中的广播消息,并将它们写入到客户端的网络连接。客户端的读取循环会在broadcaster接收到leaving通知并关闭了channel后终止。 下面演示的是当服务器有两个活动的客户端连接,并且在两个窗口中运行的情况,使用netcat来聊天: $ go build gopl.io/ch8/chat\n$ go build gopl.io/ch8/netcat3\n$ ./chat &\n$ ./netcat3\nYou are 127.0.0.1:64208 $ ./netcat3\n127.0.0.1:64211 has arrived You are 127.0.0.1:64211\nHi!\n127.0.0.1:64208: Hi! 127.0.0.1:64208: Hi! Hi yourself.\n127.0.0.1:64211: Hi yourself. 127.0.0.1:64211: Hi yourself.\n^C 127.0.0.1:64208 has left\n$ ./netcat3\nYou are 127.0.0.1:64216 127.0.0.1:64216 has arrived Welcome.\n127.0.0.1:64211: Welcome. 127.0.0.1:64211: Welcome. ^C\n127.0.0.1:64211 has left” 当与n个客户端保持聊天session时,这个程序会有2n+2个并发的goroutine,然而这个程序却并不需要显式的锁(§9.2)。clients这个map被限制在了一个独立的goroutine中,broadcaster,所以它不能被并发地访问。多个goroutine共享的变量只有这些channel和net.Conn的实例,两个东西都是并发安全的。我们会在下一章中更多地讲解约束,并发安全以及goroutine中共享变量的含义。 练习 8.12: 使broadcaster能够在每个新的客户端到来时通知它当前的客户端集合。这需要你在clients集合中,以及entering和leaving的channel中记录客户端的名字。 练习 8.13: 使聊天服务器能够断开空闲的客户端连接,比如最近五分钟之后没有发送任何消息的那些客户端。提示:可以在其它goroutine中调用conn.Close()来解除Read调用,就像input.Scanner()所做的那样。 练习 8.14: 修改聊天服务器的网络协议,这样每一个客户端就可以在entering时提供他们的名字。将消息前缀由之前的网络地址改为这个名字。 练习 8.15: 如果一个客户端没有及时地读取数据可能会导致所有的客户端被阻塞。修改broadcaster来跳过一条消息,而不是等待这个客户端一直到其准备好读写。或者为每一个客户端的消息发送channel建立缓冲区,这样大部分的消息便不会被丢掉;broadcaster应该用一个非阻塞的send向这个channel中发消息。","breadcrumbs":"Goroutines和Channels » 示例: 聊天服务 » 8.10. 示例: 聊天服务","id":"112","title":"8.10. 示例: 聊天服务"},"113":{"body":"前一章我们介绍了一些使用goroutine和channel这样直接而自然的方式来实现并发的方法。然而这样做我们实际上回避了在写并发代码时必须处理的一些重要而且细微的问题。 在本章中,我们会细致地了解并发机制。尤其是在多goroutine之间的共享变量,并发问题的分析手段,以及解决这些问题的基本模式。最后我们会解释goroutine和操作系统线程之间的技术上的一些区别。","breadcrumbs":"基于共享变量的并发 » 第9章 基于共享变量的并发","id":"113","title":"第9章 基于共享变量的并发"},"114":{"body":"在一个线性(就是说只有一个goroutine的)的程序中,程序的执行顺序只由程序的逻辑来决定。例如,我们有一段语句序列,第一个在第二个之前(废话),以此类推。在有两个或更多goroutine的程序中,每一个goroutine内的语句也是按照既定的顺序去执行的,但是一般情况下我们没法去知道分别位于两个goroutine的事件x和y的执行顺序,x是在y之前还是之后还是同时发生是没法判断的。当我们没有办法自信地确认一个事件是在另一个事件的前面或者后面发生的话,就说明x和y这两个事件是并发的。 考虑一下,一个函数在线性程序中可以正确地工作。如果在并发的情况下,这个函数依然可以正确地工作的话,那么我们就说这个函数是并发安全的,并发安全的函数不需要额外的同步工作。我们可以把这个概念概括为一个特定类型的一些方法和操作函数,对于某个类型来说,如果其所有可访问的方法和操作都是并发安全的话,那么该类型便是并发安全的。 在一个程序中有非并发安全的类型的情况下,我们依然可以使这个程序并发安全。确实,并发安全的类型是例外,而不是规则,所以只有当文档中明确地说明了其是并发安全的情况下,你才可以并发地去访问它。我们会避免并发访问大多数的类型,无论是将变量局限在单一的一个goroutine内,还是用互斥条件维持更高级别的不变性,都是为了这个目的。我们会在本章中说明这些术语。 相反,包级别的导出函数一般情况下都是并发安全的。由于package级的变量没法被限制在单一的gorouine,所以修改这些变量“必须”使用互斥条件。 一个函数在并发调用时没法工作的原因太多了,比如死锁(deadlock)、活锁(livelock)和饿死(resource starvation)。我们没有空去讨论所有的问题,这里我们只聚焦在竞争条件上。 竞争条件指的是程序在多个goroutine交叉执行操作时,没有给出正确的结果。竞争条件是很恶劣的一种场景,因为这种问题会一直潜伏在你的程序里,然后在非常少见的时候蹦出来,或许只是会在很大的负载时才会发生,又或许是会在使用了某一个编译器、某一种平台或者某一种架构的时候才会出现。这些使得竞争条件带来的问题非常难以复现而且难以分析诊断。 传统上经常用经济损失来为竞争条件做比喻,所以我们来看一个简单的银行账户程序。 // Package bank implements a bank with only one account.\npackage bank\nvar balance int\nfunc Deposit(amount int) { balance = balance + amount }\nfunc Balance() int { return balance } (当然我们也可以把Deposit存款函数写成balance += amount,这种形式也是等价的,不过长一些的形式解释起来更方便一些。) 对于这个简单的程序而言,我们一眼就能看出,以任意顺序调用函数Deposit和Balance都会得到正确的结果。也就是说,Balance函数会给出之前的所有存入的额度之和。然而,当我们并发地而不是顺序地调用这些函数的话,Balance就再也没办法保证结果正确了。考虑一下下面的两个goroutine,其代表了一个银行联合账户的两笔交易: // Alice:\ngo func() { bank.Deposit(200) // A1 fmt.Println(\"=\", bank.Balance()) // A2\n}() // Bob:\ngo bank.Deposit(100) // B Alice存了$200,然后检查她的余额,同时Bob存了$100。因为A1和A2是和B并发执行的,我们没法预测他们发生的先后顺序。直观地来看的话,我们会认为其执行顺序只有三种可能性:“Alice先”,“Bob先”以及“Alice/Bob/Alice”交错执行。下面的表格会展示经过每一步骤后balance变量的值。引号里的字符串表示余额单。 Alice first Bob first Alice/Bob/Alice 0 0 0 A1 200 B 100 A1 200 A2 \"= 200\" A1 300 B 300 B 300 A2 \"= 300\" A2 \"= 300\" 所有情况下最终的余额都是$300。唯一的变数是Alice的余额单是否包含了Bob交易,不过无论怎么着客户都不会在意。 但是事实是上面的直觉推断是错误的。第四种可能的结果是事实存在的,这种情况下Bob的存款会在Alice存款操作中间,在余额被读到(balance + amount)之后,在余额被更新之前(balance = ...),这样会导致Bob的交易丢失。而这是因为Alice的存款操作A1实际上是两个操作的一个序列,读取然后写;可以称之为A1r和A1w。下面是交叉时产生的问题: Data race\n0\nA1r 0 ... = balance + amount\nB 100\nA1w 200 balance = ...\nA2 \"= 200\" 在A1r之后,balance + amount会被计算为200,所以这是A1w会写入的值,并不受其它存款操作的干预。最终的余额是$200。银行的账户上的资产比Bob实际的资产多了$100。(译注:因为丢失了Bob的存款操作,所以其实是说Bob的钱丢了。) 这个程序包含了一个特定的竞争条件,叫作数据竞争。无论任何时候,只要有两个goroutine并发访问同一变量,且至少其中的一个是写操作的时候就会发生数据竞争。 如果数据竞争的对象是一个比一个机器字(译注:32位机器上一个字=4个字节)更大的类型时,事情就变得更麻烦了,比如interface,string或者slice类型都是如此。下面的代码会并发地更新两个不同长度的slice: var x []int\ngo func() { x = make([]int, 10) }()\ngo func() { x = make([]int, 1000000) }()\nx[999999] = 1 // NOTE: undefined behavior; memory corruption possible! 最后一个语句中的x的值是未定义的;其可能是nil,或者也可能是一个长度为10的slice,也可能是一个长度为1,000,000的slice。但是回忆一下slice的三个组成部分:指针(pointer)、长度(length)和容量(capacity)。如果指针是从第一个make调用来,而长度从第二个make来,x就变成了一个混合体,一个自称长度为1,000,000但实际上内部只有10个元素的slice。这样导致的结果是存储999,999元素的位置会碰撞一个遥远的内存位置,这种情况下难以对值进行预测,而且debug也会变成噩梦。这种语义雷区被称为未定义行为,对C程序员来说应该很熟悉;幸运的是在Go语言里造成的麻烦要比C里小得多。 尽管并发程序的概念让我们知道并发并不是简单的语句交叉执行。我们将会在9.4节中看到,数据竞争可能会有奇怪的结果。许多程序员,甚至一些非常聪明的人也还是会偶尔提出一些理由来允许数据竞争,比如:“互斥条件代价太高”,“这个逻辑只是用来做logging”,“我不介意丢失一些消息”等等。因为在他们的编译器或者平台上很少遇到问题,可能给了他们错误的信心。一个好的经验法则是根本就没有什么所谓的良性数据竞争。所以我们一定要避免数据竞争,那么在我们的程序中要如何做到呢? 我们来重复一下数据竞争的定义,因为实在太重要了:数据竞争会在两个以上的goroutine并发访问相同的变量且至少其中一个为写操作时发生。根据上述定义,有三种方式可以避免数据竞争: 第一种方法是不要去写变量。考虑一下下面的map,会被“懒”填充,也就是说在每个key被第一次请求到的时候才会去填值。如果Icon是被顺序调用的话,这个程序会工作很正常,但如果Icon被并发调用,那么对于这个map来说就会存在数据竞争。 var icons = make(map[string]image.Image)\nfunc loadIcon(name string) image.Image // NOTE: not concurrency-safe!\nfunc Icon(name string) image.Image { icon, ok := icons[name] if !ok { icon = loadIcon(name) icons[name] = icon } return icon\n} 反之,如果我们在创建goroutine之前的初始化阶段,就初始化了map中的所有条目并且再也不去修改它们,那么任意数量的goroutine并发访问Icon都是安全的,因为每一个goroutine都只是去读取而已。 var icons = map[string]image.Image{ \"spades.png\": loadIcon(\"spades.png\"), \"hearts.png\": loadIcon(\"hearts.png\"), \"diamonds.png\": loadIcon(\"diamonds.png\"), \"clubs.png\": loadIcon(\"clubs.png\"),\n} // Concurrency-safe.\nfunc Icon(name string) image.Image { return icons[name] } 上面的例子里icons变量在包初始化阶段就已经被赋值了,包的初始化是在程序main函数开始执行之前就完成了的。只要初始化完成了,icons就再也不会被修改。数据结构如果从不被修改或是不变量则是并发安全的,无需进行同步。不过显然,如果update操作是必要的,我们就没法用这种方法,比如说银行账户。 第二种避免数据竞争的方法是,避免从多个goroutine访问变量。这也是前一章中大多数程序所采用的方法。例如前面的并发web爬虫(§8.6)的main goroutine是唯一一个能够访问seen map的goroutine,而聊天服务器(§8.10)中的broadcaster goroutine是唯一一个能够访问clients map的goroutine。这些变量都被限定在了一个单独的goroutine中。 由于其它的goroutine不能够直接访问变量,它们只能使用一个channel来发送请求给指定的goroutine来查询更新变量。这也就是Go的口头禅“不要使用共享数据来通信;使用通信来共享数据”。一个提供对一个指定的变量通过channel来请求的goroutine叫做这个变量的monitor(监控)goroutine。例如broadcaster goroutine会监控clients map的全部访问。 下面是一个重写了的银行的例子,这个例子中balance变量被限制在了monitor goroutine中,名为teller: gopl.io/ch9/bank1 // Package bank provides a concurrency-safe bank with one account.\npackage bank var deposits = make(chan int) // send amount to deposit\nvar balances = make(chan int) // receive balance func Deposit(amount int) { deposits <- amount }\nfunc Balance() int { return <-balances } func teller() { var balance int // balance is confined to teller goroutine for { select { case amount := <-deposits: balance += amount case balances <- balance: } }\n} func init() { go teller() // start the monitor goroutine\n} 即使当一个变量无法在其整个生命周期内被绑定到一个独立的goroutine,绑定依然是并发问题的一个解决方案。例如在一条流水线上的goroutine之间共享变量是很普遍的行为,在这两者间会通过channel来传输地址信息。如果流水线的每一个阶段都能够避免在将变量传送到下一阶段后再去访问它,那么对这个变量的所有访问就是线性的。其效果是变量会被绑定到流水线的一个阶段,传送完之后被绑定到下一个,以此类推。这种规则有时被称为串行绑定。 下面的例子中,Cakes会被严格地顺序访问,先是baker gorouine,然后是icer gorouine: type Cake struct{ state string } func baker(cooked chan<- *Cake) { for { cake := new(Cake) cake.state = \"cooked\" cooked <- cake // baker never touches this cake again }\n} func icer(iced chan<- *Cake, cooked <-chan *Cake) { for cake := range cooked { cake.state = \"iced\" iced <- cake // icer never touches this cake again }\n} 第三种避免数据竞争的方法是允许很多goroutine去访问变量,但是在同一个时刻最多只有一个goroutine在访问。这种方式被称为“互斥”,在下一节来讨论这个主题。 练习 9.1: 给gopl.io/ch9/bank1程序添加一个Withdraw(amount int)取款函数。其返回结果应该要表明事务是成功了还是因为没有足够资金失败了。这条消息会被发送给monitor的goroutine,且消息需要包含取款的额度和一个新的channel,这个新channel会被monitor goroutine来把boolean结果发回给Withdraw。","breadcrumbs":"基于共享变量的并发 » 竞争条件 » 9.1. 竞争条件","id":"114","title":"9.1. 竞争条件"},"115":{"body":"在8.6节中,我们使用了一个buffered channel作为一个计数信号量,来保证最多只有20个goroutine会同时执行HTTP请求。同理,我们可以用一个容量只有1的channel来保证最多只有一个goroutine在同一时刻访问一个共享变量。一个只能为1和0的信号量叫做二元信号量(binary semaphore)。 gopl.io/ch9/bank2 var ( sema = make(chan struct{}, 1) // a binary semaphore guarding balance balance int\n) func Deposit(amount int) { sema <- struct{}{} // acquire token balance = balance + amount <-sema // release token\n} func Balance() int { sema <- struct{}{} // acquire token b := balance <-sema // release token return b\n} 这种互斥很实用,而且被sync包里的Mutex类型直接支持。它的Lock方法能够获取到token(这里叫锁),并且Unlock方法会释放这个token: gopl.io/ch9/bank3 import \"sync\" var ( mu sync.Mutex // guards balance balance int\n) func Deposit(amount int) { mu.Lock() balance = balance + amount mu.Unlock()\n} func Balance() int { mu.Lock() b := balance mu.Unlock() return b\n} 每次一个goroutine访问bank变量时(这里只有balance余额变量),它都会调用mutex的Lock方法来获取一个互斥锁。如果其它的goroutine已经获得了这个锁的话,这个操作会被阻塞直到其它goroutine调用了Unlock使该锁变回可用状态。mutex会保护共享变量。惯例来说,被mutex所保护的变量是在mutex变量声明之后立刻声明的。如果你的做法和惯例不符,确保在文档里对你的做法进行说明。 在Lock和Unlock之间的代码段中的内容goroutine可以随便读取或者修改,这个代码段叫做临界区。锁的持有者在其他goroutine获取该锁之前需要调用Unlock。goroutine在结束后释放锁是必要的,无论以哪条路径通过函数都需要释放,即使是在错误路径中,也要记得释放。 上面的bank程序例证了一种通用的并发模式。一系列的导出函数封装了一个或多个变量,那么访问这些变量唯一的方式就是通过这些函数来做(或者方法,对于一个对象的变量来说)。每一个函数在一开始就获取互斥锁并在最后释放锁,从而保证共享变量不会被并发访问。这种函数、互斥锁和变量的编排叫作监控monitor(这种老式单词的monitor是受“monitor goroutine”的术语启发而来的。两种用法都是一个代理人保证变量被顺序访问)。 由于在存款和查询余额函数中的临界区代码这么短——只有一行,没有分支调用——在代码最后去调用Unlock就显得更为直截了当。在更复杂的临界区的应用中,尤其是必须要尽早处理错误并返回的情况下,就很难去(靠人)判断对Lock和Unlock的调用是在所有路径中都能够严格配对的了。Go语言里的defer简直就是这种情况下的救星:我们用defer来调用Unlock,临界区会隐式地延伸到函数作用域的最后,这样我们就从“总要记得在函数返回之后或者发生错误返回时要记得调用一次Unlock”这种状态中获得了解放。Go会自动帮我们完成这些事情。 func Balance() int { mu.Lock() defer mu.Unlock() return balance\n} 上面的例子里Unlock会在return语句读取完balance的值之后执行,所以Balance函数是并发安全的。这带来的另一点好处是,我们再也不需要一个本地变量b了。 此外,一个deferred Unlock即使在临界区发生panic时依然会执行,这对于用recover(§5.10)来恢复的程序来说是很重要的。defer调用只会比显式地调用Unlock成本高那么一点点,不过却在很大程度上保证了代码的整洁性。大多数情况下对于并发程序来说,代码的整洁性比过度的优化更重要。如果可能的话尽量使用defer来将临界区扩展到函数的结束。 考虑一下下面的Withdraw函数。成功的时候,它会正确地减掉余额并返回true。但如果银行记录资金对交易来说不足,那么取款就会恢复余额,并返回false。 // NOTE: not atomic!\nfunc Withdraw(amount int) bool { Deposit(-amount) if Balance() < 0 { Deposit(amount) return false // insufficient funds } return true\n} 函数终于给出了正确的结果,但是还有一点讨厌的副作用。当过多的取款操作同时执行时,balance可能会瞬时被减到0以下。这可能会引起一个并发的取款被不合逻辑地拒绝。所以如果Bob尝试买一辆sports car时,Alice可能就没办法为她的早咖啡付款了。这里的问题是取款不是一个原子操作:它包含了三个步骤,每一步都需要去获取并释放互斥锁,但任何一次锁都不会锁上整个取款流程。 理想情况下,取款应该只在整个操作中获得一次互斥锁。下面这样的尝试是错误的: // NOTE: incorrect!\nfunc Withdraw(amount int) bool { mu.Lock() defer mu.Unlock() Deposit(-amount) if Balance() < 0 { Deposit(amount) return false // insufficient funds } return true\n} 上面这个例子中,Deposit会调用mu.Lock()第二次去获取互斥锁,但因为mutex已经锁上了,而无法被重入(译注:go里没有重入锁,关于重入锁的概念,请参考java)——也就是说没法对一个已经锁上的mutex来再次上锁——这会导致程序死锁,没法继续执行下去,Withdraw会永远阻塞下去。 关于Go的mutex不能重入这一点我们有很充分的理由。mutex的目的是确保共享变量在程序执行时的关键点上能够保证不变性。不变性的一层含义是“没有goroutine访问共享变量”,但实际上这里对于mutex保护的变量来说,不变性还包含更深层含义:当一个goroutine获得了一个互斥锁时,它能断定被互斥锁保护的变量正处于不变状态(译注:即没有其他代码块正在读写共享变量),在其获取并保持锁期间,可能会去更新共享变量,这样不变性只是短暂地被破坏,然而当其释放锁之后,锁必须保证共享变量重获不变性并且多个goroutine按顺序访问共享变量。尽管一个可以重入的mutex也可以保证没有其它的goroutine在访问共享变量,但它不具备不变性更深层含义。(译注: 更详细的解释 ,Russ Cox认为可重入锁是bug的温床,是一个失败的设计) 一个通用的解决方案是将一个函数分离为多个函数,比如我们把Deposit分离成两个:一个不导出的函数deposit,这个函数假设锁总是会被保持并去做实际的操作,另一个是导出的函数Deposit,这个函数会调用deposit,但在调用前会先去获取锁。同理我们可以将Withdraw也表示成这种形式: func Withdraw(amount int) bool { mu.Lock() defer mu.Unlock() deposit(-amount) if balance < 0 { deposit(amount) return false // insufficient funds } return true\n} func Deposit(amount int) { mu.Lock() defer mu.Unlock() deposit(amount)\n} func Balance() int { mu.Lock() defer mu.Unlock() return balance\n} // This function requires that the lock be held.\nfunc deposit(amount int) { balance += amount } 当然,这里的存款deposit函数很小,实际上取款Withdraw函数不需要理会对它的调用,尽管如此,这里的表达还是表明了规则。 封装(§6.6),用限制一个程序中的意外交互的方式,可以使我们获得数据结构的不变性。因为某种原因,封装还帮我们获得了并发的不变性。当你使用mutex时,确保mutex和其保护的变量没有被导出(在go里也就是小写,且不要被大写字母开头的函数访问啦),无论这些变量是包级的变量还是一个struct的字段。","breadcrumbs":"基于共享变量的并发 » sync.Mutex互斥锁 » 9.2. sync.Mutex互斥锁","id":"115","title":"9.2. sync.Mutex互斥锁"},"116":{"body":"在100刀的存款消失时不做记录多少还是会让我们有一些恐慌,Bob写了一个程序,每秒运行几百次来检查他的银行余额。他会在家,在工作中,甚至会在他的手机上来运行这个程序。银行注意到这些陡增的流量使得存款和取款有了延时,因为所有的余额查询请求是顺序执行的,这样会互斥地获得锁,并且会暂时阻止其它的goroutine运行。 由于Balance函数只需要读取变量的状态,所以我们同时让多个Balance调用并发运行事实上是安全的,只要在运行的时候没有存款或者取款操作就行。在这种场景下我们需要一种特殊类型的锁,其允许多个只读操作并行执行,但写操作会完全互斥。这种锁叫作“多读单写”锁(multiple readers, single writer lock),Go语言提供的这样的锁是sync.RWMutex: var mu sync.RWMutex\nvar balance int\nfunc Balance() int { mu.RLock() // readers lock defer mu.RUnlock() return balance\n} Balance函数现在调用了RLock和RUnlock方法来获取和释放一个读取或者共享锁。Deposit函数没有变化,会调用mu.Lock和mu.Unlock方法来获取和释放一个写或互斥锁。 在这次修改后,Bob的余额查询请求就可以彼此并行地执行并且会很快地完成了。锁在更多的时间范围可用,并且存款请求也能够及时地被响应了。 RLock只能在临界区共享变量没有任何写入操作时可用。一般来说,我们不应该假设逻辑上的只读函数/方法也不会去更新某一些变量。比如一个方法功能是访问一个变量,但它也有可能会同时去给一个内部的计数器+1(译注:可能是记录这个方法的访问次数啥的),或者去更新缓存——使即时的调用能够更快。如果有疑惑的话,请使用互斥锁。 RWMutex只有当获得锁的大部分goroutine都是读操作,而锁在竞争条件下,也就是说,goroutine们必须等待才能获取到锁的时候,RWMutex才是最能带来好处的。RWMutex需要更复杂的内部记录,所以会让它比一般的无竞争锁的mutex慢一些。","breadcrumbs":"基于共享变量的并发 » sync.RWMutex读写锁 » 9.3. sync.RWMutex读写锁","id":"116","title":"9.3. sync.RWMutex读写锁"},"117":{"body":"你可能比较纠结为什么Balance方法需要用到互斥条件,无论是基于channel还是基于互斥量。毕竟和存款不一样,它只由一个简单的操作组成,所以不会碰到其它goroutine在其执行“期间”执行其它逻辑的风险。这里使用mutex有两方面考虑。第一Balance不会在其它操作比如Withdraw“中间”执行。第二(更重要的)是“同步”不仅仅是一堆goroutine执行顺序的问题,同样也会涉及到内存的问题。 在现代计算机中可能会有一堆处理器,每一个都会有其本地缓存(local cache)。为了效率,对内存的写入一般会在每一个处理器中缓冲,并在必要时一起flush到主存。这种情况下这些数据可能会以与当初goroutine写入顺序不同的顺序被提交到主存。像channel通信或者互斥量操作这样的原语会使处理器将其聚集的写入flush并commit,这样goroutine在某个时间点上的执行结果才能被其它处理器上运行的goroutine得到。 考虑一下下面代码片段的可能输出: var x, y int\ngo func() { x = 1 // A1 fmt.Print(\"y:\", y, \" \") // A2\n}()\ngo func() { y = 1 // B1 fmt.Print(\"x:\", x, \" \") // B2\n}() 因为两个goroutine是并发执行,并且访问共享变量时也没有互斥,会有数据竞争,所以程序的运行结果没法预测的话也请不要惊讶。我们可能希望它能够打印出下面这四种结果中的一种,相当于几种不同的交错执行时的情况: y:0 x:1\nx:0 y:1\nx:1 y:1\ny:1 x:1 第四行可以被解释为执行顺序A1,B1,A2,B2或者B1,A1,A2,B2的执行结果。然而实际运行时还是有些情况让我们有点惊讶: x:0 y:0\ny:0 x:0 根据所使用的编译器,CPU,或者其它很多影响因子,这两种情况也是有可能发生的。那么这两种情况要怎么解释呢? 在一个独立的goroutine中,每一个语句的执行顺序是可以被保证的,也就是说goroutine内顺序是连贯的。但是在不使用channel且不使用mutex这样的显式同步操作时,我们就没法保证事件在不同的goroutine中看到的执行顺序是一致的了。尽管goroutine A中一定需要观察到x=1执行成功之后才会去读取y,但它没法确保自己观察得到goroutine B中对y的写入,所以A还可能会打印出y的一个旧版的值。 尽管去理解并发的一种尝试是去将其运行理解为不同goroutine语句的交错执行,但看看上面的例子,这已经不是现代的编译器和cpu的工作方式了。因为赋值和打印指向不同的变量,编译器可能会断定两条语句的顺序不会影响执行结果,并且会交换两个语句的执行顺序。如果两个goroutine在不同的CPU上执行,每一个核心有自己的缓存,这样一个goroutine的写入对于其它goroutine的Print,在主存同步之前就是不可见的了。 所有并发的问题都可以用一致的、简单的既定的模式来规避。所以可能的话,将变量限定在goroutine内部;如果是多个goroutine都需要访问的变量,使用互斥条件来访问。","breadcrumbs":"基于共享变量的并发 » 内存同步 » 9.4. 内存同步","id":"117","title":"9.4. 内存同步"},"118":{"body":"如果初始化成本比较大的话,那么将初始化延迟到需要的时候再去做就是一个比较好的选择。如果在程序启动的时候就去做这类初始化的话,会增加程序的启动时间,并且因为执行的时候可能也并不需要这些变量,所以实际上有一些浪费。让我们来看在本章早一些时候的icons变量: var icons map[string]image.Image 这个版本的Icon用到了懒初始化(lazy initialization)。 func loadIcons() { icons = map[string]image.Image{ \"spades.png\": loadIcon(\"spades.png\"), \"hearts.png\": loadIcon(\"hearts.png\"), \"diamonds.png\": loadIcon(\"diamonds.png\"), \"clubs.png\":\tloadIcon(\"clubs.png\"), }\n} // NOTE: not concurrency-safe!\nfunc Icon(name string) image.Image { if icons == nil { loadIcons() // one-time initialization } return icons[name]\n} 如果一个变量只被一个单独的goroutine所访问的话,我们可以使用上面的这种模板,但这种模板在Icon被并发调用时并不安全。就像前面银行的那个Deposit(存款)函数一样,Icon函数也是由多个步骤组成的:首先测试icons是否为空,然后load这些icons,之后将icons更新为一个非空的值。直觉会告诉我们最差的情况是loadIcons函数被多次访问会带来数据竞争。当第一个goroutine在忙着loading这些icons的时候,另一个goroutine进入了Icon函数,发现变量是nil,然后也会调用loadIcons函数。 不过这种直觉是错误的。(我们希望你从现在开始能够构建自己对并发的直觉,也就是说对并发的直觉总是不能被信任的!),回忆一下9.4节。因为缺少显式的同步,编译器和CPU是可以随意地去更改访问内存的指令顺序,以任意方式,只要保证每一个goroutine自己的执行顺序一致。其中一种可能loadIcons的语句重排是下面这样。它会在填写icons变量的值之前先用一个空map来初始化icons变量。 func loadIcons() { icons = make(map[string]image.Image) icons[\"spades.png\"] = loadIcon(\"spades.png\") icons[\"hearts.png\"] = loadIcon(\"hearts.png\") icons[\"diamonds.png\"] = loadIcon(\"diamonds.png\") icons[\"clubs.png\"] = loadIcon(\"clubs.png\")\n} 因此,一个goroutine在检查icons是非空时,也并不能就假设这个变量的初始化流程已经走完了(译注:可能只是塞了个空map,里面的值还没填完,也就是说填值的语句都没执行完呢)。 最简单且正确的保证所有goroutine能够观察到loadIcons效果的方式,是用一个mutex来同步检查。 var mu sync.Mutex // guards icons\nvar icons map[string]image.Image // Concurrency-safe.\nfunc Icon(name string) image.Image { mu.Lock() defer mu.Unlock() if icons == nil { loadIcons() } return icons[name]\n} 然而使用互斥访问icons的代价就是没有办法对该变量进行并发访问,即使变量已经被初始化完毕且再也不会进行变动。这里我们可以引入一个允许多读的锁: var mu sync.RWMutex // guards icons\nvar icons map[string]image.Image\n// Concurrency-safe.\nfunc Icon(name string) image.Image { mu.RLock() if icons != nil { icon := icons[name] mu.RUnlock() return icon } mu.RUnlock() // acquire an exclusive lock mu.Lock() if icons == nil { // NOTE: must recheck for nil loadIcons() } icon := icons[name] mu.Unlock() return icon\n} 上面的代码有两个临界区。goroutine首先会获取一个读锁,查询map,然后释放锁。如果条目被找到了(一般情况下),那么会直接返回。如果没有找到,那goroutine会获取一个写锁。不释放共享锁的话,也没有任何办法来将一个共享锁升级为一个互斥锁,所以我们必须重新检查icons变量是否为nil,以防止在执行这一段代码的时候,icons变量已经被其它gorouine初始化过了。 上面的模板使我们的程序能够更好的并发,但是有一点太复杂且容易出错。幸运的是,sync包为我们提供了一个专门的方案来解决这种一次性初始化的问题:sync.Once。概念上来讲,一次性的初始化需要一个互斥量mutex和一个boolean变量来记录初始化是不是已经完成了;互斥量用来保护boolean变量和客户端数据结构。Do这个唯一的方法需要接收初始化函数作为其参数。让我们用sync.Once来简化前面的Icon函数吧: var loadIconsOnce sync.Once\nvar icons map[string]image.Image\n// Concurrency-safe.\nfunc Icon(name string) image.Image { loadIconsOnce.Do(loadIcons) return icons[name]\n} 每一次对Do(loadIcons)的调用都会锁定mutex,并会检查boolean变量(译注:Go1.9中会先判断boolean变量是否为1(true),只有不为1才锁定mutex,不再需要每次都锁定mutex)。在第一次调用时,boolean变量的值是false,Do会调用loadIcons并会将boolean变量设置为true。随后的调用什么都不会做,但是mutex同步会保证loadIcons对内存(这里其实就是指icons变量啦)产生的效果能够对所有goroutine可见。用这种方式来使用sync.Once的话,我们能够避免在变量被构建完成之前和其它goroutine共享该变量。 练习 9.2: 重写2.6.2节中的PopCount的例子,使用sync.Once,只在第一次需要用到的时候进行初始化。(虽然实际上,对PopCount这样很小且高度优化的函数进行同步可能代价没法接受。)","breadcrumbs":"基于共享变量的并发 » sync.Once惰性初始化 » 9.5. sync.Once惰性初始化","id":"118","title":"9.5. sync.Once惰性初始化"},"119":{"body":"即使我们小心到不能再小心,但在并发程序中犯错还是太容易了。幸运的是,Go的runtime和工具链为我们装备了一个复杂但好用的动态分析工具,竞争检查器(the race detector)。 只要在go build,go run或者go test命令后面加上-race的flag,就会使编译器创建一个你的应用的“修改”版或者一个附带了能够记录所有运行期对共享变量访问工具的test,并且会记录下每一个读或者写共享变量的goroutine的身份信息。另外,修改版的程序会记录下所有的同步事件,比如go语句,channel操作,以及对(*sync.Mutex).Lock,(*sync.WaitGroup).Wait等等的调用。(完整的同步事件集合是在The Go Memory Model文档中有说明,该文档是和语言文档放在一起的。译注:https://golang.org/ref/mem ) 竞争检查器会检查这些事件,会寻找在哪一个goroutine中出现了这样的case,例如其读或者写了一个共享变量,这个共享变量是被另一个goroutine在没有进行干预同步操作便直接写入的。这种情况也就表明了是对一个共享变量的并发访问,即数据竞争。这个工具会打印一份报告,内容包含变量身份,读取和写入的goroutine中活跃的函数的调用栈。这些信息在定位问题时通常很有用。9.7节中会有一个竞争检查器的实战样例。 竞争检查器会报告所有的已经发生的数据竞争。然而,它只能检测到运行时的竞争条件;并不能证明之后不会发生数据竞争。所以为了使结果尽量正确,请保证你的测试并发地覆盖到了你的包。 由于需要额外的记录,因此构建时加了竞争检测的程序跑起来会慢一些,且需要更大的内存,即使是这样,这些代价对于很多生产环境的程序(工作)来说还是可以接受的。对于一些偶发的竞争条件来说,让竞争检查器来干活可以节省无数日夜的debugging。(译注:多少服务端C和C++程序员为此竞折腰。)","breadcrumbs":"基于共享变量的并发 » 竞争条件检测 » 9.6. 竞争条件检测","id":"119","title":"9.6. 竞争条件检测"},"12":{"body":"下面的程序会演示Go语言标准库里的image这个package的用法,我们会用这个包来生成一系列的bit-mapped图,然后将这些图片编码为一个GIF动画。我们生成的图形名字叫利萨如图形(Lissajous figures),这种效果是在1960年代的老电影里出现的一种视觉特效。它们是协振子在两个纬度上振动所产生的曲线,比如两个sin正弦波分别在x轴和y轴输入会产生的曲线。图1.1是这样的一个例子: 译注:要看这个程序的结果,需要将标准输出重定向到一个GIF图像文件(使用 ./lissajous > output.gif 命令)。下面是GIF图像动画效果: 这段代码里我们用了一些新的结构,包括const声明,struct结构体类型,复合声明。和我们举的其它的例子不太一样,这一个例子包含了浮点数运算。这些概念我们只在这里简单地说明一下,之后的章节会更详细地讲解。 gopl.io/ch1/lissajous // Lissajous generates GIF animations of random Lissajous figures.\npackage main import ( \"image\" \"image/color\" \"image/gif\" \"io\" \"math\" \"math/rand\" \"os\" \"time\"\n) var palette = []color.Color{color.White, color.Black} const ( whiteIndex = 0 // first color in palette blackIndex = 1 // next color in palette\n) func main() { // The sequence of images is deterministic unless we seed // the pseudo-random number generator using the current time. // Thanks to Randall McPherson for pointing out the omission. rand.Seed(time.Now().UTC().UnixNano()) lissajous(os.Stdout)\n} func lissajous(out io.Writer) { const ( cycles = 5 // number of complete x oscillator revolutions res = 0.001 // angular resolution size = 100 // image canvas covers [-size..+size] nframes = 64 // number of animation frames delay = 8 // delay between frames in 10ms units ) freq := rand.Float64() * 3.0 // relative frequency of y oscillator anim := gif.GIF{LoopCount: nframes} phase := 0.0 // phase difference for i := 0; i < nframes; i++ { rect := image.Rect(0, 0, 2*size+1, 2*size+1) img := image.NewPaletted(rect, palette) for t := 0.0; t < cycles*2*math.Pi; t += res { x := math.Sin(t) y := math.Sin(t*freq + phase) img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5), blackIndex) } phase += 0.1 anim.Delay = append(anim.Delay, delay) anim.Image = append(anim.Image, img) } gif.EncodeAll(out, &anim) // NOTE: ignoring encoding errors\n} 当我们import了一个包路径包含有多个单词的package时,比如image/color(image和color两个单词),通常我们只需要用最后那个单词表示这个包就可以。所以当我们写color.White时,这个变量指向的是image/color包里的变量,同理gif.GIF是属于image/gif包里的变量。 这个程序里的常量声明给出了一系列的常量值,常量是指在程序编译后运行时始终都不会变化的值,比如圈数、帧数、延迟值。常量声明和变量声明一般都会出现在包级别,所以这些常量在整个包中都是可以共享的,或者你也可以把常量声明定义在函数体内部,那么这种常量就只能在函数体内用。目前常量声明的值必须是一个数字值、字符串或者一个固定的boolean值。 []color.Color{...}和gif.GIF{...}这两个表达式就是我们说的复合声明(4.2和4.4.1节有说明)。这是实例化Go语言里的复合类型的一种写法。这里的前者生成的是一个slice切片,后者生成的是一个struct结构体。 gif.GIF是一个struct类型(参考4.4节)。struct是一组值或者叫字段的集合,不同的类型集合在一个struct可以让我们以一个统一的单元进行处理。anim是一个gif.GIF类型的struct变量。这种写法会生成一个struct变量,并且其内部变量LoopCount字段会被设置为nframes;而其它的字段会被设置为各自类型默认的零值。struct内部的变量可以以一个点(.)来进行访问,就像在最后两个赋值语句中显式地更新了anim这个struct的Delay和Image字段。 lissajous函数内部有两层嵌套的for循环。外层循环会循环64次,每一次都会生成一个单独的动画帧。它生成了一个包含两种颜色的201*201大小的图片,白色和黑色。所有像素点都会被默认设置为其零值(也就是调色板palette里的第0个值),这里我们设置的是白色。每次外层循环都会生成一张新图片,并将一些像素设置为黑色。其结果会append到之前结果之后。这里我们用到了append(参考4.2.1)内置函数,将结果append到anim中的帧列表末尾,并设置一个默认的80ms的延迟值。循环结束后所有的延迟值被编码进了GIF图片中,并将结果写入到输出流。out这个变量是io.Writer类型,这个类型支持把输出结果写到很多目标,很快我们就可以看到例子。 内层循环设置两个偏振值。x轴偏振使用sin函数。y轴偏振也是正弦波,但其相对x轴的偏振是一个0-3的随机值,初始偏振值是一个零值,随着动画的每一帧逐渐增加。循环会一直跑到x轴完成五次完整的循环。每一步它都会调用SetColorIndex来为(x,y)点来染黑色。 main函数调用lissajous函数,用它来向标准输出流打印信息,所以下面这个命令会像图1.1中产生一个GIF动画。 $ go build gopl.io/ch1/lissajous\n$ ./lissajous >out.gif 练习 1.5: 修改前面的Lissajous程序里的调色板,由黑色改为绿色。我们可以用color.RGBA{0xRR, 0xGG, 0xBB, 0xff}来得到#RRGGBB这个色值,三个十六进制的字符串分别代表红、绿、蓝像素。 练习 1.6: 修改Lissajous程序,修改其调色板来生成更丰富的颜色,然后修改SetColorIndex的第三个参数,看看显示结果吧。","breadcrumbs":"入门 » GIF动画 » 1.4. GIF动画","id":"12","title":"1.4. GIF动画"},"120":{"body":"本节中我们会做一个无阻塞的缓存,这种工具可以帮助我们来解决现实世界中并发程序出现但没有现成的库可以解决的问题。这个问题叫作缓存(memoizing)函数(译注:Memoization的定义: memoization 一词是Donald Michie 根据拉丁语memorandum杜撰的一个词。相应的动词、过去分词、ing形式有memoiz、memoized、memoizing),也就是说,我们需要缓存函数的返回结果,这样在对函数进行调用的时候,我们就只需要一次计算,之后只要返回计算的结果就可以了。我们的解决方案会是并发安全且会避免对整个缓存加锁而导致所有操作都去争一个锁的设计。 我们将使用下面的httpGetBody函数作为我们需要缓存的函数的一个样例。这个函数会去进行HTTP GET请求并且获取http响应body。对这个函数的调用本身开销是比较大的,所以我们尽量避免在不必要的时候反复调用。 func httpGetBody(url string) (interface{}, error) { resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() return ioutil.ReadAll(resp.Body)\n} 最后一行稍微隐藏了一些细节。ReadAll会返回两个结果,一个[]byte数组和一个错误,不过这两个对象可以被赋值给httpGetBody的返回声明里的interface{}和error类型,所以我们也就可以这样返回结果并且不需要额外的工作了。我们在httpGetBody中选用这种返回类型是为了使其可以与缓存匹配。 下面是我们要设计的cache的第一个“草稿”: gopl.io/ch9/memo1 // Package memo provides a concurrency-unsafe\n// memoization of a function of type Func.\npackage memo // A Memo caches the results of calling a Func.\ntype Memo struct { f Func cache map[string]result\n} // Func is the type of the function to memoize.\ntype Func func(key string) (interface{}, error) type result struct { value interface{} err error\n} func New(f Func) *Memo { return &Memo{f: f, cache: make(map[string]result)}\n} // NOTE: not concurrency-safe!\nfunc (memo *Memo) Get(key string) (interface{}, error) { res, ok := memo.cache[key] if !ok { res.value, res.err = memo.f(key) memo.cache[key] = res } return res.value, res.err\n} Memo实例会记录需要缓存的函数f(类型为Func),以及缓存内容(里面是一个string到result映射的map)。每一个result都是简单的函数返回的值对儿——一个值和一个错误值。继续下去我们会展示一些Memo的变种,不过所有的例子都会遵循上面的这些方面。 下面是一个使用Memo的例子。对于流入的URL的每一个元素我们都会调用Get,并打印调用延时以及其返回的数据大小的log: m := memo.New(httpGetBody)\nfor url := range incomingURLs() { start := time.Now() value, err := m.Get(url) if err != nil { log.Print(err) } fmt.Printf(\"%s, %s, %d bytes\\n\", url, time.Since(start), len(value.([]byte)))\n} 我们可以使用测试包(第11章的主题)来系统地鉴定缓存的效果。从下面的测试输出,我们可以看到URL流包含了一些重复的情况,尽管我们第一次对每一个URL的(*Memo).Get的调用都会花上几百毫秒,但第二次就只需要花1毫秒就可以返回完整的数据了。 $ go test -v gopl.io/ch9/memo1\n=== RUN Test\nhttps://golang.org, 175.026418ms, 7537 bytes\nhttps://godoc.org, 172.686825ms, 6878 bytes\nhttps://play.golang.org, 115.762377ms, 5767 bytes\nhttp://gopl.io, 749.887242ms, 2856 bytes\nhttps://golang.org, 721ns, 7537 bytes\nhttps://godoc.org, 152ns, 6878 bytes\nhttps://play.golang.org, 205ns, 5767 bytes\nhttp://gopl.io, 326ns, 2856 bytes\n--- PASS: Test (1.21s)\nPASS\nok gopl.io/ch9/memo1 1.257s 这个测试是顺序地去做所有的调用的。 由于这种彼此独立的HTTP请求可以很好地并发,我们可以把这个测试改成并发形式。可以使用sync.WaitGroup来等待所有的请求都完成之后再返回。 m := memo.New(httpGetBody)\nvar n sync.WaitGroup\nfor url := range incomingURLs() { n.Add(1) go func(url string) { start := time.Now() value, err := m.Get(url) if err != nil { log.Print(err) } fmt.Printf(\"%s, %s, %d bytes\\n\", url, time.Since(start), len(value.([]byte))) n.Done() }(url)\n}\nn.Wait() 这次测试跑起来更快了,然而不幸的是貌似这个测试不是每次都能够正常工作。我们注意到有一些意料之外的cache miss(缓存未命中),或者命中了缓存但却返回了错误的值,或者甚至会直接崩溃。 但更糟糕的是,有时候这个程序还是能正确的运行(译:也就是最让人崩溃的偶发bug),所以我们甚至可能都不会意识到这个程序有bug。但是我们可以使用-race这个flag来运行程序,竞争检测器(§9.6)会打印像下面这样的报告: $ go test -run=TestConcurrent -race -v gopl.io/ch9/memo1\n=== RUN TestConcurrent\n...\nWARNING: DATA RACE\nWrite by goroutine 36: runtime.mapassign1() ~/go/src/runtime/hashmap.go:411 +0x0 gopl.io/ch9/memo1.(*Memo).Get() ~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205 ...\nPrevious write by goroutine 35: runtime.mapassign1() ~/go/src/runtime/hashmap.go:411 +0x0 gopl.io/ch9/memo1.(*Memo).Get() ~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205\n...\nFound 1 data race(s)\nFAIL gopl.io/ch9/memo1 2.393s memo.go的32行出现了两次,说明有两个goroutine在没有同步干预的情况下更新了cache map。这表明Get不是并发安全的,存在数据竞争。 28 func (memo *Memo) Get(key string) (interface{}, error) {\n29 res, ok := memo.cache(key)\n30 if !ok {\n31 res.value, res.err = memo.f(key)\n32 memo.cache[key] = res\n33 }\n34 return res.value, res.err\n35 } 最简单的使cache并发安全的方式是使用基于监控的同步。只要给Memo加上一个mutex,在Get的一开始获取互斥锁,return的时候释放锁,就可以让cache的操作发生在临界区内了: gopl.io/ch9/memo2 type Memo struct { f Func mu sync.Mutex // guards cache cache map[string]result\n} // Get is concurrency-safe.\nfunc (memo *Memo) Get(key string) (value interface{}, err error) { memo.mu.Lock() res, ok := memo.cache[key] if !ok { res.value, res.err = memo.f(key) memo.cache[key] = res } memo.mu.Unlock() return res.value, res.err\n} 测试依然并发进行,但这回竞争检查器“沉默”了。不幸的是对于Memo的这一点改变使我们完全丧失了并发的性能优点。每次对f的调用期间都会持有锁,Get将本来可以并行运行的I/O操作串行化了。我们本章的目的是完成一个无锁缓存,而不是现在这样的将所有请求串行化的函数的缓存。 下一个Get的实现,调用Get的goroutine会两次获取锁:查找阶段获取一次,如果查找没有返回任何内容,那么进入更新阶段会再次获取。在这两次获取锁的中间阶段,其它goroutine可以随意使用cache。 gopl.io/ch9/memo3 func (memo *Memo) Get(key string) (value interface{}, err error) { memo.mu.Lock() res, ok := memo.cache[key] memo.mu.Unlock() if !ok { res.value, res.err = memo.f(key) // Between the two critical sections, several goroutines // may race to compute f(key) and update the map. memo.mu.Lock() memo.cache[key] = res memo.mu.Unlock() } return res.value, res.err\n} 这些修改使性能再次得到了提升,但有一些URL被获取了两次。这种情况在两个以上的goroutine同一时刻调用Get来请求同样的URL时会发生。多个goroutine一起查询cache,发现没有值,然后一起调用f这个慢不拉叽的函数。在得到结果后,也都会去更新map。其中一个获得的结果会覆盖掉另一个的结果。 理想情况下是应该避免掉多余的工作的。而这种“避免”工作一般被称为duplicate suppression(重复抑制/避免)。下面版本的Memo每一个map元素都是指向一个条目的指针。每一个entry包含对函数f调用结果的内容缓存。与之前不同的是这次entry还包含了一个叫ready的channel。在entry的res字段被设置之后,这个channel就会被关闭,以向其它goroutine广播(§8.9)去读取该entry内的结果是安全的了。 gopl.io/ch9/memo4 type entry struct { res result ready chan struct{} // closed when res is ready\n} func New(f Func) *Memo { return &Memo{f: f, cache: make(map[string]*entry)}\n} type Memo struct { f Func mu sync.Mutex // guards cache cache map[string]*entry\n} func (memo *Memo) Get(key string) (value interface{}, err error) { memo.mu.Lock() e := memo.cache[key] if e == nil { // This is the first request for this key. // This goroutine becomes responsible for computing // the value and broadcasting the ready condition. e = &entry{ready: make(chan struct{})} memo.cache[key] = e memo.mu.Unlock() e.res.value, e.res.err = memo.f(key) close(e.ready) // broadcast ready condition } else { // This is a repeat request for this key. memo.mu.Unlock() <-e.ready // wait for ready condition } return e.res.value, e.res.err\n} 现在Get函数包括下面这些步骤了:获取互斥锁来保护共享变量cache map,查询map中是否存在指定条目,如果没有找到那么分配空间插入一个新条目,释放互斥锁。如果存在条目的话且其值没有写入完成(也就是有其它的goroutine在调用f这个慢函数)时,goroutine必须等待值ready之后才能读到条目的结果。而想知道是否ready的话,可以直接从ready channel中读取,由于这个读取操作在channel关闭之前一直是阻塞。 如果没有条目的话,需要向map中插入一个没有准备好的条目,当前正在调用的goroutine就需要负责调用慢函数、更新条目以及向其它所有goroutine广播条目已经ready可读的消息了。 条目中的e.res.value和e.res.err变量是在多个goroutine之间共享的。创建条目的goroutine同时也会设置条目的值,其它goroutine在收到\"ready\"的广播消息之后立刻会去读取条目的值。尽管会被多个goroutine同时访问,但却并不需要互斥锁。ready channel的关闭一定会发生在其它goroutine接收到广播事件之前,因此第一个goroutine对这些变量的写操作是一定发生在这些读操作之前的。不会发生数据竞争。 这样并发、不重复、无阻塞的cache就完成了。 上面这样Memo的实现使用了一个互斥量来保护多个goroutine调用Get时的共享map变量。不妨把这种设计和前面提到的把map变量限制在一个单独的monitor goroutine的方案做一些对比,后者在调用Get时需要发消息。 Func、result和entry的声明和之前保持一致: // Func is the type of the function to memoize.\ntype Func func(key string) (interface{}, error) // A result is the result of calling a Func.\ntype result struct { value interface{} err error\n} type entry struct { res result ready chan struct{} // closed when res is ready\n} 然而Memo类型现在包含了一个叫做requests的channel,Get的调用方用这个channel来和monitor goroutine来通信。requests channel中的元素类型是request。Get的调用方会把这个结构中的两组key都填充好,实际上用这两个变量来对函数进行缓存的。另一个叫response的channel会被拿来发送响应结果。这个channel只会传回一个单独的值。 gopl.io/ch9/memo5 // A request is a message requesting that the Func be applied to key.\ntype request struct { key string response chan<- result // the client wants a single result\n} type Memo struct{ requests chan request }\n// New returns a memoization of f. Clients must subsequently call Close.\nfunc New(f Func) *Memo { memo := &Memo{requests: make(chan request)} go memo.server(f) return memo\n} func (memo *Memo) Get(key string) (interface{}, error) { response := make(chan result) memo.requests <- request{key, response} res := <-response return res.value, res.err\n} func (memo *Memo) Close() { close(memo.requests) } 上面的Get方法,会创建一个response channel,把它放进request结构中,然后发送给monitor goroutine,然后马上又会接收它。 cache变量被限制在了monitor goroutine ``(*Memo).server`中,下面会看到。monitor会在循环中一直读取请求,直到request channel被Close方法关闭。每一个请求都会去查询cache,如果没有找到条目的话,那么就会创建/插入一个新的条目。 func (memo *Memo) server(f Func) { cache := make(map[string]*entry) for req := range memo.requests { e := cache[req.key] if e == nil { // This is the first request for this key. e = &entry{ready: make(chan struct{})} cache[req.key] = e go e.call(f, req.key) // call f(key) } go e.deliver(req.response) }\n} func (e *entry) call(f Func, key string) { // Evaluate the function. e.res.value, e.res.err = f(key) // Broadcast the ready condition. close(e.ready)\n} func (e *entry) deliver(response chan<- result) { // Wait for the ready condition. <-e.ready // Send the result to the client. response <- e.res\n} 和基于互斥量的版本类似,第一个对某个key的请求需要负责去调用函数f并传入这个key,将结果存在条目里,并关闭ready channel来广播条目的ready消息。使用(*entry).call来完成上述工作。 紧接着对同一个key的请求会发现map中已经有了存在的条目,然后会等待结果变为ready,并将结果从response发送给客户端的goroutine。上述工作是用(*entry).deliver来完成的。对call和deliver方法的调用必须让它们在自己的goroutine中进行以确保monitor goroutines不会因此而被阻塞住而没法处理新的请求。 这个例子说明我们无论用上锁,还是通信来建立并发程序都是可行的。 上面的两种方案并不好说特定情境下哪种更好,不过了解他们还是有价值的。有时候从一种方式切换到另一种可以使你的代码更为简洁。(译注:不是说好的golang推崇通信并发么。) 练习 9.3: 扩展Func类型和(*Memo).Get方法,支持调用方提供一个可选的done channel,使其具备通过该channel来取消整个操作的能力(§8.9)。一个被取消了的Func的调用结果不应该被缓存。","breadcrumbs":"基于共享变量的并发 » 示例: 并发的非阻塞缓存 » 9.7. 示例: 并发的非阻塞缓存","id":"120","title":"9.7. 示例: 并发的非阻塞缓存"},"121":{"body":"在上一章中我们说goroutine和操作系统的线程区别可以先忽略。尽管两者的区别实际上只是一个量的区别,但量变会引起质变的道理同样适用于goroutine和线程。现在正是我们来区分开两者的最佳时机。","breadcrumbs":"基于共享变量的并发 » Goroutines和线程 » 9.8. Goroutines和线程","id":"121","title":"9.8. Goroutines和线程"},"122":{"body":"每一个OS线程都有一个固定大小的内存块(一般会是2MB)来做栈,这个栈会用来存储当前正在被调用或挂起(指在调用其它函数时)的函数的内部变量。这个固定大小的栈同时很大又很小。因为2MB的栈对于一个小小的goroutine来说是很大的内存浪费,比如对于我们用到的,一个只是用来WaitGroup之后关闭channel的goroutine来说。而对于go程序来说,同时创建成百上千个goroutine是非常普遍的,如果每一个goroutine都需要这么大的栈的话,那这么多的goroutine就不太可能了。除去大小的问题之外,固定大小的栈对于更复杂或者更深层次的递归函数调用来说显然是不够的。修改固定的大小可以提升空间的利用率,允许创建更多的线程,并且可以允许更深的递归调用,不过这两者是没法同时兼备的。 相反,一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB。一个goroutine的栈,和操作系统线程一样,会保存其活跃或挂起的函数调用的本地变量,但是和OS线程不太一样的是,一个goroutine的栈大小并不是固定的;栈的大小会根据需要动态地伸缩。而goroutine的栈的最大值有1GB,比传统的固定大小的线程栈要大得多,尽管一般情况下,大多goroutine都不需要这么大的栈。 练习 9.4: 创建一个流水线程序,支持用channel连接任意数量的goroutine,在跑爆内存之前,可以创建多少流水线阶段?一个变量通过整个流水线需要用多久?(这个练习题翻译不是很确定)","breadcrumbs":"基于共享变量的并发 » Goroutines和线程 » 9.8.1. 动态栈","id":"122","title":"9.8.1. 动态栈"},"123":{"body":"OS线程会被操作系统内核调度。每几毫秒,一个硬件计时器会中断处理器,这会调用一个叫作scheduler的内核函数。这个函数会挂起当前执行的线程并将它的寄存器内容保存到内存中,检查线程列表并决定下一次哪个线程可以被运行,并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行线程。因为操作系统线程是被内核所调度,所以从一个线程向另一个“移动”需要完整的上下文切换,也就是说,保存一个用户线程的状态到内存,恢复另一个线程的到寄存器,然后更新调度器的数据结构。这几步操作很慢,因为其局部性很差需要几次内存访问,并且会增加运行的cpu周期。 Go的运行时包含了其自己的调度器,这个调度器使用了一些技术手段,比如m:n调度,因为其会在n个操作系统线程上多工(调度)m个goroutine。Go调度器的工作和内核的调度是相似的,但是这个调度器只关注单独的Go程序中的goroutine(译注:按程序独立)。 和操作系统的线程调度不同的是,Go调度器并不是用一个硬件定时器,而是被Go语言“建筑”本身进行调度的。例如当一个goroutine调用了time.Sleep,或者被channel调用或者mutex操作阻塞时,调度器会使其进入休眠并开始执行另一个goroutine,直到时机到了再去唤醒第一个goroutine。因为这种调度方式不需要进入内核的上下文,所以重新调度一个goroutine比调度一个线程代价要低得多。 练习 9.5: 写一个有两个goroutine的程序,两个goroutine会向两个无buffer channel反复地发送ping-pong消息。这样的程序每秒可以支持多少次通信?","breadcrumbs":"基于共享变量的并发 » Goroutines和线程 » 9.8.2. Goroutine调度","id":"123","title":"9.8.2. Goroutine调度"},"124":{"body":"Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码。其默认的值是运行机器上的CPU的核心数,所以在一个有8个核心的机器上时,调度器一次会在8个OS线程上去调度GO代码。(GOMAXPROCS是前面说的m:n调度中的n)。在休眠中的或者在通信中被阻塞的goroutine是不需要一个对应的线程来做调度的。在I/O中或系统调用中或调用非Go语言函数时,是需要一个对应的操作系统线程的,但是GOMAXPROCS并不需要将这几种情况计算在内。 你可以用GOMAXPROCS的环境变量来显式地控制这个参数,或者也可以在运行时用runtime.GOMAXPROCS函数来修改它。我们在下面的小程序中会看到GOMAXPROCS的效果,这个程序会无限打印0和1。 for { go fmt.Print(0) fmt.Print(1)\n} $ GOMAXPROCS=1 go run hacker-cliché.go\n111111111111111111110000000000000000000011111... $ GOMAXPROCS=2 go run hacker-cliché.go\n010101010101010101011001100101011010010100110... 在第一次执行时,最多同时只能有一个goroutine被执行。初始情况下只有main goroutine被执行,所以会打印很多1。过了一段时间后,GO调度器会将其置为休眠,并唤醒另一个goroutine,这时候就开始打印很多0了,在打印的时候,goroutine是被调度到操作系统线程上的。在第二次执行时,我们使用了两个操作系统线程,所以两个goroutine可以一起被执行,以同样的频率交替打印0和1。我们必须强调的是goroutine的调度是受很多因子影响的,而runtime也是在不断地发展演进的,所以这里的你实际得到的结果可能会因为版本的不同而与我们运行的结果有所不同。 练习9.6: 测试一下计算密集型的并发程序(练习8.5那样的)会被GOMAXPROCS怎样影响到。在你的电脑上最佳的值是多少?你的电脑CPU有多少个核心?","breadcrumbs":"基于共享变量的并发 » Goroutines和线程 » 9.8.3. GOMAXPROCS","id":"124","title":"9.8.3. GOMAXPROCS"},"125":{"body":"在大多数支持多线程的操作系统和程序语言中,当前的线程都有一个独特的身份(id),并且这个身份信息可以以一个普通值的形式被很容易地获取到,典型的可以是一个integer或者指针值。这种情况下我们做一个抽象化的thread-local storage(线程本地存储,多线程编程中不希望其它线程访问的内容)就很容易,只需要以线程的id作为key的一个map就可以解决问题,每一个线程以其id就能从中获取到值,且和其它线程互不冲突。 goroutine没有可以被程序员获取到的身份(id)的概念。这一点是设计上故意而为之,由于thread-local storage总是会被滥用。比如说,一个web server是用一种支持tls的语言实现的,而非常普遍的是很多函数会去寻找HTTP请求的信息,这代表它们就是去其存储层(这个存储层有可能是tls)查找的。这就像是那些过分依赖全局变量的程序一样,会导致一种非健康的“距离外行为”,在这种行为下,一个函数的行为可能并不仅由自己的参数所决定,而是由其所运行在的线程所决定。因此,如果线程本身的身份会改变——比如一些worker线程之类的——那么函数的行为就会变得神秘莫测。 Go鼓励更为简单的模式,这种模式下参数(译注:外部显式参数和内部显式参数。tls 中的内容算是\"外部\"隐式参数)对函数的影响都是显式的。这样不仅使程序变得更易读,而且会让我们自由地向一些给定的函数分配子任务时不用担心其身份信息影响行为。 你现在应该已经明白了写一个Go程序所需要的所有语言特性信息。在后面两章节中,我们会回顾一些之前的实例和工具,支持我们写出更大规模的程序:如何将一个工程组织成一系列的包,如何获取,构建,测试,性能测试,剖析,写文档,并且将这些包分享出去。","breadcrumbs":"基于共享变量的并发 » Goroutines和线程 » 9.8.4. Goroutine没有ID号","id":"125","title":"9.8.4. Goroutine没有ID号"},"126":{"body":"现在随便一个小程序的实现都可能包含超过10000个函数。然而作者一般只需要考虑其中很小的一部分和做很少的设计,因为绝大部分代码都是由他人编写的,它们通过类似包或模块的方式被重用。 Go语言有超过100个的标准包(译注:可以用go list std | wc -l命令查看标准包的具体数目),标准库为大多数的程序提供了必要的基础构件。在Go的社区,有很多成熟的包被设计、共享、重用和改进,目前互联网上已经发布了非常多的Go语言开源包,它们可以通过 http://godoc.org 检索。在本章,我们将演示如何使用已有的包和创建新的包。 Go还自带了工具箱,里面有很多用来简化工作区和包管理的小工具。在本书开始的时候,我们已经见识过如何使用工具箱自带的工具来下载、构建和运行我们的演示程序了。在本章,我们将看看这些工具的基本设计理论和尝试更多的功能,例如打印工作区中包的文档和查询相关的元数据等。在下一章,我们将探讨testing包的单元测试用法。","breadcrumbs":"包和工具 » 第10章 包和工具","id":"126","title":"第10章 包和工具"},"127":{"body":"任何包系统设计的目的都是为了简化大型程序的设计和维护工作,通过将一组相关的特性放进一个独立的单元以便于理解和更新,在每个单元更新的同时保持和程序中其它单元的相对独立性。这种模块化的特性允许每个包可以被其它的不同项目共享和重用,在项目范围内、甚至全球范围统一的分发和复用。 每个包一般都定义了一个不同的名字空间用于它内部的每个标识符的访问。每个名字空间关联到一个特定的包,让我们给类型、函数等选择简短明了的名字,这样可以在使用它们的时候减少和其它部分名字的冲突。 每个包还通过控制包内名字的可见性和是否导出来实现封装特性。通过限制包成员的可见性并隐藏包API的具体实现,将允许包的维护者在不影响外部包用户的前提下调整包的内部实现。通过限制包内变量的可见性,还可以强制用户通过某些特定函数来访问和更新内部变量,这样可以保证内部变量的一致性和并发时的互斥约束。 当我们修改了一个源文件,我们必须重新编译该源文件对应的包和所有依赖该包的其他包。即使是从头构建,Go语言编译器的编译速度也明显快于其它编译语言。Go语言的闪电般的编译速度主要得益于三个语言特性。第一点,所有导入的包必须在每个文件的开头显式声明,这样的话编译器就没有必要读取和分析整个源文件来判断包的依赖关系。第二点,禁止包的环状依赖,因为没有循环依赖,包的依赖关系形成一个有向无环图,每个包可以被独立编译,而且很可能是被并发编译。第三点,编译后包的目标文件不仅仅记录包本身的导出信息,目标文件同时还记录了包的依赖关系。因此,在编译一个包的时候,编译器只需要读取每个直接导入包的目标文件,而不需要遍历所有依赖的的文件(译注:很多都是重复的间接依赖)。","breadcrumbs":"包和工具 » 包简介 » 10.1. 包简介","id":"127","title":"10.1. 包简介"},"128":{"body":"每个包是由一个全局唯一的字符串所标识的导入路径定位。出现在import语句中的导入路径也是字符串。 import ( \"fmt\" \"math/rand\" \"encoding/json\" \"golang.org/x/net/html\" \"github.com/go-sql-driver/mysql\"\n) 就像我们在2.6.1节提到过的,Go语言的规范并没有指明包的导入路径字符串的具体含义,导入路径的具体含义是由构建工具来解释的。在本章,我们将深入讨论Go语言工具箱的功能,包括大家经常使用的构建测试等功能。当然,也有第三方扩展的工具箱存在。例如,Google公司内部的Go语言码农,他们就使用内部的多语言构建系统(译注:Google公司使用的是类似 Bazel 的构建系统,支持多种编程语言,目前该构件系统还不能完整支持Windows环境),用不同的规则来处理包名字和定位包,用不同的规则来处理单元测试等等,因为这样可以更紧密适配他们内部环境。 如果你计划分享或发布包,那么导入路径最好是全球唯一的。为了避免冲突,所有非标准库包的导入路径建议以所在组织的互联网域名为前缀;而且这样也有利于包的检索。例如,上面的import语句导入了Go团队维护的HTML解析器和一个流行的第三方维护的MySQL驱动。","breadcrumbs":"包和工具 » 导入路径 » 10.2. 导入路径","id":"128","title":"10.2. 导入路径"},"129":{"body":"在每个Go语言源文件的开头都必须有包声明语句。包声明语句的主要目的是确定当前包被其它包导入时默认的标识符(也称为包名)。 例如,math/rand包的每个源文件的开头都包含package rand包声明语句,所以当你导入这个包,你就可以用rand.Int、rand.Float64类似的方式访问包的成员。 package main import ( \"fmt\" \"math/rand\"\n) func main() { fmt.Println(rand.Int())\n} 通常来说,默认的包名就是包导入路径名的最后一段,因此即使两个包的导入路径不同,它们依然可能有一个相同的包名。例如,math/rand包和crypto/rand包的包名都是rand。稍后我们将看到如何同时导入两个有相同包名的包。 关于默认包名一般采用导入路径名的最后一段的约定也有三种例外情况。第一个例外,包对应一个可执行程序,也就是main包,这时候main包本身的导入路径是无关紧要的。名字为main的包是给go build(§10.7.3)构建命令一个信息,这个包编译完之后必须调用连接器生成一个可执行程序。 第二个例外,包所在的目录中可能有一些文件名是以_test.go为后缀的Go源文件(译注:前面必须有其它的字符,因为以_或.开头的源文件会被构建工具忽略),并且这些源文件声明的包名也是以_test为后缀名的。这种目录可以包含两种包:一种是普通包,另一种则是测试的外部扩展包。所有以_test为后缀包名的测试外部扩展包都由go test命令独立编译,普通包和测试的外部扩展包是相互独立的。测试的外部扩展包一般用来避免测试代码中的循环导入依赖,具体细节我们将在11.2.4节中介绍。 第三个例外,一些依赖版本号的管理工具会在导入路径后追加版本号信息,例如“gopkg.in/yaml.v2”。这种情况下包的名字并不包含版本号后缀,而是yaml。","breadcrumbs":"包和工具 » 包声明 » 10.3. 包声明","id":"129","title":"10.3. 包声明"},"13":{"body":"对于很多现代应用来说,访问互联网上的信息和访问本地文件系统一样重要。Go语言在net这个强大package的帮助下提供了一系列的package来做这件事情,使用这些包可以更简单地用网络收发信息,还可以建立更底层的网络连接,编写服务器程序。在这些情景下,Go语言原生的并发特性(在第八章中会介绍)显得尤其好用。 为了最简单地展示基于HTTP获取信息的方式,下面给出一个示例程序fetch,这个程序将获取对应的url,并将其源文本打印出来;这个例子的灵感来源于curl工具(译注:unix下的一个用来发http请求的工具,具体可以man curl)。当然,curl提供的功能更为复杂丰富,这里只编写最简单的样例。这个样例之后还会多次被用到。 gopl.io/ch1/fetch // Fetch prints the content found at a URL.\npackage main import ( \"fmt\" \"io/ioutil\" \"net/http\" \"os\"\n) func main() { for _, url := range os.Args[1:] { resp, err := http.Get(url) if err != nil { fmt.Fprintf(os.Stderr, \"fetch: %v\\n\", err) os.Exit(1) } b, err := io.ReadAll(resp.Body) resp.Body.Close() if err != nil { fmt.Fprintf(os.Stderr, \"fetch: reading %s: %v\\n\", url, err) os.Exit(1) } fmt.Printf(\"%s\", b) }\n} 这个程序从两个package中导入了函数,net/http和io,http.Get函数是创建HTTP请求的函数,如果获取过程没有出错,那么会在resp这个结构体中得到访问的请求结果。resp的Body字段包括一个可读的服务器响应流。io.ReadAll函数从response中读取到全部内容;将其结果保存在变量b中。resp.Body.Close关闭resp的Body流,防止资源泄露,Printf函数会将结果b写出到标准输出流中。 $ go build gopl.io/ch1/fetch\n$ ./fetch http://gopl.io\n\n\n# | State | User | Title | \n
---|---|---|---|
{{.Number}} | {{.State}} | {{.User.Login}} | {{.Title}} | \n
A: {{.A}}
B: {{.B}}
` t := template.Must(template.New(\"escape\").Parse(templ)) var data struct { A string // untrusted plain text B template.HTML // trusted HTML } data.A = \"Hello!\" data.B = \"Hello!\" if err := t.Execute(os.Stdout, data); err != nil { log.Fatal(err) }\n} 图4.6显示了出现在浏览器中的模板输出。我们看到A的黑体标记被转义失效了,但是B没有。 我们这里只讲述了模板系统中最基本的特性。一如既往,如果想了解更多的信息,请自己查看包文档: $ go doc text/template\n$ go doc html/template 练习 4.14: 创建一个web服务器,查询一次GitHub,然后生成BUG报告、里程碑和对应的用户信息。","breadcrumbs":"复合数据类型 » 文本和HTML模板 » 4.6. 文本和HTML模板","id":"58","title":"4.6. 文本和HTML模板"},"59":{"body":"函数可以让我们将一个语句序列打包为一个单元,然后可以从程序中其它地方多次调用。函数的机制可以让我们将一个大的工作分解为小的任务,这样的小任务可以让不同程序员在不同时间、不同地方独立完成。一个函数同时对用户隐藏了其实现细节。由于这些因素,对于任何编程语言来说,函数都是一个至关重要的部分。 我们已经见过许多函数了。现在,让我们多花一点时间来彻底地讨论函数特性。本章的运行示例是一个网络蜘蛛,也就是web搜索引擎中负责抓取网页部分的组件,它们根据抓取网页中的链接继续抓取链接指向的页面。一个网络蜘蛛的例子给我们足够的机会去探索递归函数、匿名函数、错误处理和函数其它的很多特性。","breadcrumbs":"函数 » 第5章 函数","id":"59","title":"第5章 函数"},"6":{"body":"最佳的帮助信息来自Go语言的官方网站,https://golang.org ,它提供了完善的参考文档,包括编程语言规范和标准库等诸多权威的帮助信息。同时也包含了如何编写更地道的Go程序的基本教程,还有各种各样的在线文本资源和视频资源,它们是本书最有价值的补充。Go语言的官方博客 https://blog.golang.org 会不定期发布一些Go语言最好的实践文章,包括当前语言的发展状态、未来的计划、会议报告和Go语言相关的各种会议的主题等信息(译注: http://talks.golang.org/ 包含了官方收录的各种报告的讲稿)。 在线访问的一个有价值的地方是可以从web页面运行Go语言的程序(而纸质书则没有这么便利了)。这个功能由来自 https://play.golang.org 的 Go Playground 提供,并且可以方便地嵌入到其他页面中,例如 https://golang.org 的主页,或 godoc 提供的文档页面中。 Playground可以简单的通过执行一个小程序来测试对语法、语义和对程序库的理解,类似其他很多语言提供的REPL即时运行的工具。同时它可以生成对应的url,非常适合共享Go语言代码片段,汇报bug或提供反馈意见等。 基于 Playground 构建的 Go Tour,https://tour.golang.org ,是一个系列的Go语言入门教程,它包含了诸多基本概念和结构相关的并可在线运行的互动小程序。 当然,Playground 和 Tour 也有一些限制,它们只能导入标准库,而且因为安全的原因对一些网络库做了限制。如果要在编译和运行时需要访问互联网,对于一些更复杂的实验,你可能需要在自己的电脑上构建并运行程序。幸运的是下载Go语言的过程很简单,从 https://golang.org 下载安装包应该不超过几分钟(译注:感谢伟大的长城,让大陆的Gopher们都学会了自己打洞的基本生活技能,下载时间可能会因为洞的大小等因素从几分钟到几天或更久),然后就可以在自己电脑上编写和运行Go程序了。 Go语言是一个开源项目,你可以在 https://golang.org/pkg 阅读标准库中任意函数和类型的实现代码,和下载安装包的代码完全一致。这样,你可以知道很多函数是如何工作的, 通过挖掘找出一些答案的细节,或者仅仅是出于欣赏专业级Go代码。","breadcrumbs":"前言 » 更多的信息","id":"6","title":"更多的信息"},"60":{"body":"函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。 func name(parameter-list) (result-list) { body\n} 形式参数列表描述了函数的参数名以及参数类型。这些参数作为局部变量,其值由参数调用者提供。返回值列表描述了函数返回值的变量名以及类型。如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。如果一个函数声明不包括返回值列表,那么函数体执行完毕后,不会返回任何值。在hypot函数中: func hypot(x, y float64) float64 { return math.Sqrt(x*x + y*y)\n}\nfmt.Println(hypot(3,4)) // \"5\" x和y是形参名,3和4是调用时的传入的实参,函数返回了一个float64类型的值。 返回值也可以像形式参数一样被命名。在这种情况下,每个返回值被声明成一个局部变量,并根据该返回值的类型,将其初始化为该类型的零值。 如果一个函数在声明时,包含返回值列表,该函数必须以 return语句结尾,除非函数明显无法运行到结尾处。例如函数在结尾时调用了panic异常或函数中存在无限循环。 正如hypot一样,如果一组形参或返回值有相同的类型,我们不必为每个形参都写出参数类型。下面2个声明是等价的: func f(i, j, k int, s, t string) { /* ... */ }\nfunc f(i int, j int, k int, s string, t string) { /* ... */ } 下面,我们给出4种方法声明拥有2个int型参数和1个int型返回值的函数.blank identifier(译者注:即下文的_符号)可以强调某个参数未被使用。 func add(x int, y int) int {return x + y}\nfunc sub(x, y int) (z int) { z = x - y; return}\nfunc first(x int, _ int) int { return x }\nfunc zero(int, int) int { return 0 } fmt.Printf(\"%T\\n\", add) // \"func(int, int) int\"\nfmt.Printf(\"%T\\n\", sub) // \"func(int, int) int\"\nfmt.Printf(\"%T\\n\", first) // \"func(int, int) int\"\nfmt.Printf(\"%T\\n\", zero) // \"func(int, int) int\" 函数的类型被称为函数的签名。如果两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型或签名。形参和返回值的变量名不影响函数签名,也不影响它们是否可以以省略参数类型的形式表示。 每一次函数调用都必须按照声明顺序为所有参数提供实参(参数值)。在函数调用时,Go语言没有默认参数值,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义。 在函数体中,函数的形参作为局部变量,被初始化为调用者提供的值。函数的形参和有名返回值作为函数最外层的局部变量,被存储在相同的词法块中。 实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针,slice(切片)、map、function、channel等类型,实参可能会由于函数的间接引用被修改。 你可能会偶尔遇到没有函数体的函数声明,这表示该函数不是以Go实现的。这样的声明定义了函数签名。 package math func Sin(x float64) float //implemented in assembly language","breadcrumbs":"函数 » 函数声明 » 5.1. 函数声明","id":"60","title":"5.1. 函数声明"},"61":{"body":"函数可以是递归的,这意味着函数可以直接或间接的调用自身。对许多问题而言,递归是一种强有力的技术,例如处理递归的数据结构。在4.4节,我们通过遍历二叉树来实现简单的插入排序,在本章节,我们再次使用它来处理HTML文件。 下文的示例代码使用了非标准包 golang.org/x/net/html ,解析HTML。golang.org/x/... 目录下存储了一些由Go团队设计、维护,对网络编程、国际化文件处理、移动平台、图像处理、加密解密、开发者工具提供支持的扩展包。未将这些扩展包加入到标准库原因有二,一是部分包仍在开发中,二是对大多数Go语言的开发者而言,扩展包提供的功能很少被使用。 例子中调用golang.org/x/net/html的部分api如下所示。html.Parse函数读入一组bytes解析后,返回html.Node类型的HTML页面树状结构根节点。HTML拥有很多类型的结点如text(文本)、comments(注释)类型,在下面的例子中,我们 只关注< name key='value' >形式的结点。 golang.org/x/net/html package html type Node struct { Type NodeType Data string Attr []Attribute FirstChild, NextSibling *Node\n} type NodeType int32 const ( ErrorNode NodeType = iota TextNode DocumentNode ElementNode CommentNode DoctypeNode\n) type Attribute struct { Key, Val string\n} func Parse(r io.Reader) (*Node, error) main函数解析HTML标准输入,通过递归函数visit获得links(链接),并打印出这些links: gopl.io/ch5/findlinks1 // Findlinks1 prints the links in an HTML document read from standard input.\npackage main import ( \"fmt\" \"os\" \"golang.org/x/net/html\"\n) func main() { doc, err := html.Parse(os.Stdin) if err != nil { fmt.Fprintf(os.Stderr, \"findlinks1: %v\\n\", err) os.Exit(1) } for _, link := range visit(nil, doc) { fmt.Println(link) }\n} visit函数遍历HTML的节点树,从每一个anchor元素的href属性获得link,将这些links存入字符串数组中,并返回这个字符串数组。 // visit appends to links each link found in n and returns the result.\nfunc visit(links []string, n *html.Node) []string { if n.Type == html.ElementNode && n.Data == \"a\" { for _, a := range n.Attr { if a.Key == \"href\" { links = append(links, a.Val) } } } for c := n.FirstChild; c != nil; c = c.NextSibling { links = visit(links, c) } return links\n} 为了遍历结点n的所有后代结点,每次遇到n的孩子结点时,visit递归的调用自身。这些孩子结点存放在FirstChild链表中。 让我们以Go的主页(golang.org)作为目标,运行findlinks。我们以fetch(1.5章)的输出作为findlinks的输入。下面的输出做了简化处理。 $ go build gopl.io/ch1/fetch\n$ go build gopl.io/ch5/findlinks1\n$ ./fetch https://golang.org | ./findlinks1\n#\n/doc/\n/pkg/\n/help/\n/blog/\nhttp://play.golang.org/\n//tour.golang.org/\nhttps://golang.org/dl/\n//blog.golang.org/\n/LICENSE\n/doc/tos.html\nhttp://www.google.com/intl/en/policies/privacy/ 注意在页面中出现的链接格式,在之后我们会介绍如何将这些链接,根据根路径( https://golang.org )生成可以直接访问的url。 在函数outline中,我们通过递归的方式遍历整个HTML结点树,并输出树的结构。在outline内部,每遇到一个HTML元素标签,就将其入栈,并输出。 gopl.io/ch5/outline func main() { doc, err := html.Parse(os.Stdin) if err != nil { fmt.Fprintf(os.Stderr, \"outline: %v\\n\", err) os.Exit(1) } outline(nil, doc)\n}\nfunc outline(stack []string, n *html.Node) { if n.Type == html.ElementNode { stack = append(stack, n.Data) // push tag fmt.Println(stack) } for c := n.FirstChild; c != nil; c = c.NextSibling { outline(stack, c) }\n} 有一点值得注意:outline有入栈操作,但没有相对应的出栈操作。当outline调用自身时,被调用者接收的是stack的拷贝。被调用者对stack的元素追加操作,修改的是stack的拷贝,其可能会修改slice底层的数组甚至是申请一块新的内存空间进行扩容;但这个过程并不会修改调用方的stack。因此当函数返回时,调用方的stack与其调用自身之前完全一致。 下面是 https://golang.org 页面的简要结构: $ go build gopl.io/ch5/outline\n$ ./fetch https://golang.org | ./outline\n[html]\n[html head]\n[html head meta]\n[html head title]\n[html head link]\n[html body]\n[html body div]\n[html body div]\n[html body div div]\n[html body div div form]\n[html body div div form div]\n[html body div div form div a]\n... 正如你在上面实验中所见,大部分HTML页面只需几层递归就能被处理,但仍然有些页面需要深层次的递归。 大部分编程语言使用固定大小的函数调用栈,常见的大小从64KB到2MB不等。固定大小栈会限制递归的深度,当你用递归处理大量数据时,需要避免栈溢出;除此之外,还会导致安全性问题。与此相反,Go语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题。 练习 5.1: 修改findlinks代码中遍历n.FirstChild链表的部分,将循环调用visit,改成递归调用。 练习 5.2: 编写函数,记录在HTML树中出现的同名元素的次数。 练习 5.3: 编写函数输出所有text结点的内容。注意不要访问