Go 语言的一些编程规范

主要参考了 Google 对外发布的编程规范:Go Code Review Comments 和内部的一些使用习惯。

两个小工具

  • gofmt:帮助你自动格式化代码
  • goimports:帮助你自动添加/删除包的引用

代码注释的语句

更多参见:https://golang.org/doc/effective_go.html#commentary
尽管有时候会显得比较多余,但是代码注释最好是完整的语句,大写开头句号结尾。
这样做的好处是,在自动生成 godoc 文档时,会产生更好的格式。

// A Request represents a request to run a command.
type Request struct { ...

// Encode writes the JSON encoding of req to w.
func Encode(w io.Writer, req *Request) { ...

Contexts

如果需要 Contexts 的话,将它作为方法的第一个参数传入:
func F(ctx context.Context, /* other arguments */) {}

  • 不要将 Context 作为一个结构体成员变量
  • 不要自定义 Context 类型
  • Context 是一个 immutable 不可变对象

复制 Copying

例如,bytes.Buffer 类型包含一个 []byte 切片。当你去复制一个 Buffer 对象时,实际最后都是指向同一个 []byte

定义空的切片 Declaring Empty Slices

推崇的方式 var t []string:定义了个 nil
不推崇的方式 t := []string{}:不是 nil,但是长度为 0
不过对上面这两种方式定义的变量,调用 len()cap() 的结果都是 0

加密随机数 Crypto Rand

Do not use package math/rand to generate keys, even throwaway ones. Unseeded, the generator is completely predictable. Seeded with time.Nanoseconds(), there are just a few bits of entropy. Instead, use crypto/rand's Reader, and if you need text, print to hexadecimal or base64:
不要使用 math/rand 来产生加密的 keys,因为 math/rand 的随机数算法是可预测的。
推荐使用 crypto/rand,也可以将结果转换成 hexadecimal 或者 base64
示例:

import (
    "fmt"
    "crypto/rand"
)

func Key() string {
    buf := make([]byte, 16)
    _, err := rand.Read(buf)
    if err != nil {
        panic(err)  // out of randomness, should never happen
    }
    return fmt.Sprintf("%x", buf)
    // or hex.EncodeToString(buf)
    // or base64.StdEncoding.EncodeToString(buf)
}

func main() {
    fmt.Println(Key()) // 40ca8b6ff7e65501b097cc0e9aebdc2e
    fmt.Println(Key()) // 5faaad1a34483977420349e980954b6f
}

文档注释 Doc Comments

Exported 导出的方法/变量需要有注释。

不要使用 Panic Don't Panic

不要使用 Panic 来进行异常的处理,例如:

import (
    "os"
)

func main() {
    _, err := os.Create("/tmp/file")
        if err != nil {
            panic(err) // 不推荐
        }
}

如果你真的想要程序马上退出的话,可以使用 log.Exit。否则,可以使用 log.Fatal 来描述一般的错误。

异常语句 Error Strings

异常语句不要以大写开头,结尾也不要有标点符号
推荐:fmt.Errorf("something bad")
不推荐:fmt.Errorf("Something bad")
什么原因呢?因为通常异常都会被捕获,最后作为日志的一部分打印出来,所以就最好不要有大写字母和标点符号。

因此对于日志语句而言,可以大写开头,也可以标点符号结尾,例如 log.Printf("Reading %s: %v", filename, err)

示例 Examples

在添加新的 package 时,可以添加一些示例,会自动添加到 godoc 文档中,例如:


Go 语言的一些编程规范_第1张图片
示例 Examples

Goroutine Lifetimes 生命周期

使用 goroutines 时候,确保他们及时退出。

处理异常 Handle Errors

不要使用 _ 来忽略异常,例如 res, _ := func()
要检查并处理异常,例如:

res, err := func()
if err != nil {
    // Handle the error, return it, or, in truly exceptional situations, call log.Fatal or, if necessary, panic.
}

包的导入 Imports

尽量不用重命名 package,包的引用最好进行分组,使用空白行分隔,例如:

import (
    "fmt"
    "hash/adler32"
    "os"

    "appengine/foo"
    "appengine/user"

    "github.com/foo/bar"
    "rsc.io/goversion/version"
)

ImportBlank

Go 语言要求导入的包必须在后续中使用,否则会报错。
如果想要避免这个错误,可以在包的前面加上下划线 _,例如 _ "net/http"

问题来了,如果一个包不被使用,那为什么要导入呢?
因为导入匿名包仅仅表示无法再访问其内的属性。但导入这个匿名包的时候,会进行一些初始化操作,例如 init(),如果这个初始化操作会影响当前包,那么这个匿名导入就是有意义的。
例如:

import (
  "database/sql"

  _ "github.com/lib/pq" // 我们需要的是这个包里面的 init() 方法
)

Import Dot

如果不想在访问包属性的时候加上包名,则导入的时候,可以为其设置特殊的别名:点 .,例如:

import (
    . "fmt"
)

func main() {
    Println()    // 无需包名,直接访问Println
}

In-Band Errors

不要返回 -1 或者空指针来代表出现了异常。Go 支持多个返回值,因此将异常或者状态作为一个单独的值返回,例如:

// Lookup returns the value for key or ok=false if there is no mapping for key.
func Lookup(key string) (value string, ok bool)

这样的话,我们就可以通过如下的方式来调用该方法:

value, ok := Lookup(key)
if !ok  {
    return fmt.Errorf("no value for %q", key)
}
return Parse(value)

Indent Error Flow

保持缩进尽量的少。
例如下面一段代码:

if err != nil {
    // error handling
} else {
    // normal code
}

我们可以修改为如下的方式,从而减少 normal code 的缩进:

if err != nil {
    // error handling
    return // or continue, etc.
}
// normal code

Initialisms

对于缩略词,保持统一的大小写。
推荐 URL,不推荐 Url
推荐 ServeHTTP,不推荐 ServeHttp
推荐 appID,不推荐 appId

接口 Interfaces

Do not define interfaces on the implementor side of an API "for mocking"; instead, design the API so that it can be tested using the public API of the real implementation.

Do not define interfaces before they are used: without a realistic example of usage, it is too difficult to see whether an interface is even necessary, let alone what methods it ought to contain.

Line Length

Go 语言对每一行的长度没有明确的限制。

Mixed Caps

对于多个单词组成的名字,使用 MixedCaps,而不是使用下划线分割。
例如,推荐 maxLength,不推荐 MaxLengthMAX_LENGTH

Named Result Parameters VS Naked Returns

在定义函数的返回值的时候,需不需要命名呢?

对比下面的两个函数定义:
func (n *Node) Parent1() *Node
func (n *Node) Parent1() (node *Node)
前者更简单明了。

但是如果某个函数有很多个返回值,并且每个返回值的意义不容易从上下文中推断的话,建议命名。
对比下面的两个函数定义:

func (f *Foo) Location() (float64, float64, error)
// Location returns f's latitude and longitude.
// Negative values mean south and west, respectively.
func (f *Foo) Location() (lat, long float64, err error)

后者表达的更清楚,并且能体现到 godoc 中。

包的注释 Package Comments

添加在包名的上方,大写开头,句号结尾。例如:

// Package math provides basic constants and mathematical functions.
package math
/*
Package template implements data-driven templates for generating textual
output such as HTML.
....
*/
package template

包的名字 Package Names

  • 如果已经定义好了包名,例如 chubby,那么这个包里面定义的其他东西,就不用再加上包名了。
    • 例如不需要定义一个结构体叫做 ChubbyFile,这样别人在使用的时候,就会出现 chubby.ChubbyFile,略显重复。
    • 我们直接将该结构体命名为 File,这样别人在使用的时候,就是 chubby.File,简单明了。
  • 避免使用没有意义或者意义过于宽泛的名字,例如 util, common, misc, api, types, interfaces 等等。

使用值来传递函数的参数 Pass Values

不要为了节省空间,而使用指针来传递函数的参数。

几个特例:

  • 对于大的结构体,或者会增大的结构体,用指针来传递函数的参数。
  • 对于 protocol buffer 类型,用指针来传递函数的参数。

用什么变量名来接收函数返回的结果 Receiver Names

Go 语言推崇使用一两个字母的缩写。
例如,用 c 或者 cl 替代 client

用什么类型来接收函数返回的结果 Receiver Type

到底是用值还是指针来接收函数返回的结果?
Go 语言推崇使用指针。有时候也可以使用值来接收,特别是对于一些不会改变的结构体或者原始类型。

一些基本原则如下:

  • 如果函数返回的是 map,函数或者是 slice,不要使用指针。
  • 如果函数需要修改这个返回值,那么需要使用指针。
  • 如果函数返回的是一个结构体,并且包含了 sync.Mutex 或者类似的同步字段,那么需要使用指针,来避免内容的复制。
  • 如果函数返回的是一个大的结构体或者数组,那么使用指针更高效。
  • 如果使用值来接收,实际上是产出了内容的拷贝,对内容的修改不会影响函数内部。如果想要结果对函数内部可见,需要使用指针。
  • 如果函数返回的是一个结构体或者数组或者 slice 切片,并且里面有指针类型的成员,那么推荐使用指针来接收。
  • 如果函数返回的是小的一个结构体或者数组,并且里面的都是值类型的成员,例如 time.Time 或者 int,那么推荐使用值来接收,这样更高效。原因:使用值接收的话,是直接在栈空间上进行复制,而使用指针的话,需要分配堆空间。

返回同步结果的函数 Synchronous Functions

Go 语言推崇直接返回同步结果的函数。

打印有用的测试错误信息 Useful Test Failures

当单元测试出现错误时,需要打印有用的信息,包括输入是什么,实际结果是什么,期望结果是什么。例如:

if got != tt.want {
    t.Errorf("Foo(%q) = %d; want %d", tt.in, got, tt.want) // or Fatalf, if test can't test anything more past this point
}

注意:Go 语言推崇将实际结果放在前面,期望结果放在后面。

变量名 Variable Names

Go 语言推崇的变量名,特别是局部变量。
例如,用 c 替代 lineCount,用 i 替代 sliceIndex

一个基本原则是,如果一个变量在将来会被更多的用到,那么这个变量名可以取得更有意义,否则就应该用短的名字。

断言 Assert

Go 语言不推荐在测试中使用断言,例如:assert.isNotNil(t, "obj", obj),它会使得测试提前结束,或者丢失掉有用的信息。

Go 语言推荐如下的方式:

if obj == nil {
    t.Errorf("AddPost() = %+v", obj)
}

如果要比较两个对象,可以使用 cmp.Equal
如果要比较两个 Protocol buffers,可以使用 proto.Equal 或者 messagediff.Compare

Getters

对于成员变量,Go 语言不推荐在 Get 方法前添加 Get 前缀,例如:owner := obj.GetOwner(),推荐直接使用 owner := obj.Owner()

SwitchBreak

不同于 Java 等语言,Go 语言在 switch 中会自动 break,因此不需要手动添加 break

switch x {
case "A", "B":
  buf.WriteString(x)
case "C":
  // handled outside of the switch statement
default:
  return fmt.Errorf("unknown value: %q", x)
}

测试包 TestPackage

单元测试的文件可以放在被测试文件同一个包中,命名为 foo_test.go

Use %q

%q 会将字符串包裹在双引号中。
例如:

fmt.Printf("value %q", "Hello") // value "Hello"
fmt.Printf("value '%s'", "Hello") // value 'Hello'

你可能感兴趣的:(Go 语言的一些编程规范)