本文是关于美团tutor给出的学习资料 — 官方文档”Effective Go”的笔记
参考资料:
Formatting
使用命令gofmt -w code.go
或者go fmt
,可以对代码进行格式化,使代码易读、易维护,不用再纠结多敲几个空格让代码更加美观 — fmt将会为我们做这些事情
before:
1 | type T struct { |
after:
1 | type T struct { |
当然还有一些细节需要用户注意:
- 缩进 — tab键
- 行长度 — 没有限制
Commentary
Go中的注释同C语言中一致,包括行注释以及块注释,形式分别为//
以及/* */
每一个package source代码,在起始部分,都应该有属于自己的包注释。但是对于包含多个源代码文件的包,只需要其中一个文件中含有包注释即可。godoc
将会收集包注释并且进行整合。
如果包很简单,包注释可以是多个行注释。但是不管如何,都应该遵循书面英语的模式:句子首字母大写,用句号分割。
在包内,任何声明语句前面的那条注释,都是针对声明语句的doc comment
。每一个从外部import的名称都应该有doc comment
。
对于声明出来的函数,doc comment
在注释第一个单词是函数名称的时候,有最好的效果,如下面所示
1 | // Compile parses a regular expression and returns, if successful, |
若遵循上面的规范,那么就可以使用命令go doc -all regexp | grep -i parse
,给定函数名称,返回自己对函数的注释,便于回忆以及快速抓取相关内容。
当然也可以按组来声明变量,如下面所示:
1 | // Error codes returned by failures to parse an expression. |
Names
Package names
一般包名都是小写的单词,而不使用下划线或者mixedCaps
不需要担心包名重复,因为它只是默认的导入名称,即使出现了重复,也可以在局部选择另外一个名称使用。
在导入包之后,需要通过包名来访问其中的内容,如在包bufio
中的缓存读取类型为Reader
,那么可以通过bufio.Reader
来进行访问;同样的,生成一个ring.Ring
的instance
,可以直接使用ring.New
,这样使用的话,不会与自带的New
冲突。灵活使用包结构,可以没有拘束地为变量起名。
Getters
在Go中,不同于Java,提供了自动的getter 以及 setter,用户需要主动提供这些方法。官方文档建议,对于getter方法,比如有一个字段的名称为owner
,那么getter的方法命应该是Owner
;而对于setter方法,可以命名为SetOwner
,如果真的有必要存在的话
1 | owner := obj.Owner() |
Interface names
通常,包含一个方法的接口,只需要在方法名后加后缀er
,构成一个名次即可,如: Reader
, Writer
, Formatter
等
MixedCaps
在Go中,鼓励使用MixedCaps
或者mixedCaps
,而不鼓励使用下划线
Semicolons
在Go中不存在分号,这是因为词法分析器根据简单的规则将会自动插入分号。
对于出现在换行符之前的符号,如标识符(如int, float32等)、字面量(常数或者字符串)、以下记号之一的:
1 | break continue fallthrough return ++ -- ) } |
go将会自动在其后插入分号
同时,右括号前的分号是可以被忽略的,因此下面的语句仍然是允许的
1 | go func() { for { dst <- <-src } }() |
控制语句的左括号必须与控制语句同行,否则词法分析器将会把分号插入到控制语句与左括号之间,引发错误,下面给出一个错误示例
1 | if i < f() // wrong! |
Control structures
if
if 以及switch都支持一条初始化语句,便于创建局部变量
Redeclaration and reassignment
:=
支持至少新创建一个变量的赋值
For
1 | // Like a C for |
当遍历数组、切片、字符串、map或者通道的时候,range关键字可以更好地管理循环
1 | for key, value := range oldMap { |
如果其中某个字段不想使用,那么可以用blank identifier
— _
来替代
在遍历string的时候,go还将会自动将其转换为rune,存储到value字段中
Switch
go中的switch比起C语言中的更加灵活:除了支持常量以及整数之外,它将会从上到下遍历所有的cases,直到条件为true为止。
同时,也是可以将一系列同级的记号通过逗号分隔,写在同一个case中的,如
1 | func shouldEscape(c byte) bool { |
该关键字还可以用来获得一个接口变量的动态类型
1 | var t interface{} |
Functions
Multiple return values
依赖于这一特性,可以直接从函数的返回值error
来判断函数是否正确执行。
对于返回值,可以对其命名。在进入函数的时候,这些带有名字的返回值将会被根据其类型初始化为zero value
,待函数内部执行完毕,直接return
即可
1 | func ReadFull(r Reader, buf []byte) (n int, err error) { |
Defer
典型的应用场景(canonical examples)为释放互斥锁或者关闭文件
1 | // Contents returns the file's contents as a string. |
这样做,可以保证我们不会在使用完毕之后忘记释放或者关闭;将关闭的语句放在打开的语句周围,比起在函数尾部关闭更加清晰。
defer的执行将会按照LIFO的顺序执行。
如果defer的函数中包含有参数,并且参数是需要执行的代码,那么在执行到defer的时候,将会首先执行参数中的代码,之后推迟被defer的函数的执行
1 | func trace(s string) string { |
对应输出
1 | entering: b |
Data
Allocation with new
new方法是自带的分配内存的方法,但是它只是将创建的变量赋值为zero value
而不初始化它们。
对于某些数据,初始化构造器是必要的,如下
1 | func NewFile(fd int, name string) *File { |
不同于C中的“函数中不可以返回局部变量”,go中是可以直接返回的,对应变量的生存周期在函数返回之后仍然生效,因此上述代码可以改进最后两行为:
1 | return &File{fd, name, nil, 0} |
初始化构造器对于数组、切片以及map都是有效的
1 | a := [...]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"} |
Allocation with make
不同于new(T)
,make的形式为make(T, args)
,它仅负责初始化切片、通道以及map;返回的是初始化过的变量而不是zeroed value
;返回的类型为T
,而不是*T
切片,实际上就是由指向数组内部的指针 + len + cap三个信息组成的数据结构。在被初始化之前,其值为nil
var p *[]int = new([]int)
返回的是一个类型为*[]int
的空指针,其值为nil
Arrays
Go中的数组相对于C有三个特点:
- 数组都是值。数组之间的赋值,将会赋值整个数组中的元素进行赋值。
- 将数组作为参数传入函数后,它将会收到一份数组的拷贝,而不是指向数组的指针。
- 数组的大小是其类型的一部分。类型
[10]int
与类型[20]int
是不同的。
值传递的代价很昂贵,高效的程序尽量使用指针传递数组;但是更理想的情况下,将会使用切片完成同样的操作。
Slices
切片保存的是对数组的引用,切片之间的赋值,不会影响指向的数组。
将切片作为参数传给函数,函数内部的修改对于外部数组是可见的。
可以将切片认为是一个可变长数组vector,append
方法将会主动关心切片的增长
1 | func Append(slice, data []byte) []byte { |
Two-dimensional slices
声明二维数组或切片的方法
1 | type Transform [3][3]float64 // A 3x3 array, really an array of arrays. |
第二维是不必要都一致的
1 | text := LinesOfText{ |
Map
如果使用map中不存在的key抓取元素,那么将会返回value字段的zero value
;在某些时候可能某些原本存在的key对应的value就是zero value
,此时可以通过第二个返回值来判断抓取是否成功
1 | var seconds int |
可以使用关键字delete
来删除map中的键值对
1 | delete(timeZone, "PDT") // Now on Standard Time |