Effective Go Thoughts
命名
某个名称包外是否可见,取决于其首个字符是否为大写字母。
Go中约定使用驼峰记法 MixedCaps 或 mixedCaps。
包名
包应当以小写的单个单词来命名,且不应使用下划线或驼峰记法,保持简洁易于理解。
包的导入者可以通过包名来引用其内容,因此包中可导出的名称 可以用包名来避免冲突。如bufio里的读取器叫Reader而非BufReader。因为用户使用时是bufio.Reader。这个显然清楚而简洁,不会与io.Reader发生冲突。同样用户创建ring.Ring的方法,可以叫做ring.New而不用ring.NewRing。因为Ring和它的包重名,也利用理解。
**长命名并不会使其更具可读性。一份有用的说明文档通常比额外的长名更有价值。**如once.Do(setup)就比once.DoOrWaitUntilDone(setup)清晰。
接口名
只包含一个方法的接口应当以该方法的名称加上-er后缀来命名。如 Reader、Writer、 Formatter、CloseNotifier等。
|
|
控制结构
if
if执行体里以 break、continue、goto 或 return 结束时,不必要的 else 会被省略。这是 代码为了防范一系列的错误条件,若控制流程继续,则说明排除了错误。由于错误后,直接return所以也就无需else了。
|
|
重新声明与再次赋值
|
|
发现两个语句都出现了err,第一条中err被声明,第二条中err只是被再次赋值。
在满足下列条件时,已被声明的变量 v 可出现在:= 声明中:
- 本次声明与已声明的 v 处于同一作用域中(若 v 已在外层作用域中声明过,则此次声明会创建一个新的变量
- 在初始化中与其类型相应的值才能赋予 v,且在此次声明中至少另有一个变量是新声明的
For
它统一了do和while,有三种形式。
|
|
若你想遍历数组、slice、字符串或者map,或从channel中读取消息,使用range实现循环。
|
|
Switch
case 语句会自上而下逐一进行求值直到匹配为止,但break可以使switch提前终止。有时候要打破循环,可以使用break到标签位置。fallthrough强制执行后面的case代码,如果所有case都有fallthrough,则default会被执行。
|
|
类型选择
switch 也可用于判断接口变量的动态类型。如 类型选择 通过圆括号中的关键字 type 使用类型断言语法。若 switch 在表达式中声明了一个变量,那么该变量的每个子句中都将有该变量对应的类型。代码
函数
可命名结果形参
Go函数的返回值或结果“形参”可被命名,并作为常规变量使用,就像传入的形参一样。 命名后,一旦该函数开始执行,它们就会被初始化为与其类型相应的零值; 若该函数执行了一条不带实参的 return 语句,则结果形参的当前值将被返回。
这种做法能让代码简单而清晰。如下面的io.ReadFull
|
|
defer
defer用于预设一个函数调用(即推迟执行函数)。通常用来释放资源,解锁互斥和关闭文件。
|
|
defer的函数若包含实参,它会在defer执行的时候求值,而不是在调用时求值。这样就不用关心变量在函数执行时被改变。defer函数会按照(LIFO)后进先出的顺序执行。
|
|
defer是在return之前执行的,参考官方文档
|
|
defer匿名函数与外部数据形成闭包时,闭包里的数据传入的是引用。
|
|
数据
new/make分配
new不会初始化内存,只会将内存置为零。也就是说,new(T)会为类型T分配一个已置零的内存空间,并返回它的地址,也是就*T的值。
复合字面,即File{fd, name, nil, 0}这种。而复合字面不包含字段时,它将创建该类型的零值。即:new(File) 和 &File{} 是等价的。
make用于创建slice/map/chan,并返回类型为T(而非*T)的 已初始化(而非零值)的值。造成这种差异的原因,这三种类型本质上为引用数据类型,它们内部还包含其它内容,所以在使用之前必须初始化。
理解:零值就是 该数据类型的初始值,如int默认为0,bool默认为false,而引用类型默认就是nil。
所以make返回 引用类型 初始化值,用于将其内部数据结构准备好将要使用的值。
下面例子阐明new和make的区别:
|
|
Arrays
数组作为切片的构件。它是值类型。
- 数组是值。将一个数组赋予另一个数组会复制其所有元素。
- 将某个数组传入某个函数,它将接收到该数组的一份副本而非指针。
- 数组的大小是其类型的一部分。类型 [10]int 和 [20]int 是不同的。
针对第二点的例子:
|
|
Slices
切片对数据进行封装,提供更方便的接口。
切片保存了对底层数组的引用
,将某个切片赋予给另一个切片,它会引用同一个数组。**因此,切片作为函数参数传入时候,函数可以修改其内部元素。**可以理解为切片传递了底层数组的指针。
|
|
因此,Read函数可以接受一个切片实参,而非一个指针和一个计数。
Maps
map一般通过复合字面语法进行构建。通过key去获取某个val时,若key不存在,就会返回val类型对应的零值。一般我们通过ok来判断key是否存在。
|
|
这里注意一下覆写String方法是,在print时会无限递归的情况。
|
|
在Sprintf里隐式的会调用m的String方法,就造成了无限递归。 解决:
|
|
初始化
常量
Go中的常量就是不变量。枚举常量使用枚举器iota创建,它可以隐式的重复。
|
|
init函数
每个源文件都可以通过init来设置一些必要的状态。注意init函数可以声明多个,且按照声明的先后顺序执行。文件从import到声明到init的顺序,参考
方法
指针vs值
我们可以为任何已命名的类型(除了指针或接口)定义方法; 接收者可不必为结构体。
|
|
可以看出Fprintf需要的是io.Writer类型,而我们只有*ByteSlice才实现了io.Writer,而ByteSlice没有。所以我们要传入&b。
不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。
指针方法的好处在于可以在方法里修改接收者,而值方法会导致方法 接收到的是 该值的副本(产生一次拷贝),任何修改都不会成功。
空白标识符
可被赋予或声明为任何类型的任何值,而其值会被无害地丢弃。在需要变量但不需要实际值的地方用作占位符。
有时候,写了一半的程序有未使用的包 和 变量,又想让它能编译。这是可以: 用空白标识符来引用未使用的包、将未使用的变量赋值给空白标识符。如下:
|
|
为副作用而导入,如pprof导入只是使用其中的init。
|
|
接口检查
一个类型无需显式地声明它实现了某个接口。取而代之,该类型只要实现了某个接口的方法, 其实就实现了该接口。大部分接口检测都会在编译时,例如,将一个 *os.File 传入一个预期的 io.Reader 函数将不会被编译, 除非 *os.File 实现了 io.Reader 接口。
而有些接口检测会在运行时,如encoding/json中的Marshaler接口。当JSON编码器接收到一个实现了该接口的值,那么该编码器就会调用该值的编组方法, 将其转换为JSON,而非进行标准的类型转换。
而编码器则使用类型断言来检测其属性。通常只需判断,就使用空白标识符来忽略类型断言的值。
|
|
确保一个类型 必须实现该接口,否则编译出错。
|
|
若Interface接口修改,则最后一行是无法编译成功的。
内嵌
Go不提供子类化的概念,但可以通过将类型内嵌到结构体或接口中,它就能借鉴部分实现。
接口内嵌:io.ReadWriter就包含了Reader和Writer接口,从而包含了Read和Write方法。
|
|
结构体内嵌:bufio.ReadWriter就包含了bufio.Reader和bufio.Writer,它就包含了两者的方法。同时还满足io.Reader、io.Writer、io.ReadWriter三个接口。
|
|
可以看出这个结构体内嵌的方便,它比在结构体内声明变量,然后显示的去调用变量的方法方便很多。如下就是显示的调用:
|
|
注意:
当内嵌一个类型时,该类型的方法会成为外部类型的方法, 但当它们被调用时,该方法的接收者是内部类型,而非外部的。在我们的例子中,当 bufio.ReadWriter 的 Read 方法被调用时,它与上面显示声明变量并调用的方法 具有同样的效果;接收者是 ReadWriter 的 reader 字段,而非 ReadWriter 本身。
实例:
|
|
这里的Job就具备了Logger的方法,如job.Log("xxx")
。
而Logger 是 Job 结构体的常规字段,所以我们通过一般方式初始化。
|
|
**注意:内嵌类型是作为外部结构体的常规字段存在,所以在初始化后,外部结构体是可以访问的内嵌结体里的公开属性的。**如下:
|
|
并发
不要通过共享内存来通信,而应通过通信来共享内存。
Channels
- 接收方会一直阻塞直到有数据到来
- 如果channel是无缓冲的,发送方会一直阻塞直到接收方将数据取出
- 如果channel带有缓冲区,发送方会一直阻塞直到数据被拷贝到缓冲区
- 如果缓冲区已满,则发送方只能在接收方取走数据后才能从阻塞状态恢复
带缓冲的信道可被用作信号量,例如限制吞吐量。缓冲区的容量决定了处理数量上限。
Channels of channels
这种可以理解成一个横的管道,其中的每个元素都是一个竖着的管道。
这种特性通常被用来实现安全、并行的多路分解。
代码 这里面的workerPool就是一个带缓冲的chan chan job。即其中每一个worker都包含一个chan job用来读取任务。
并行化
这里就要谈到Go的并发:关于goroutine的管理调度执行称为go的并发。 而并行是指在不同物理CPU上,执行不同的代码。
简单说,并行是可以同时做很多事情。并发是同时管理很多事情,它同一时刻只能执行一件事。参考
错误
error是一个内建的接口
|
|
我们可以实现这个接口来自定义 一些错误信息。
panic
有时错误无法恢复,不能让程序进行。为此,提供了内建panic函数,它会产生一个运行时错误并终止程序。它一般接收字符串,并在程序终止时打印。
recover
当panic后,程序立刻终止当前函数的执行,并开始回溯goroutine的栈,运行defer函数。当回溯到goroutine的顶端,程序就将终止。但是,我们可以通过recover函数来获取goroutine控制权并恢复正常。
调用recover将停止回溯过程,由于在回溯时只有defer函数中的代码在运行,因此 recover 只能在被推迟的函数中才有效。
Reference
Effective Debugging Techniques
attach effective debugging video by Andrii Soldatenko