这篇文章是Golang官方在Review代码时发现的一些常见问题,也是讨论比较多的,应此罗列这些常见问题供大家参考,这仅仅是一份共识,而不是一份规范指引。可以当作一份GO开发规范的补充信息。

goimports

虽 Go 默认带有 gofmt 工具,但还是强烈推荐增强性工具 goimport ,该工具在 gofmt 工具基础上增强提供自动删除和引入包功能。

你可在保存文件时,让编辑器自动执行命令,如 Sublime 编辑器插件 GoSublime 和 Atom 编辑器插件 go-plus均可实现。

在使用前,你需要获取包到本地:

$ go get golang.org/x/tools/cmd/goimports
  • 在 Sublime 中使用:具体参见: 插件说明文档
  • 在 LiteIDE 中使用:默认支持,如果不起作用,可手工配置:属性配置 -> golangfmt -> 勾选goimports
  • 在 Atom 中使用:具体参见:插件说明文档

注释

注释应该是一个完整的句子,这有利于在 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) { ...

定义空Slices

当定义一个空的 Slices 时,应该写成:

var t []string

而不是:

t := []string{}

这样的好处是避免在正式使用前分配内存。

文档说明

作为最顶级的,可以供包外部使用的信息均应该写文档说明。

不要Painc

不要到处使用Panic ,对于普通的错误,应该更多的使用包含 error 的多值返回。关于 Error 使用,可参见官方文档

错误信息

error 错误信息作为和上下文相关的信息不需要大写(除非是特定名词),也不需要以.结束。如使用fmt.Errorf("something bad") 而不写成:fmt.Errorf("Something bad."),这样在结合 log 使用时便有完整的信息:log.Print("Reading %s: %v", filename, err)

错误处理

不要使用_来忽略错误信息,如果一个方法有返回 error,则必须对 error 处理,已确保方法执行成功。处理 Error 时可以返回给上一层,也可以在特殊场景下 panic

导入包

对导入包进行特定分组,按标准包, 项目包,第三方包 依次分组,组与组间用空行隔开,其中标准包放在最前面。

package main

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

    "appengine/foo"
    "appengine/user"

    "code.google.com/p/x/y"
    "github.com/ysqi/beego"
)

高兴的是,goimprots 工具可以自动处理分组。

精简错误处理

尽量让代码按常规逻辑处理,先处理错误信息,缩短和精简错误处理范围,有利于提高代码可读性。不要写成:

if err != nil {
    // 错误处理
} else {
    // 常规逻辑
}

而应写成:

if err != nil {
    // 错误处理
    return // 或者继续.
}
// 常规逻辑

如果包含变量赋值,大部分人会写成:

if x, err := f(); err != nil {
  // 错误处理
  return
} else {
  // 使用变量 x
}
这时可以将赋值放到前面,精简错误处理:
```Go
x, err := f();
if  err != nil {
  // 错误处理
  return
} 
// 使用变量 x

命名简称

在使用简称或者缩写 (如:”URL” , “RMB” ) 时需保持命名一致性。例如URL使用时要么是URL,要么是url,不应该写出Url(如:”urlPony” , “URLPony”)。Go语言中一个很好的例子是ServerHTTP,而不是ServerHttp。

该规则也适合于简称,如ID是 identifier 的简称,使用是应该写出appID而不是appId

规范命名能有效提高可阅读性。

单行长度

这不是 Go 的的硬性规定,但单行长文字给人看得不舒服,如果单号超过越80个字符,请换行。长文字是不利于阅读的,分多行精简内容才是王道。看过报纸就知道,不是一篇文章从报纸最左边写到最右边,而是分成多个小区域排版的。当然Go 和 godoc 作为程序是能很好的展示的。

返回参数命名

根据实际情况给返还参数命名,想想看如果在 godoc 中看到:

func (n *Node) Parent1() (node *Node)
func (n *Node) Parent2() (node *Node, err error)

这个命名就完全没必要,还不如写成:

func (n *Node) Parent1() *Node
func (n *Node) Parent2() (*Node, error)

实际上一两个参数,通过返回类型就能明白含义的话,就没必要给返回参数命名。但另一方面,如果返回参数包含多个相同类型,或者没法推断含义的话,则需要给返回参数命名,如:

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 math provides basic constants and mathematical functions.
package math

/*
Package regexp 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-ranges ']'
        '(' regexp ')'
*/
package regexp

关于注释书写,可参考 Go 官方文档说明

包内命名

在包内定义方法,类型时,外部必须使用包名调用。这样,你在给方法命名就不需要在包含包名了。如你不应将方法命名为ChubbyFile,别人调用时会变成chubby.ChubbyFile。实际上,你应命名为File,调用为chubby.File

更多包内命名规范,可参考 Go 官方文档说明

值传递

不要为了节省少量的内存空间,而将值类型数据通过指针方式传递给方法。值类型数据本身是固定大小的,不会太大,是可以直接传递的。当然这适合 struct 类型数据,不管多大多小。

对象方法

在定义对象方法时,对象名应该对象类型简称,一两个单词表示即可。如:

func (cl *Client) Say(){...}

不要使用面向对象语言中常来指向自己的特定词,thismeself等。这个命名不是描述方法的一部分,它的作用非常明显,该类型下所有方法定义时都应该使用一致的简称。不要在一个方法中定义为cl,而在其他方法中定义为c。在 Go 中应该保持代码的一致和精简。

入参类型

常常非常困惑方法入参是定义传值还是传指针,尤其是对新手。如果困惑,那么你就传指针,但有时候传值也是有道理的。总得判断标准是看执行效率,如大小不变的固定结构和基础类型数据,一些经验规则:

  • 如果是 map,func,chan 则不需要用指针,本身即是引用类型。
  • 如果是 slice ,而方法内部又不需要修改它们,则不需要用指针。
  • 如果是 func ,也需要方法内部替换,则需要使用指针
  • 如果是包含 sysnc.Mutex 的 struct ,则需要使用指针,避免对象复制。
  • 如果是一个大的 struct 或者 array ,则使用指针更加高效。到底多大才是大呢?粗暴点就是比方法对象还大。
  • 更多…

准确反馈测试失败信息

在 Testing 中,测试失败打印的错误信息应该包含输入的是什么,期望的是什么,得到的又是什么。也许会促使你写一个帮助类,但不管如何,你都需要让错误信息是有用的。即是别人来调试使用你的程序,也能明白意思。一个测试失败的示例:

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

注意这里的顺序是实际!=预期,是消息提示的顺序,而 Go 中不需要想其他语言鼓励写出反向的。0 != x或者expected 0, got x

这里提供一个 Go 官方的具有代表性的测试代码,源自 fmt 包。

var flagtests = []struct {
    in  string
    out string
}{
    {"%a", "[%a]"},
    {"%-a", "[%-a]"},
    {"%+a", "[%+a]"},
    {"%#a", "[%#a]"},
    {"% a", "[% a]"},
    {"%0a", "[%0a]"},
    {"%1.2a", "[%1.2a]"},
    {"%-1.2a", "[%-1.2a]"},
    {"%+1.2a", "[%+1.2a]"},
    {"%-+1.2a", "[%+-1.2a]"},
    {"%-+1.2abc", "[%+-1.2a]bc"},
    {"%-1.2abc", "[%-1.2a]bc"},
}

func TestFlagParser(t *testing.T) {
    var flagprinter flagPrinter
    for _, tt := range flagtests {
        s := Sprintf(tt.in, &flagprinter)
        if s != tt.out {
            t.Errorf("Sprintf(%q, &flagprinter) => %q, want %q", tt.in, s, tt.out)
        }
    }
}

上述示例中,在测试不通过时,能非常清晰的知道哪个方法测试不通过。t.Errorf不是断言,而是提示错误信息,并继续运行。

变量命名

变量命名以精简为佳,但也有具有描述性。特别是局部变量可以使用更短的命名,用c比 lineCount 好,用i比 sliceIndex 好。

基本规则:命名必须具有描述性。对于对象方法,对象名用前面一两个单词做简称即可,优先使用一些常见的惯用词i,但全局性变量和不常见信息还是需要使用更多描述性内容的名称。

这篇文章能用来4小时断断续续才写完,当然是根据 Go 官方 Wiki-CodeReviewComments 所写。对于 Go 编码规范和开发习惯具有非常高的参考价值,经过验证提炼的指导意见,能帮助我们写出更优美更极致的 Go 代码。