Contents

Effective Go

导语
如何编写清晰、地道的 Go 代码

Formatting 格式化

在 Golang 中,gofmt 以包未处理对象而非源文件,它将 Go 程序按照标准风格缩进、对齐,保留注释并在需要时重新格式化。

  • Indentation 缩进:使用 制表符 Tab 缩进,gofmt 默认使用
  • Line length 行长度:Go 对行的长度没有限制
  • Parentheses 括号:控制结构(if, for, switch)在语法上并不需要圆括号

Commentary 注释

Go 支持 C 风格的块注释 /* */ 和 C++ 风格的单行注释 //,其中,// 注释更常用,而 /* */ 则主要用于包的注释

godoc 即使一个程序,又是一个 Web 服务器,它对 Go 的源码进行处理,并提取包中的文档内容: 出现在顶级声明之前,且与该声明之间没有空行的注释,将与该声明一起被提出来,作为该条目的说明文档。

每个包都应包含一段包注释,即放置在包子句前的一个块注释。 对于包含多个文件的包,包注释只需出现在其中的任一文件中即可。 包注释应在整体上对该包进行介绍,并提供包的相关信息。 它将出现在 godoc 页面中的最上面,并为紧随其后的内容建立详细的文档。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/*
Package regex implements a simple library for regular expressions.

The syntax of the regular expressions accepted is:

  regexp:
    concatenation { '|' concatenation }
  concatenation:
    { closure }
  closure:
    term [ '*' | '+' | '?' ]
  term:
    '^'
    '$'
    '.'
    character
    '[' [ '^' ] character-range ']'
    '(' regexp ')'
*/
package regex

Names 命名

Package names 包名

当一个包被导入后,包名就会成为内容的访问器 import "bytes",按照惯例,包应当以某个小写的单个单词命名,且不应使用下划线或驼峰记法。 例如,err 的命名就是出于简短考虑。

包名是导入时所需的唯一默认名称,它并不需要在所有源码中保持唯一,即便在少数发生冲突的情况下,也可为导入的包选择一个别名来局部使用。

另一个约定:包名应为其源码目录的基本名称。例如,src/pkg/encoding/base64 中的包应作为 encoding/basee64 导入,其包名应为 base64 而不是 encoding_base64 / encodingBase64.

长命名并不会使包更具有可读性,反而一份有用的说明文档通常比额外的长名更具价值。

Getter / Setter

Go 并不对 getter 和 setter 提供自动支持。

如将 Get 放入 getter 的名字中,既不符合习惯,也没有必要,但大写字母作为字段导出的 getter 是一个不错的选择,另外 Set 放入 setter 是个不错的选择。

1
2
3
4
5
6
type Object struct {
  ower string
}

func (o *Object) Ower() string { return o.ower }
func (o *Object) SetOwer(s string) { o.ower = s }

Interface names 接口名

按照规定,只包含一个方法的接口应当以该方法的名称加上 er 后缀来命名,如 Reader / Writer / Formater 等。

字符串转换方法命名应为 String 而非 ToString

MixedCaps 驼峰记法

Go 中约定使用驼峰记法

分号

和 C 一样,Go 的正式语法使用分号 ; 来结束语句,但 Go 的分号不一定出现在源码中,而是词法分析器会使用一条简单的规则来自动插入分号

规则:如在新行前的最后一个标记为标识符(int/float64等)、数值或字符串常量之类的基本字面或breakcontinuefallthroughreturn++--)} 之一,则词法分析器将始终在该标记后面插入分号,即如果新行前的标记为语句的末尾,则插入分号;

通常,Go 程序只在诸如 for 循环子句这样的地方使用分号,来以此将初始化器、条件及增量元素分开;

Control structures 控制结构

Go 不再使用 do / while 循环,只有一个更为通用的 for

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// C: for
for init; condition; post { }
// C: while
for condition { }
// C: for(;;)
for { }

// [12]aT, []vT, map[sting]any mT
for key, value := range aT/vT/mT { }
for key := range aT/vT/mT { }
for _, value := range aT/vT/mT { }

Go 没有逗号操作符,且 ++/-- 是语句而非表达式

1
2
3
for i, j := 0, len(aT) - 1; i < j; i, j = i + 1, j - 1 { // Not: i++, j--
  a[i], a[j] = a[j], a[i]
}

switch 更加灵活,其表达式无需为常量或整数,case 语句会自上而下逐一进行求值直至匹配为止,它不会自动下溯,但 case 可通过逗号分隔来列举相同的处理条件

break 语句会使 switch 提前终止

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

func shouldEscape(c byte) bool {
  switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
      return true
  }
  return false
}

if 强制使用大括号,并且接受初始化语句

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

Function 函数

Go 与众不同的特性之一,就是函数和方法可以返回多个值,返回值或结果“形参”可被命名,并作常规变量使用。

Go 的 defer 语句用于预设一个函数调用(即延迟执行函数),该函数会在执行 defer 的函数返回之前立即执行。 被推迟的多个函数,会按照后进先出(LIFO)的顺序执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func Contents(filename string) (string, error) {
  f, err := os.Open(filename)
  if err != nil {
    return "", err
  }
  defer f.Close()

  var result []byte
  buf := make([]byte, 100)
  for {
    n, err := f.Read(buf[0:])
    result = append(result, buf[0:n]...)
    if err != nil {
      if err == io.EOF {
        break
      }
      return "", err
    }
  }
  return string(result), nil
}

Data 数据

new(T) 会为类型为 T 的新项分配已置零的内存空间, 并返回它的地址,也就是一个类型为 *T 的值(返回一个指针, 该指针指向新分配的,类型为 T 的零值)。

内建函数 make(T, args) 的目的不同于 new(T)。它只用于创建切片、映射和信道,并返回类型为 T(而非 *T )的一个已初始化 (而非置零)的值。 出现这种用差异的原因在于,这三种类型本质上为引用数据类型,它们在使用前必须初始化。

1
2
3
4
5
6
7
// Allocates slice structure; *p == nil; rarely useful
var p *[]int = new([]int)
// The slice v now refers to a new array of 100 ints
var v []int = make([]int, 100)

// 惯用法
v := make([]int, 100)

Array 数组

数组主要用作切片的构件,主要特点:

  • 数组是值,讲一个数组赋值给另一个数组会复制其所有元素
  • 如将数组作为参数传入某个函数,则会收到该数组的一份副本而非指针
  • 数组的大小是其类型的一部分
1
2
3
4
5
6
7
8
9
func Sum(a *[3]float64) (sum float64) {
  for _, v := range *a {
    sum += v
  }
  return
}

aV := [...]float64{1, 2, 0.7}
fmt.Println(Sum(&aV))

Slice 切片

切片通过对数组进行封装,为数据序列提供了更通用、强大而方便的接口。

slice 保存了对底层数组的引用,如将某个 slice 赋值给另一个 slice,则他们会引用同一个数组。

若某个函数将一个切片作为参数传入,则它对该切片元素的修改对调用者而言同样可见, 这可以理解为传递了底层数组的指针

只要切片不超出底层数组的限制,它的长度就是可变的,只需将它赋予其自身的切片即可。切片的容量可通过内建函数 cap 获得,它将给出该切片可取得的最大长度。

若数据超出其容量,则会重新分配该切片,返回值即为所得的切片。

尽管 Append 可修改 slice 的元素,但切片自身(其运行时数据结构包含指针、长度和容量)是通过值传递的.

二维数组

一种是独立地分配每一个切片;而另一种就是只分配一个数组, 将各个切片都指向它

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 独立地分配每一个切片
pic := make([][]uint8, YSize)
for i := range pic {
  // 一次一行
  pic[i] = make([]uint8, XSize)
}

// 顶层 slice
pic := make([][]uint8, YSize)
// 分配一个大的切片来保存所有像素
pixels := make([]uint8, XSize*YSize)
// 遍历行,从剩余像素切片的前面切出每一行
for i := range pic {
  pic[i], pixels = picxels[:XSize], pixels[XSize:]
}

Map

可以关联不同类型的值。其键可以是任何相等性操作符支持的类型, 如整数、浮点数、复数、字符串、指针、接口(只要其动态类型支持相等性判断)、结构以及数组。 切片不能用作映射键,因为它们的相等性还未定义。与切片一样,

映射也是引用类型。 若将映射传入函数中,并更改了该映射的内容,则此修改对调用者同样可见。

若试图通过映射中不存在的键来取值,就会返回与该映射中项的类型对应的零值

要删除 map 中的某项,可使用内建函数 delete,它以映射及要被删除的键为实参。 即便对应的键不在该 map 中,此操作也是安全的。

Reference