EffectiveGo学习笔记
格式化
在go中,为了避免各种格式化问题的争论而浪费时间,我可以用gofmt将go程序进行统一的格式化,使得所有人遵循标准风格。
使用命令:
1 | gofmt -w program.go |
该命令会将源代码格式化后再去覆盖原来的内容。
All GO code in the standard packages has been formatted with gofmt
注释
Go语言支持块注释**/* */和行注释/ /**。在golang中,因为godoc的提供,我们可以通过两种方式来查看文档。godoc既是一个程序,也是一个web服务器。
考虑这样一段代码:
1 | /* |
go doc的使用可以接收包名作为参数,也可以不输入任何参数,那么显示觉得就是当前目录下的文档。
1 | go doc |
当然也可以打开web服务器去访问:
1 | godoc -http=:6060 |
打开之后是官网的一个拷贝,但包的文档http://127.0.0.1:6060/pkg/会更丰富,因为它是基于电脑上GOROOT和GOPATH路径下的所有包生成的文档。
命名
命名在任何语言中都是非常重要的
包名
1 | import "bytes" |
一般来说,包名应该以小写的单个单词为命名;另一个约定俗成的是包名应为其源码目录的基本名称。
由于包名作为一个访问器,使得导出名称可以避免冲突。比如bufio.Reader和io.Reader就不会发生冲突。
接口名
按照约定,只包含一个方法的接口应该以该方法的名称加上-er后缀来命名,如Reader、Writer。
分号
和C一天,go的正式语法使用分号来结束语句,但这些分号不在源码中出现,而是此法解析器会在每行最后一个标记为标识符时,在后面加上分号。
另外,分号在闭合的大括号之前可以直接省略,所以尽量不要将大括号换行,以下是错误做法:
1 | if i < f() |
如果一行内写多个语句,也应该用分号隔开。
控制结构
Golang有几种控制结构,for,if,switch和select,没有圆括号,而且主体必须使用大括号。
if
一个简单的if语句:
1 | if x > 0 { |
另外,if可以接收初始化语句,设置局部变量:
1 | if err := file.Chmod(0664); err != nil { |
重新声明与再次赋值
比如这样的例程:
1 | f, err := os.Open(name) |
在满足下列条件时,已被声明的变量可以出现:=声明中:
- 本次声明与已声明的v处于同一作用域;(若v在外层作用域已经声明过,则此次声明会创建新的变量)
- 在初始化中与其类型相应的值才能赋予v,并且此次声明中至少另有一个变量是新声明的
for
golang的for循环有三种:
1 | for init; condition; post { } |
如果要遍历数组、切片、字符串或者映射,抑或是从channel中读取信息,可以使用range子句:
1 | for key, value := range oldMap { |
switch
golang的switch语句,其表达式无需为常量或整数。
1 | func unhex(c byte) byte { |
这样就可以将一系列的if-else转为switch-case。
另外,switch的case可以使用逗号分隔来列举相同的处理条件:
1 | func shouldEscape(c byte) bool { |
跟C一样,我们往往会希望用break打破循环,在golang中,如果我们希望打破外层的循环,可以给break增加一个标签:
1 | Loop: |
命名的结果参数
Go函数的返回可以给定一个名字,在命名后,一旦函数开始执行,它们就会被初始化为与其类型响应的零值;若该函数执行了一条不带参数的return语句,该结果形参的当前值就会被返回。
1 | func ReadFull(r Reader, buf []byte) (n int, err error) { |
defer
Go的defer语句可以预设一个函数调用,让它在执行defer的函数返回之前立即执行,这可以用来执行一些释放资源的函数:
1 | // Contents returns the file's contents as a string. |
被defer的函数调用按照LIFO的顺序执行,比如这样的代码:
1 | for i := 0; i < 5; i++ { |
data
golang提供了两种分配方法,即内建函数make和new,它们做的事情不同,但可能引起混淆。
new分配
这个内建函数会分配内存,并且将内存置为0,但不会初始化内存。在分配完已置为0的内存之后,会返回它的地址,比如new(T)会返回*T,即一个指向新分配的,类型为T的零值。
因此使用者只需要用new就可以创建一个新的对象,例如:零值bytes.Buffer就是已准备就绪的缓冲区,而零值的sync.Mutex就是已解锁的互斥锁。
另外这种零值属性是具有传递性的,考虑这样的structure:
1 | type SyncedBuffer struct { |
那么在后续的声明时,对于SyncedBuffer类型,只要声明分配好内存就可以直接使用:
1 | p := new(SyncedBuffer) // type *SyncedBuffer |
构造函数与复合字面量
在很多情况下,我们声明一个对象不单单是要零值,还希望初始化成员,但总不能逐个赋值这么丑陋的做法嘛:
1 | func NewFile(fd int, name string) *File { |
而golang的做法是这样的:
1 | func NewFile(fd int, name string) *File { |
与C不同的是,golang返回一个局部变量地址是有效的,因此可以直接合并上面的语句。当然也可以以key:value的方式标出长远,如上。
make分配
内建函数make(T, args) 的目的不同于
new(T)。它只用于创建切片、映射和信道,并返回类型为
T(而非 *T
)的一个已初始化 (而非置零)的值。例如:
1 | make([]int, 10, 100) //分配一个100个int的数组空间,接着创建一个长度为10,容量为100并且指向该数组前10个元素的切片结构 |
若要返回指针,请用new分配内存
slice
切片是对数组的封装,保存了对底层数组的引用。若某个函数将一个切片作为参数传入时,函数对切片的修改对调用者而言是可见的。
1 | func (file *File) Read(buf []byte) (n int, err error) |
若要从更大的缓冲区 b 中读取前 32 个字节,只需对其进行切片即可。
1 | n, err := f.Read(buf[0:32]) |
map
map的key可以是任何相等性操作符支持的类型, 如整数、浮点数、复数、字符串、指针、接口,结构以及数组,但不能是切片。
若试图通过map中不存在的键来取值,就会返回与该映射中项的类型对应的零值。
但这种设计我们不知道到底是不是因为不存在该项而得到零值,因此可以使用这种做法来检查:
1 | var seconds int |
初始化
常量
Golang中的常量在编译时创建,而且只能是数字,字符,字符串或者布尔量。
init函数
init函数会在每个包完成初始化后自动执行,并且执行优先级会比main函数高,一般用来:
- 对变量进行初始化;
- 检查或者修复程序的状态;
- 注册;
- 进行一次计算;
接口
golang的接口为指定对象的行为提供了一种途径,如果某个类型可以完成这个,那么它就可以被用在这里。通过实现 String 方法,我们可以自定义打印函数,而通过 Write 方法,Fprintf 则能对任何对象产生输出。
例如一个实现了 sort.Interface 接口的集合就可通过 sort 包中的例程进行排序。该接口包括 Len()、Less(i, j int) bool 以及 Swap(i, j int)。
1 | type Sequence []int |
接口转换和类型断言
类型选择是类型转换的一种形式:它接受一个接口,通过switch判断选择读经的case,并在某种意义上将其转换为该种类型。以下代码为 fmt.Printf 通过类型选择将值转换为字符串的简化版。
1 | type Stringer interface { |
一个接口类型的变量可能包含任何类型的值,因此我们需要用类型断言来检查其动态类型,即运行时在变量中存储的值的实际类型。例如我们可以测试某个时刻接口varI是否包含类型T的值:
1 | if v, ok := varI.(T); ok { // checked type assertion |
如果转换合法,那么v就是varI转换到类型T的值。
空白标识符
空白标识符可被赋予为任何类型的任何值,它表示只写的值,作为占位符。
另外,空白标识符能使得那些未使用的导入和变量不受影响,能顺利通过编译,比如这样的程序,由于有两个未使用的导入和一个未使用的变量fd,因此不能编译。
1 | package main |
通过引入空白标识符,则可以顺利通过编译:
1 | package main |
还有一种用法,是为了使用其副作用而引入包,比如在 net/http/pprof 包的 init 函数中记录了 HTTP 处理程序的调试信息。它有个可导出的 API, 但大部分客户端只需要该处理程序的记录和通过 Web 页面访问数据。那么可以这样使用:
1 | import _ "net/http/pprof" |
错误
由于golang的多值返回,我们可以很轻易地返回各种类型的错误提示,按照约定,错误的类型通常为 error,这是一个内建的简单接口。
1 | type error interface { |
Panic
内建的panic函数,会产生一个运行时错误并终止程序,还会在程序终止时打印。比如这样:
1 | func CubeRoot(x float64) float64 { |
由于panic被调用后,程序会终止当前函数的执行,并开始回溯goroutine的栈,当到达栈顶时,程序就会终止。不过我们可以用内建的 recover 函数来重新取回 goroutine 的控制权限并使其恢复正常执行。
由于在回溯时只有被推迟函数中的代码在运行,因此 recover 只能在被推迟的函数中才有效。
1 | func server(workChan <-chan *Work) { |