Effective Go

本文是关于美团tutor给出的学习资料 — 官方文档”Effective Go”的笔记

参考资料:

Formatting

使用命令gofmt -w code.go或者go fmt,可以对代码进行格式化,使代码易读、易维护,不用再纠结多敲几个空格让代码更加美观 — fmt将会为我们做这些事情

before:

1
2
3
4
type T struct {
name string // name of the object
value int // its value
}

after:

1
2
3
4
type T struct {
name string // name of the object
value int // its value
}

当然还有一些细节需要用户注意:

  • 缩进 — tab键
  • 行长度 — 没有限制

Commentary

Go中的注释同C语言中一致,包括行注释以及块注释,形式分别为//以及/* */

每一个package source代码,在起始部分,都应该有属于自己的包注释。但是对于包含多个源代码文件的包,只需要其中一个文件中含有包注释即可。godoc将会收集包注释并且进行整合。

如果包很简单,包注释可以是多个行注释。但是不管如何,都应该遵循书面英语的模式:句子首字母大写,用句号分割。

在包内,任何声明语句前面的那条注释,都是针对声明语句的doc comment。每一个从外部import的名称都应该有doc comment

对于声明出来的函数,doc comment在注释第一个单词是函数名称的时候,有最好的效果,如下面所示

1
2
3
// Compile parses a regular expression and returns, if successful,
// a Regexp that can be used to match against text.
func Compile(str string) (*Regexp, error) {

若遵循上面的规范,那么就可以使用命令go doc -all regexp | grep -i parse,给定函数名称,返回自己对函数的注释,便于回忆以及快速抓取相关内容。

当然也可以按组来声明变量,如下面所示:

1
2
3
4
5
6
7
// Error codes returned by failures to parse an expression.
var (
ErrInternal = errors.New("regexp: internal error")
ErrUnmatchedLpar = errors.New("regexp: unmatched '('")
ErrUnmatchedRpar = errors.New("regexp: unmatched ')'")
...
)

Names

Package names

一般包名都是小写的单词,而不使用下划线或者mixedCaps

不需要担心包名重复,因为它只是默认的导入名称,即使出现了重复,也可以在局部选择另外一个名称使用。

在导入包之后,需要通过包名来访问其中的内容,如在包bufio中的缓存读取类型为Reader,那么可以通过bufio.Reader来进行访问;同样的,生成一个ring.Ringinstance,可以直接使用ring.New,这样使用的话,不会与自带的New冲突。灵活使用包结构,可以没有拘束地为变量起名。

Getters

在Go中,不同于Java,提供了自动的getter 以及 setter,用户需要主动提供这些方法。官方文档建议,对于getter方法,比如有一个字段的名称为owner,那么getter的方法命应该是Owner;而对于setter方法,可以命名为SetOwner,如果真的有必要存在的话

1
2
3
4
owner := obj.Owner()
if owner != user {
obj.SetOwner(user)
}

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
2
3
4
if i < f()  // wrong!
{ // wrong!
g()
}

Control structures

if

if 以及switch都支持一条初始化语句,便于创建局部变量

Redeclaration and reassignment

:=支持至少新创建一个变量的赋值

For

1
2
3
4
5
6
7
8
// Like a C for
for init; condition; post { }

// Like a C while
for condition { }

// Like a C for(;;)
for { }

当遍历数组、切片、字符串、map或者通道的时候,range关键字可以更好地管理循环

1
2
3
for key, value := range oldMap {
newMap[key] = value
}

如果其中某个字段不想使用,那么可以用blank identifier_来替代

在遍历string的时候,go还将会自动将其转换为rune,存储到value字段中

Switch

go中的switch比起C语言中的更加灵活:除了支持常量以及整数之外,它将会从上到下遍历所有的cases,直到条件为true为止。

同时,也是可以将一系列同级的记号通过逗号分隔,写在同一个case中的,如

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

该关键字还可以用来获得一个接口变量的动态类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T\n", t) // %T prints whatever type t has
case bool:
fmt.Printf("boolean %t\n", t) // t has type bool
case int:
fmt.Printf("integer %d\n", t) // t has type int
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}

Functions

Multiple return values

依赖于这一特性,可以直接从函数的返回值error来判断函数是否正确执行。

对于返回值,可以对其命名。在进入函数的时候,这些带有名字的返回值将会被根据其类型初始化为zero value,待函数内部执行完毕,直接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

典型的应用场景(canonical examples)为释放互斥锁或者关闭文件

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的顺序执行。

如果defer的函数中包含有参数,并且参数是需要执行的代码,那么在执行到defer的时候,将会首先执行参数中的代码,之后推迟被defer的函数的执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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")
}

func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}

func main() {
b()
}

对应输出

1
2
3
4
5
6
entering: b
in b
entering: a
in a
leaving: a
leaving: b

Data

Allocation with new

new方法是自带的分配内存的方法,但是它只是将创建的变量赋值为zero value而不初始化它们。

对于某些数据,初始化构造器是必要的,如下

1
2
3
4
5
6
7
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0}
return &f
}

不同于C中的“函数中不可以返回局部变量”,go中是可以直接返回的,对应变量的生存周期在函数返回之后仍然生效,因此上述代码可以改进最后两行为:

1
return &File{fd, name, nil, 0}

初始化构造器对于数组、切片以及map都是有效的

1
2
3
a := [...]string   {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]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
2
3
4
5
6
7
8
9
10
11
12
13
func Append(slice, data []byte) []byte {
l := len(slice)
if l + len(data) > cap(slice) { // reallocate
// Allocate double what's needed, for future growth.
newSlice := make([]byte, (l+len(data))*2)
// The copy function is predeclared and works for any slice type.
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:l+len(data)]
copy(slice[l:], data)
return slice
}

Two-dimensional slices

声明二维数组或切片的方法

1
2
type Transform [3][3]float64  // A 3x3 array, really an array of arrays.
type LinesOfText [][]byte // A slice of byte slices.

第二维是不必要都一致的

1
2
3
4
5
text := LinesOfText{
[]byte("Now is the time"),
[]byte("for all good gophers"),
[]byte("to bring some fun to the party."),
}

Map

如果使用map中不存在的key抓取元素,那么将会返回value字段的zero value;在某些时候可能某些原本存在的key对应的value就是zero value,此时可以通过第二个返回值来判断抓取是否成功

1
2
3
var seconds int
var ok bool
seconds, ok = timeZone[tz]

可以使用关键字delete来删除map中的键值对

1
delete(timeZone, "PDT")  // Now on Standard Time
0%