GO语言的前世今生
不能影响到你的编程思维方式的编程语言不值得学习和使用
-
Go是Google三位大师级人物Robert Griesemer、Rob Pike及Ken Thompson共同设计的一种静态类型、编译型编程语言。它于2009年11月正式开源,一经面世就凭借语法简单、原生支持并发、标准库强大、工具链丰富等优点吸引了大量开发者
-
当时的谷歌内部主要使用C++语言构建各种系统,但C++复杂性高,编译构建速度慢,在编写服务端程序时不便支持并发
-
将int类型在64位平台上的内部表示的字节数升为8字节,即64比特。
-
由于连续栈的应用,goroutine的默认栈大小从8kB减小为2kB;
-
2015年8月19日,Go 1.5版本发布。Go 1.5是Go语言历史上的一个具有里程碑意义的重要版本。因为从这个版本开始,Go实现了自举,即无须再依赖C编译器。然而Go编译器的性能比Go 1.4的C实现有了较大幅度的下降。
-
Go编译器和运行时全部使用Go重写,原先的C代码实现被彻底移除
-
GOMAXPROCS的初始默认值由1改为运行环境的CPU核数;
-
Go 1.11是Russ Cox在GopherCon 2017大会上发表题为“Toward Go 2”的演讲之后的第一个Go版本,它与Go 1.5版本一样也是具有里程碑意义的版本,因为它引入了新的Go包管理机制:Go module
追求简单,少即是多
简单是一种伟大的美德,但我们需要更艰苦地努力才能实现它,并需要经过一个教育的过程才能去欣赏和领会它。但糟糕的是:复杂的东西似乎更有市场。
-
我们眼前的是这样的Go语言:简洁、常规的语法(不需要解析符号表),它仅有25个关键字;内置垃圾收集,降低开发人员内存管理的心智负担;没有头文件;显式依赖(package);没有循环依赖(package);常量只是数字;首字母大小写决定可见性;任何类型都可以拥有方法(没有类);没有子类型继承(没有子类);没有算术转换;接口是隐式的(无须implements声明);方法就是函数;接口只是方法集合(没有数据);方法仅按名称匹配(不是按类型);没有构造函数或析构函数;n++和n--是语句,而不是表达式;没有++n和--n;赋值不是表达式;在赋值和函数调用中定义的求值顺序(无“序列点”概念);没有指针算术;内存总是初始化为零值;没有类型注解语法(如C++中的const、static等);没有模板/泛型;没有异常(exception);内置字符串、切片(slice)、map类型;内置数组边界检查;内置并发支持;
-
比如首字母大小写决定可见性,内存分配初始零值,内置以go关键字实现的并发支持等)。Go设计者推崇“最小方式”思维,即一件事情仅有一种方式或数量尽可能少的方式去完成,这大大减少了开发人员在选择路径方式及理解他人所选路径方式上的心智负担。
-
Go语言之父Rob Pike所说:“Go语言实际上是复杂的,但只是让大家感觉很简单
-
伟大的产品都把复杂性留给了自己,把简单可依赖留给了用户
-
“简单”选择的背后是Go语言自身实现层面的复杂性,而这种复杂性被Go语言的设计者“隐藏”起来了
-
Go后续演化的最大难点是什么?Go开发团队的一名核心成员回答道:“最大的难点是如何继续保持Go语言的简单。”
-
偏好组合,正交解耦
-
Go语言无类型体系(type hierarchy),类型之间是独立的,没有子类型的概念;每个类型都可以有自己的方法集合,类型定义与方法实现是正交独立的;接口(interface)与其实现之间隐式关联;包(package)之间是相对独立的,没有子包的概念。
-
Go语言为我们呈现了这样一幅图景:一座座没有关联的“孤岛”,但每个岛内又都很精彩。现在摆在面前的工作就是以最适当的方式在这些孤岛之间建立关联(耦合),形成一个整体。Go采用了组合的方式,也是唯一的方式。
-
interface是Go语言中真正的“魔法”,是Go语言的一个创新设计,它只是方法集合,且与实现者之间的关系是隐式的,它让程序各个部分之间的耦合降至最低,同时是连接程序各个部分的“纽带”
-
。隐式的interface实现会不经意间满足依赖抽象、里氏替换、接口隔离等设计原则,这在其他语言中是需要很刻意的设计谋划才能实现的,但在Go interface看来,一切却是自然而然的
-
组合原则的应用塑造了Go程序的骨架结构。类型嵌入为类型提供垂直扩展能力,interface是水平组合的关键,它好比程序肌体上的“关节”,给予连接“关节”的两个部分各自“自由活动”的能力,而整体上又实现了某种功能
原生并发,轻量高效
-
三位设计者认为C++标准委员会在思路上是短视的,因为硬件很可能在未来十年内发生重大变化,将语言与当时的硬件紧密耦合起来是十分不明智的,是没法给开发人员在编写大规模并发程序时带去太多帮助的。
-
CPU仅靠提高主频来改进性能的做法遇到了瓶颈。主频提高导致CPU的功耗和发热量剧增,反过来制约了CPU性能的进一步提高。依靠主频的提高已无法实现性能提升,人们开始把研究重点转向把多个执行内核放进一个处理器,让每个内核在较低的频率下工作来降低功耗同时提高性能。2007年处理器领域已开始进入一个全新的多核时代,处理器厂商的竞争焦点从主频转向了多核,多核设计也为摩尔定律带来新的生命力。与传统的单核CPU相比,多核CPU带来了更强的并行处理能力、更高的计算密度和更低的时钟频率,并大大减少了散热和功耗。Go的设计者敏锐地把握了CPU向多核方向发展的这一趋势,在决定不再使用C++而去创建一门新语言的时候,果断将面向多核、原生内置并发支持作为新语言的设计原则之一。
-
传统的操作系统调度器会将系统中的多个线程按照一定算法调度到物理CPU上运行。传统编程语言(如C、C++等)的并发实现实际上就是基于操作系统调度的,即程序负责创建线程(一般通过pthread等函数库调用实现),操作系统负责调度。这种传统支持并发的方式主要有两大不足:复杂和难于扩展。
-
虽然线程的代价比进程小了很多,但我们依然不能大量创建线程,因为不仅每个线程占用的资源不小,操作系统调度切换线程的代价也不小。
-
对于很多网络服务程序,由于不能大量创建线程,就要在少量线程里做网络的多路复用,即使用epoll/kqueue/IoCompletionPort这套机制。即便有了libevent、libev这样的第三方库的帮忙,写起这样的程序也是很不容易的,存在大量回调(callback),会给程序员带来不小的心智负担。
-
goroutine。goroutine占用的资源非常少,Go运行时默认为每个goroutine分配的栈空间仅2KB。goroutine调度的切换也不用陷入(trap)操作系统内核层完成,代价很低
-
一个Go程序对于操作系统来说只是一个用户层程序。操作系统的眼中只有线程,它甚至不知道goroutine的存在。goroutine的调度全靠Go自己完成,实现Go程序内goroutine之间公平地竞争CPU资源的任务就落到了Go运行时头上。而将这些goroutine按照一定算法放到CPU上执行的程序就称为goroutine调度器(goroutine scheduler)。
面向工程,“自带电池”
-
Go语言是一门简单的语言,简单意味着可读性好,容易理解,容易上手,容易修复错误,节省开发者时间,提升开发者间的沟通效率。但作为面向工程的编程语言,光有简单的设计哲学还不够,每个语言设计细节还都要经过“工程规模化”的考验和打磨,需要在细节上进行充分的思考和讨论。比如Rob Pike就曾谈到,Go当初之所以没有使用Python那样的代码缩进而是选择了与C语言相同的大括号来表示程序结构,是因为他们经过调查发现,虽然Python的缩进结构在构建小规模程序时的确很方便,但是当代码库变得更大的时候,缩进式的结构非常容易出错。从工程的安全性和可靠性角度考虑,Go团队最终选择了大括号代码块结构。
-
重新设计编译单元和目标文件格式,实现Go源码快速构建,将大工程的构建时间缩短到接近于动态语言的交互式解释的编译时间
-
如果源文件导入了它不使用的包,则程序将无法编译。这既可以充分保证Go程序的依赖树是精确的,也可以保证在构建程序时不会编译额外的代码,从而最大限度地缩短编译时间
-
去除包的循环依赖。循环依赖会在大规模的代码中引发问题,因为它们要求编译器同时处理更大的源文件集,这会减慢增量构建速度。
-
在处理依赖关系时,有时会通过允许一部分重复代码来避免引入较多依赖关系。比如:net包具有其自己的整数到十进制转换实现,以避免依赖于较大且依赖性较强的格式化io包。
-
包名不必是唯一的约定大大降低了开发人员给包起唯一名字的心智负担
-
故意不支持默认函数参数。因为在规模工程中,很多开发者利用默认函数参数机制向函数添加过多的参数以弥补函数API的设计缺陷,这会导致函数拥有太多的参数,降低清晰度和可读性。
-
首字母大小写定义标识符可见性,这是Go的一个创新。它让开发人员通过名称即可知晓其可见性,而无须回到标识符定义的位置查找并确定其可见性,这提升了开发人员阅读代码的效率
-
提升了语言的健壮性,比如去除指针算术,去除隐式类型转换等。
-
内置垃圾收集。这对于大型工程项目来说,大大降低了程序员在内存管理方面的负担
-
内置并发支持,为网络软件带来了简单性
-
Go被称为“自带电池”(battery-included)的编程语言。“自带电池”原指购买了电子设备后,在包装盒中包含了电池,电子设备可以开箱即用,无须再单独购买电池
-
如果说一门编程语言“自带电池”,则说明这门语言标准库功能丰富,多数功能无须依赖第三方包或库,Go语言恰是这类编程语言
-
Go语言目前在GUI、机器学习(Machine Learning)等开发领域占有的份额较低,这很可能与Go标准库没有内置这类包有关
-
(3)工具链
-
而Go语言提供了十分全面、贴心的编程语言官方工具链,涵盖了编译、编辑、依赖获取、调试、测试、文档、性能剖析等的方方面面
-
值得重点提及的是gofmt统一了Go语言的编码风格,在其他语言开发者还在为代码风格争论不休的时候,Go开发者可以更加专注于领域业务
-
简单是Go语言贯穿语言设计和应用的主旨设计哲学
-
“少即是多
-
“高内聚、低耦合”是软件开发领域亘古不变的管理复杂性的准则。Go在语言设计层面也将这一准则发挥到极致
Go语言原生编程思维
-
编程语言影响编程思维,或者说每种编程语言都有属于自己的原生编程思维
-
以一门编程语言为中心的,以解决工程问题为目标的编程语言用法、辅助库、工具的固定使用方法称为该门编程语言的原生编程思维
Go语言典型项目结构**
-
由于Go语言项目自身在1.4版本中去掉了pkg这一层目录,因此有一些项目直接将包平铺到项目根路径下,但笔者认为对于一些规模稍大的项目,过多的包会让项目顶层目录不再简洁,显得很拥挤,因此个人建议对于复杂的Go项目保留pkg目录
-
vendor目录(可选):vendor是Go 1.5版本引入的用于在项目本地缓存特定版本依赖包的机制。在引入Go module机制之前,基于vendor可以实现可重现的构建(reproducible build),保证基于同一源码构建出的可执行程序是等价的。Go module本身就可以实现可重现的构建而不需要vendor,当然Go module机制也保留了vendor目录(通过go mod vendor可以生成vendor下的依赖包;通过go build -mod=vendor可以实现基于vendor的构建)
-
internal目录下的ilib1、ilib2可以被以GoLibProj目录为根目录的其他目录下的代码(比如lib.go、lib1/lib1.go等)所导入和使用,但是却不可以为GoLibProj目录以外的代码所使用,从而实现选择性地暴露API包
gofmt:Go语言在解决规模化问题上的最佳实践
-
gofmt的代码风格不是某个人的最爱,而是所有人的最爱。——Rob Pike
-
Go语言设计的目标之一是解决大型软件系统的大规模协作开发问题
-
Go核心团队将这类问题归结为一个词——规模化(scale)
-
gofmt最大的特点是没有提供任何关于代码风格设置的命令行选项和参数,这样Go开发人员就无法通过设置命令行特定选项来定制自己喜好的风格
使用Go命名惯例对标识符进行命名
-
Go及其标准库的实现是Go命名惯例形成的源头,因此如果要寻找良好命名的示范,Go标准库是一个不错的地方
-
要想做好Go标识符的命名(包括对包的命名),至少要遵循两个原则:简单且一致;利用上下文辅助命名。
简单且一致
-
- 包对于Go中的包(package),一般建议以小写形式的单个单词命名
-
Go语言建议,包名应尽量与包导入路径(import path)的最后一个路径分段保持一致
-
:保持变量声明与使用之间的距离越近越好,或者在第一次使用变量之前声明该变量
局部变量的声明形式
-
- 对于延迟初始化的局部变量声明,采用带有var关键字的声明形式
-
-
对于声明且显式初始化的局部变量,建议使用短变量声明形式
-
第四十三章 9.2 有类型常量带来的烦恼
-
Go的设计者认为,隐式转换带来的便利性不足以抵消其带来的诸多问题[1]。要解决上面的编译错误,必须进行显式类型转换:type myInt intfunc main() { var a int = 5 var b myInt = 6 fmt.Println(a + int(b)) // 输出:11}
第四十七章 11.1 Go类型的零值
-
当通过声明或调用new为变量分配存储空间,或者通过复合文字字面量或调用make创建新值,且不提供显式初始化时,Go会为变量或值提供默认值。
零值可用
- 第一个例子是关于切片的:var zeroSlice []intzeroSlice = append(zeroSlice, 1)zeroSlice = append(zeroSlice, 2)zeroSlice = append(zeroSlice, 3)fmt.Println(zeroSlice) // 输出:[1 2 3]我们声明了一个[]int类型的切片zeroSlice,但并没有对其进行显式初始化,这样zeroSlice这个变量就被Go编译器置为零值nil。按传统的思维,对于值为nil的变量,我们要先为其赋上合理的值后才能使用。但由于Go中的切片类型具备零值可用的特性,我们可以直接对其进行append操作,而不会出现引用nil的错误。
第五十二章 12.3 map复合字面值
- 12.3 map复合字面值 和结构体、数组/切片相比,map类型变量使用复合字面值作为初值构造器就显得自然许多,因为map类型具有原生的key:value构造形式: // $GOROOT/src/time/format.go var unitMap = map[string]int64{ "ns": int64(Nanosecond), "us": int64(Microsecond), "µs": int64(Microsecond), // U+00B5 = 微符号 "μs": int64(Microsecond), // U+03BC = 希腊字母μ "ms": int64(Millisecond), ... }
了解切片实现原理并高效使用
-
每当你花费大量时间使用某种特定工具时,深入了解它并了解如何高效地使用它是很值得的。
-
切片之于数组就像是文件描述符之于文件。在Go语言中,数组更多是“退居幕后”,承担的是底层存储空间的角色;而切片则走向“前台”,为底层的存储(数组)打开了一个访问的“窗口”(见图13-1)。 因此,我们可以称切片是数组的“描述符”
-
但从append的原理中我们也能看到重新分配底层数组并复制元素的操作代价还是挺大的,尤其是当元素较多的情况下。那么如何减少或避免为过多内存分配和复制付出的代价呢?一种有效的方法是根据切片的使用场景对切片的容量规模进行预估,并在创建新切片时将预估出的切片容量数据以cap参数的形式传递给内置函数make:s := make([]T, len, cap)
-
如果可以预估出切片底层数组需要承载的元素数量,强烈建议在创建切片时带上cap参数
-
append在切片上的运用让切片类型部分支持了“零值可用”的理念,并且append对切片的动态扩容将Gopher从手工管理底层存储的工作中解放了出来。
第五十八章 14.1 什么是map
-
map类型不支持“零值可用”,未显式赋初值的map类型变量的零值为nil。对处于零值状态的map变量进行操作将会导致运行时panic
-
和切片一样,map也是引用类型,将map类型变量作为函数参数传入不会有很大的性能损耗
-
且在函数内部对map变量的修改在函数外部也是可见的
第五十九章 14.2 map的基本操作
-
map类型更多用在查找和数据读取场合。所谓查找就是判断某个key是否存在于某个map中。我们可以使用“comma ok”惯用法来进行查找:_, ok := m["key"]if !ok { // "key"不在map中}
-
Go语言的一个最佳实践是总是使用“comma ok”惯用法读取map中的值
-
借助内置函数delete从map中删除数据
-
即便要删除的数据在map中不存在,delete也不会导致panic。
-
因此千万不要依赖遍历map所得到的元素次序
-
如果你需要一个稳定的遍历次序,那么一个比较通用的做法是使用另一种数据结构来按需要的次序保存key,比如切片
第六十一章 14.4 尽量使用cap参数创建map
-
如果初始创建map时没有创建足够多可以应付map使用场景的bucket,那么随着插入map元素数量的增多,map会频繁扩容,而这一过程将降低map的访问性能。因此,如果可能的话,我们最好对map使用规模做出粗略的估算,并使用cap参数对map实例进行初始化
-
我们在日常使用map的场合要把握住下面几个要点:不要依赖map的元素遍历顺序;map不是线程安全的,不支持并发写;不要尝试获取map中元素(value)的地址;尽量使用cap参数创建map,以提升map平均访问性能,减少频繁扩容带来的不必要损耗。
字符串相关的高效转换
-
也就是说string和[]rune、[]byte可以双向转换
-
无论是string转slice还是slice转string,转换都是要付出代价的,这些代价的根源在于string是不可变的,运行时要为转换后的类型分配新内存
-
Go语言还在标准库中提供了strings和strconv包
第六十七章 第16条 理解Go语言的包导入
-
Go要求每个源文件在开头处显式地列出所有依赖的包导入,这样Go编译器不必读取和处理整个文件就可以确定其依赖的包列表。Go要求包之间不能存在循环依赖,这样一个包的依赖关系便形成了一张有向无环图。由于无环,包可以被单独编译,也可以并行编译。已编译的Go包对应的目标文件(file_name.o或package_name.a)中不仅记录了该包本身的导出符号信息,还记录了其所依赖包的导出符号信息。这样,Go编译器在编译某包P时,针对P依赖的每个包导入(比如导入包Q),只需读取一个目标文件即可(比如:Q包编译成的目标文件中已经包含Q包的依赖包的导出信息),而无须再读取其他文件中的信息。
第六十八章 16.1 Go程序构建过程
-
go build -a可以让编译器将Go源文件(比如例子中的main.go)的所有直接和间接的依赖包(包括标准库)都重新编译一遍,并将最新的.a作为链接器的输入。因此,采用-a选项进行构建的时间较长
第六十九章 16.2 究竟是路径名还是包名
-
未来的Go版本将只有module-aware模式,即只在module缓存的目录下搜索包的源码
-
不过Go语言有一个惯用法,那就是包导入路径的最后一段目录名最好与包名一致
-
包导入路径上的pkg1表示的是一个目录名,而main函数体中的pkg1则是包名。
-
当包名与包导入路径中的最后一个目录名不同时,最好用下面的语法将包名显式放入包导入语句。以上面的app2为例:// app2/main.gopackage mainimport ( mypkg2 "github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg2")func main() { mypkg2.Func1()}显然,这种惯用法让代码可读性更好。
第七十章 16.3 包名冲突问题
-
Go编译器在编译过程中必然要使用的是编译单元(一个包)所依赖的包的源码;Go源码文件头部的包导入语句中import后面的部分是一个路径,路径的最后一个分段是目录名,而不是包名;Go编译器的包源码搜索路径由基本搜索路径和包导入路径组成,两者结合在一起后,编译器便可确定一个包的所有依赖包的源码路径的集合,这个集合构成了Go编译器的源码搜索路径空间;同一源码文件的依赖包在同一源码搜索路径空间下的包名冲突问题可以由显式指定包名的方式解决。
第八十章 第19条 了解Go语言控制语句惯用法及使用注意事项
-
坚持“一件事情仅有一种做法”的设计理念,仅保留for这一种循环控制语句,去掉while、do-while语法;
-
switch的case语句执行完毕后,默认不会像C语言那样继续执行下一个case中的语句
-
所谓“快乐路径”即成功逻辑的代码执行路径,这个原则要求:当出现错误时,快速返回;成功逻辑不要嵌入if-else语句中;“快乐路径”的执行逻辑在代码布局上始终靠左,这样读者可以一眼看到该函数的正常逻辑流程;“快乐路径”的返回值一般在函数最后一行,就像上面伪代码段1中的那样。
第八十二章 19.2 for range的避“坑”指南
-
当channel作为range表达式类型时,for range最终以阻塞读的方式阻塞在channel表达式上,即便是带缓冲的channel亦是如此:当channel中无数据时,for range也会阻塞在channel上,直到channel关闭
第八十三章 19.3 break跳到哪里去了
-
// chapter3/sources/control_structure_idiom_7.gofunc main() { exit := make(chan interface{}) go func() { loop: for { select { case <-time.After(time.Second): fmt.Println("tick") case <-exit: fmt.Println("exiting...") break loop } } fmt.Println("exit!") }() time.Sleep(3 * time.Second) exit <- struct{}{} // 等待子goroutine退出 time.Sleep(3 * time.Second)}在改进后的例子中,我们定义了一个label——loop,该label附在for循环的外面,指代for循环的执行。代码执行到“break loop”时,程序将停止label loop所指代的for循环的执行
-
带label的continue和break提升了Go语言的表达能力,可以让程序轻松拥有从深层循环中终止外层循环或跳转到外层循环继续执行的能力,使得Gopher无须为类似的逻辑设计复杂的程序结构或使用goto语句
-
outerLoop: for i := 0; i < n; i++ { // ... for j := 0; j < m; j++ { // 当不满足某些条件时,直接终止最外层循环的执行 break outerLoop // 当满足某些条件时,直接跳出内层循环,回到外层循环继续执行 continue outerLoop } }
19.4 尽量用case表达式列表替代fallthrough**
-
在C语言中,case语句默认都是“fall through”的,于是就出现了每个case必然有break附在结尾的场景。从这一现象来看,多数情况下,case语句是不需要“fall through”的,于是Go语言在设计之初就“纠正”了这一问题,选择switch-case语句默认是不“fall through”的,需要fall through的时候,可以使用关键字fallthrough显式实现。
-
使用if语句时遵循“快乐路径”原则;小心for range的循环变量重用,明确真实参与循环的是range表达式的副本;明确break和continue执行后的真实“目的地”;使用fallthrough关键字前,考虑能否用更简洁、清晰的case表达式列表替代。
第八十七章 20.1 认识init函数
-
Go语言中有两个特殊的函数:一个是main包中的main函数,它是所有Go可执行程序的入口函数;另一个就是包的init函数。
-
如果一个包定义了init函数,Go运行时会负责在该包初始化时调用它的init函数。在Go程序中我们不能显式调用init,否则会在编译期间报错
-
一个Go包可以拥有多个init函数,每个组成Go包的Go源文件中可以定义多个init函数
-
不要依赖init函数的执行次序。
程序初始化顺序
-
Go运行时遵循“深度优先”原则查看到pkg1依赖pkg2,于是Go运行时去初始化pkg2
第八十九章 20.3 使用init函数检查包级变量的初始状态
第九十章 第21条 让自己习惯于函数是“一等公民”
-
函数在Go语言中属于“一等公民”。众所周知,并不是在所有编程语言中函数都是“一等公民”
第九十一章 21.1 什么是“一等公民”
-
如果一门编程语言对某种语言元素的创建和使用没有限制,我们可以像对待值(value)一样对待这种语法元素,那么我们就称这种语法元素是这门编程语言的“一等公民”。拥有“一等公民”待遇的语法元素可以存储在变量中,可以作为参数传递给函数,可以在函数内部创建并可以作为返回值从函数返回。在动态类型语言中,语言运行时还支持对“一等公民”类型的检查。
-
函数还可以被放入数组、切片或map等结构中,可以像其他类型变量一样被赋值给interface{},甚至我们可以建立元素为函数的channel
第九十二章 21.2 函数作为“一等公民”的特殊运用
-
最为典型的示例就是http.HandlerFunc这个类型,我们来看一下例子:
-
// chapter4/sources/function_as_first_class_citizen_3.gotype BinaryAdder interface { Add(int, int) int}type MyAdderFunc func(int, int) intfunc (f MyAdderFunc) Add(x, y int) int { return f(x, y)}func MyAdd(x, y int) int { return x + y}func main() { var i BinaryAdder = MyAdderFunc(MyAdd) fmt.Println(i.Add(5, 6))}
-
闭包是在函数内部定义的匿名函数,并且允许该匿名函数访问定义它的外部函数的作用域。
第九十四章 22.1 defer的运作机制
-
在Go中,只有在函数和方法内部才能使用defer;
-
无论是执行到函数体尾部返回,还是在某个错误处理分支显式调用return返回,抑或出现panic,已经存储到deferred函数栈中的函数都会被调度执行。因此,deferred函数是一个在任何情况下都可以为函数进行收尾工作的好场合
第九十五章 22.2 defer的常见用法
-
22.2 defer的常见用法除了释放资源这个最基本、最常见的用法之外,defer的运作机制决定了它还可以在其他一些场合发挥作用,这些用法在Go标准库中均有体现。1. 拦截panic
-
- 修改函数的具名返回值
-
- 输出调试信息deferred函数被注册及调度执行的时间点使得它十分适合用来输出一些调试信息
-
更为典型的莫过于在出入函数时打印留痕日志(一般在调试日志级别下),这里摘录Go官方参考文档中的一个实现:func trace(s string) string { fmt.Println("entering:", s) return s}func un(s string) { fmt.Println("leaving:", s)}func a() { defer un(trace("a")) fmt.Println("in a")}
第九十六章 22.3 关于defer的几个关键问题
-
- 明确哪些函数可以作为deferred函数对于自定义的函数或方法,defer可以给予无条件的支持,但是对于有返回值的自定义函数或方法,返回值会在deferred函数被调度执行的时候被自动丢弃。
-
Go语言内置函数的完整列表:append cap close complex copy delete imag lenmake new panic print println real recover
-
append、cap、len、make、new等内置函数是不可以直接作为deferred函数的,而close、copy、delete、print、recover等可以
-
对于那些不能直接作为deferred函数的内置函数,我们可以使用一个包裹它的匿名函数来间接满足要求。以append为例:defer func() { _ = append(sl, 11)}()
-
在多数情况下,我们的程序对性能并不太敏感,笔者建议尽量使用defer。defer让资源释放变得优雅且不易出错,简化了函数实现逻辑,提高了代码可读性,让函数实现变得更加健壮。
第九十七章 第23条 理解方法的本质以选择正确的receiver类型
-
和函数相比,Go语言中的方法在声明形式上仅仅多了一个参数,Go称之为receiver参数。receiver参数是方法与类型之间的纽带。
-
Go方法具有如下特点。1)方法名的首字母是否大写决定了该方法是不是导出方法。2)方法定义要与类型定义放在同一个包内。由此我们可以推出:不能为原生类型(如int、float64、map等)添加方法,只能为自定义类型定义方法(示例代码如下)
-
)receiver参数的基类型本身不能是指针类型或接口类型,下面的示例展示了这点:type MyInt *intfunc (r MyInt) String() string { // 编译器错误:invalid receiver type MyInt (MyInt is a pointer type) return fmt.Sprintf("%d", *(*int)(r))}
第九十八章 23.1 方法的本质
-
。我们可以为任何非内置原生类型定义方法
-
C++的对象在调用方法时,编译器会自动传入指向对象自身的this指针作为方法的第一个参数。而对于Go来说,receiver其实也是同样道理,我们将receiver作为第一个参数传入方法的参数列表。
-
Go方法的本质:一个以方法所绑定类型实例为第一个参数的普通函数
第一百零章 23.3 基于对Go方法本质的理解巧解难题
-
Go方法的本质:一个以方法所绑定类型实例为第一个参数的普通函数。Go语法糖使得我们在通过类型实例调用类型方法时无须考虑实例类型与receiver参数类型是否一致,编译器会为我们做自动转换。在选择receiver参数类型时要看是否要对类型实例进行修改。如有修改需求,则选择*T;如无修改需求,T类型receiver传值的性能损耗也是考量因素之一。
第一百四十三章 34.1 无缓冲channel
-
“不要通过共享内存来通信,而应该通过通信来共享内存
第一百四十七章 第35条 了解sync包的正确用法
-
的sync包提供了针对传统基于共享内存并发模型的基本同步原语,包括互斥锁(sync.Mutex)、读写锁(sync.RWMutex)、条件变量(sync.Cond)等
第一百四十八章 sync包还是channel
不要通过共享内存来通信,而应该通过通信来共享内存
-
Go语言提倡“不要通过共享内存来通信,而应该通过通信来共享内存”。正如在前文阐述的那样,建议大家优先使用CSP并发模型进行并发程序设计。但是在下面一些场景下,我们依然需要sync包提供的低级同步原语。
第一百五十章 35.3 互斥锁还是读写锁
-
sync包提供了两种用于临界区同步的原语:互斥锁(Mutex)和读写锁(RWMutex)
-
读写锁适合应用在具有一定并发量且读多写少的场合
atomic包与原子操作
-
它就好比一个事务,要么不执行,一旦执行就一次性全部执行完毕,不可分割。正因如此,原子操作可用于共享数据的并发同步。
-
原子操作由底层硬件直接提供支持,是一种硬件实现的指令级“事务”,因此相比操作系统层面和Go运行时层面提供的同步技术而言,它更为原始
-
利用原子操作的无锁并发读的性能随着并发量增大有持续提升的趋势,并且性能约为读锁的200倍。
来自微信读书