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 文档中,例如:


示例 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'
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容