Effective Go学习

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
2
3
4
5
6
7
8
9
10
11
12
/*
提供常用库,输出一个vim-go
*/
package main

import "fmt"

//只是一个main函数
//不知道说什么了
func main() {
fmt.Println("vim-go")
}

go doc的使用可以接收包名作为参数,也可以不输入任何参数,那么显示觉得就是当前目录下的文档。

1
2
> go doc
> 提供常用库,输出一个vim-go

当然也可以打开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
2
3
4
if i < f()
{
g()
}

如果一行内写多个语句,也应该用分号隔开。

控制结构

Golang有几种控制结构,for,if,switch和select,没有圆括号,而且主体必须使用大括号。

if

一个简单的if语句:

1
2
3
if x > 0 {
return y
}

另外,if可以接收初始化语句,设置局部变量:

1
2
3
4
if err := file.Chmod(0664); err != nil {
log.Print(err)
return err
}

重新声明与再次赋值

比如这样的例程:

1
2
3
f, err := os.Open(name)
//xxxxxx
d, err := f.Stat()

在满足下列条件时,已被声明的变量可以出现:=声明中:

  • 本次声明与已声明的v处于同一作用域;(若v在外层作用域已经声明过,则此次声明会创建新的变量)
  • 在初始化中与其类型相应的值才能赋予v,并且此次声明中至少另有一个变量是新声明的

for

golang的for循环有三种:

1
2
3
4
5
for init; condition; post { }

for condition { }

for { }

如果要遍历数组、切片、字符串或者映射,抑或是从channel中读取信息,可以使用range子句:

1
2
3
for key, value := range oldMap {

}

switch

golang的switch语句,其表达式无需为常量或整数。

1
2
3
4
5
6
7
8
9
10
11
func unhex(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 0
}

这样就可以将一系列的if-else转为switch-case。

另外,switch的case可以使用逗号分隔来列举相同的处理条件:

1
2
3
4
5
6
7
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}

跟C一样,我们往往会希望用break打破循环,在golang中,如果我们希望打破外层的循环,可以给break增加一个标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Loop:
for n := 0; n < len(src); n += size {
switch {
case src[n] < sizeOne:
if validateOnly {
break
}
size = 1
update(src[n])

case src[n] < sizeTwo:
if n+1 >= len(src) {
err = errShortInput
break Loop
}
if validateOnly {
break
}
size = 2
update(src[n] + src[n+1]<<shift)
}
}

命名的结果参数

Go函数的返回可以给定一个名字,在命名后,一旦函数开始执行,它们就会被初始化为与其类型响应的零值;若该函数执行了一条不带参数的return语句,该结果形参的当前值就会被返回。

1
2
3
4
5
6
7
8
9
func ReadFull(r Reader, buf []byte) (n int, err error) {
for len(buf) > 0 && err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:]
}
return
}

defer

Go的defer语句可以预设一个函数调用,让它在执行defer的函数返回之前立即执行,这可以用来执行一些释放资源的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // f.Close will run when we're finished.

var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
result = append(result, buf[0:n]...) // append is discussed later.
if err != nil {
if err == io.EOF {
break
}
return "", err // f will be closed if we return here.
}
}
return string(result), nil // f will be closed if we return here.
}

被defer的函数调用按照LIFO的顺序执行,比如这样的代码:

1
2
3
4
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
//4 3 2 1 0

data

golang提供了两种分配方法,即内建函数make和new,它们做的事情不同,但可能引起混淆。

new分配

这个内建函数会分配内存,并且将内存置为0,但不会初始化内存。在分配完已置为0的内存之后,会返回它的地址,比如new(T)会返回*T,即一个指向新分配的,类型为T的零值。

因此使用者只需要用new就可以创建一个新的对象,例如:零值bytes.Buffer就是已准备就绪的缓冲区,而零值的sync.Mutex就是已解锁的互斥锁。

另外这种零值属性是具有传递性的,考虑这样的structure:

1
2
3
4
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}

那么在后续的声明时,对于SyncedBuffer类型,只要声明分配好内存就可以直接使用:

1
2
p := new(SyncedBuffer)  // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer

构造函数与复合字面量

在很多情况下,我们声明一个对象不单单是要零值,还希望初始化成员,但总不能逐个赋值这么丑陋的做法嘛:

1
2
3
4
5
6
7
8
9
10
11
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := new(File)
f.fd = fd
f.name = name
f.dirinfo = nil
f.nepipe = 0
return f
}

而golang的做法是这样的:

1
2
3
4
5
6
7
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
//return &File{fd, name, nil, 0}
return &File{fd: fd, name: name} //此时其它字段默认为零值
}

与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
2
3
var seconds int
var ok bool
seconds, ok = timeZone[tz]

初始化

常量

Golang中的常量在编译时创建,而且只能是数字,字符,字符串或者布尔量。

init函数

init函数会在每个包完成初始化后自动执行,并且执行优先级会比main函数高,一般用来:

  • 对变量进行初始化;
  • 检查或者修复程序的状态;
  • 注册;
  • 进行一次计算;

接口

golang的接口为指定对象的行为提供了一种途径,如果某个类型可以完成这个,那么它就可以被用在这里。通过实现 String 方法,我们可以自定义打印函数,而通过 Write 方法,Fprintf 则能对任何对象产生输出。

例如一个实现了 sort.Interface 接口的集合就可通过 sort 包中的例程进行排序。该接口包括 Len()、Less(i, j int) bool 以及 Swap(i, j int)。

1
2
3
4
5
6
7
8
9
10
11
12
13
type Sequence []int

// Methods required by sort.Interface.
// sort.Interface 所需的方法。
func (s Sequence) Len() int {
return len(s)
}
func (s Sequence) Less(i, j int) bool {
return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}

接口转换和类型断言

类型选择是类型转换的一种形式:它接受一个接口,通过switch判断选择读经的case,并在某种意义上将其转换为该种类型。以下代码为 fmt.Printf 通过类型选择将值转换为字符串的简化版。

1
2
3
4
5
6
7
8
9
10
11
type Stringer interface {
String() string
}

var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
return str
case Stringer:
return str.String()
}

一个接口类型的变量可能包含任何类型的值,因此我们需要用类型断言来检查其动态类型,即运行时在变量中存储的值的实际类型。例如我们可以测试某个时刻接口varI是否包含类型T的值:

1
2
3
4
5
if v, ok := varI.(T); ok {  // checked type assertion
Process(v)
return
}
// varI is not of type T

如果转换合法,那么v就是varI转换到类型T的值。

空白标识符

空白标识符可被赋予为任何类型的任何值,它表示只写的值,作为占位符。

另外,空白标识符能使得那些未使用的导入和变量不受影响,能顺利通过编译,比如这样的程序,由于有两个未使用的导入和一个未使用的变量fd,因此不能编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"io"
"log"
"os"
)

func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: use fd.
}

通过引入空白标识符,则可以顺利通过编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"io"
"log"
"os"
)

var _ = fmt.Printf // For debugging; delete when done. // 用于调试,结束时删除。
var _ io.Reader // For debugging; delete when done. // 用于调试,结束时删除。

func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: use fd.
_ = fd
}

还有一种用法,是为了使用其副作用而引入包,比如在 net/http/pprof 包的 init 函数中记录了 HTTP 处理程序的调试信息。它有个可导出的 API, 但大部分客户端只需要该处理程序的记录和通过 Web 页面访问数据。那么可以这样使用:

1
import _ "net/http/pprof"

错误

由于golang的多值返回,我们可以很轻易地返回各种类型的错误提示,按照约定,错误的类型通常为 error,这是一个内建的简单接口。

1
2
3
type error interface {
Error() string
}

Panic

内建的panic函数,会产生一个运行时错误并终止程序,还会在程序终止时打印。比如这样:

1
2
3
4
5
6
7
8
9
10
11
12
func CubeRoot(x float64) float64 {
z := x/3 // Arbitrary initial value
for i := 0; i < 1e6; i++ {
prevz := z
z -= (z*z*z-x) / (3*z*z)
if veryClose(z, prevz) {
return z
}
}
// A million iterations has not converged; something is wrong.
panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
}

由于panic被调用后,程序会终止当前函数的执行,并开始回溯goroutine的栈,当到达栈顶时,程序就会终止。不过我们可以用内建的 recover 函数来重新取回 goroutine 的控制权限并使其恢复正常执行。

由于在回溯时只有被推迟函数中的代码在运行,因此 recover 只能在被推迟的函数中才有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func server(workChan <-chan *Work) {
for work := range workChan {
go safelyDo(work)
}
}

func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
do(work)
}