Effective-Go中文翻译

原文链接,省略了一些无关的句子。

这里有别人翻译好的,需要翻墙。

介绍

Go是一种新的语言,尽管它从其他语言中借鉴了很多特性,但有些不同寻常的特性让Go语言程序和其他程序有所不同。想要直接把C++或者Java代码转换成Go不会得到令人满意的结果,Java程序是用Java写的而不是GO。另一方面,从GO的角度来考虑问题可以写出成功运行但略有不同的程序。换句话说,想要写出好的GO程序,了解其特性是很重要的。同样,了解约定俗成的惯例也很重要,比如命名、格式、项目结构,这样你写的代码才能方便其他GO语言程序员理解。

这篇文档给你一些小建议以便写出简洁且符合规范的GO代码。看这篇文档前建议先看language specificationthe Tour of Go,和 How to Write Go Code

例子

GO源码不仅仅是作为核心库来使用,更可以用作写代码的实例参考。此外,你可以直接在官网上运行那些没有额外依赖的程序,比如这个。如果你有关于如何解决或实现某个问题的疑问,标准库中的文档、代码、例子可以提供答案或者思路。

代码风格

代码风格是一个重要而且很有争议的问题,人们虽然可以适应多种代码风格,但这最好避免这样。如果大家的风格一致就可以把时间投入到更重要的地方。问题是如何统一所有人的风格而不需要冗长的风格说明文档。

在GO中我们提供了一种不常见的方法并且让机器去处理常见的代码格式化问题。gofmt命令(也可以使用go fmt,这个命令作用于包而不是源文件)用于读取源文件并且将其使用标准代码风格缩进,如果有需要的话也会格式化注释。

如下面这个例子,不需要花时间去格式化结构体中存在的注释,gofmt会帮你完成:

type T struct {
    name string // name of the object
    value int // its value
}

执行gomft后:

type T struct {
    name    string // name of the object
    value   int    // its value
}

标准库中的所有代码都是经过gofmt处理过的。

一些格式化的细节如下,很简短:

  1. 缩进:gofmt默认使用tabs缩进,如果必须的话才使用空格。
  2. 行长度:GO没有代码行长度限制。如果一个行过长,可以另起一行并添加额外的tab。
  3. 括号:GO比C和Java需要的括号少很多,控制结构中(if for switch)不需要有括号,操作符号优先级更短更清晰,比如:
x<<8 + y<<16

间隔就意味着操作顺序。

注释

GO提供了/**/这样的块注释和//这样的行注释。行注释是最常用的,块注释通常应用在包说明注释、有用的表达式和禁用大块代码时。

godoc命令将从源码中提取关于这个包的文档。最先出现的评论将被作为文件的说明文档被提取出来,这部分评论的风格和质量决定了godoc命令产生文档质量的好坏。

每一个包都应该有个说明,这个说明使用块评论并且在包声明语句之前。对于有多个源文件的包,包说明仅需要出现在一个文件中。包说明应该包含简介以及相关的全部信息。这些信息将出现在godoc的首页并且有详细说明的链接。

/*
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

如果包很简单,则可以使用简短的注释说明。

// Package path implements utility routines for
// manipulating slash-separated filename paths.

注释不需要额外的格式,生成的输出可能都不是固定宽度,所以不要依赖间距这种对齐方式,godoc和gofmt会自动处理。注释是纯文本的格式,所以HTML或者其他格式类似_this_这样的不应该使用。godoc一个作用就是调整程序代码缩进为固定宽度。fmt package这个包的说明就是这样处理的,效果很好。

根据不同情况,godoc可能不会重新格式化注释,所以确保他们看起来好看:使用正确的拼写、标点、句子结构,正确的折行等等。

在包内部,任何出现在紧贴函数声明上面的评论都会作为函数的说明文档。每个对外暴露的函数(大写字母开头)都应该有说明文档。

文档说明应该使用完整的句子,第一句应该是一个完整的摘要,并且以函数名开头。

// 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) {}

如果每个函数说明都是以函数描述为开头,godoc的输出就可以使用grep命令进行过滤。你可能不记得“Compile”但要为正则表达式寻找解析函数,则可以使用下面的命令:

godoc regexp | grep -i parse

如果所有的文档都在包开头的地方,“This function…",grep不能帮你机器函数名,但由于每个包名都在包起始说明的地方,你可能看到如下输出,有助于回忆起寻找的东西:

$ godoc regexp | grep parse
    Compile parses a regular expression and returns, if successful, a Regexp
    parsed. It simplifies safe initialization of global variables holding
    cannot be parsed. It simplifies safe initialization of global variables

GO语法准许以组为单位进行声明,一个单一的文档说明可以介绍这个组的常量或者变量。正因如此,说明可以简短一些:

// 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 ')'")
    ...
)

分组声明也可以表明组成员之间的关系,比如下面这个例子,变量由一个互斥锁保护:

var (
    countLock   sync.Mutex
    inputCount  uint32
    outputCount uint32
    errorCount  uint32
)

命名规范

命名在GO中十分重要,甚至还有语义上的效果:名字开头字母是不是大写还决定了一个变量是否能被外部引用。所以花费一些时间讨论命名规范是十分值得的。

包名

当包被引入后,可以通过包名访问其内容。在

import "bytes"

后,就可以调用bytes.Buffer了,所有人都通过相同的方式引用其内容是十分有益的,也就是说包名应该尽可能的简洁明了。按照惯例,包名应该小写且使用单个单词,不包含下划线或驼峰命名法。Err包就十分简洁,任何使用它的人都会输入这个名字。不用担心包引入顺序的问题,包名就是引入时候的默认名。包名不需要在所有源码中都保持唯一,少数冲突的情况下可以在局部使用别名引用。不论如何,冲突都是很少见的,因为文件名就决定了哪一个包被使用。

另一个约定就是包名应为其源码目录的基本名称。在 src/pkg/encoding/base64 中的包应作为 encoding/base64 导入,其包名应为 base64, 而非 encoding_base64encodingBase64

包的导入者可通过包名来引用其内容,因此包中的可导出名称以此来避免冲突。(请勿使用 import . 记法,它可以简化必须在被测试包外运行的测试, 除此之外应尽量避免使用。)例如,bufio包中的缓存读取器类型叫做Reader而非BufReader,因为用户将它看做bufio.Reader,这是个清楚而简洁的名称。 此外,由于被导入的项总是通过它们的包名来确定,因此bufio.Reader不会与io.Reader发生冲突。同样,用于创建ring.Ring的新实例的函数(这就是Go中的构造函数)一般会称之为NewRing,但由于Ring是该包所导出的唯一类型,且该包也叫ring,因此它可以只叫做New,它跟在包的后面,就像ring.New

另一个简短的例子是once.Do,once.Do(setup)表述足够清晰, 使用once.DoOrWaitUntilDone(setup)完全多余。 长命名并不会使其更具可读性,一份有用的说明文档通常比额外的长名更有价值。

Getters

GO中不直接提供Getters和Setters,如果需要可以自己实现。把Get这3个字母放入Getters命名中不合习惯也没必要。如果你有一个叫做owner(小写,不能被外部引用)的变量,getter方法最好叫做Owner(大写,可以被外部引用),而不是GetOwner。大写字母即为可导出的这种规定为区分方法和字段提供了便利。若要提供Setter函数,SetOwner 是个不错的选择。两个命名看起来都很合理:

owner := obj.Owner()
if owner != user {
    obj.SetOwner(user)
}

接口名

按惯例,只包含一个方法的接口应当以该方法的名称加上-er后缀来命名,如Reader、Writer、Formatter、CloseNotifier等。

诸如此类的命名有很多,遵循它们及其代表的函数名会让事情变得简单。 Read、Write、Close、Flush、 String 等都具有典型的签名和意义。为避免冲突,请不要用这些名称为你的方法命名, 除非你明确知道它们的签名和意义相同。反之,若你的类型实现了的方法, 与一个众所周知的类型的方法拥有相同的含义,那就使用相同的命名。 请将字符串转换方法命名为 String 而非 ToString。

驼峰命名法

最后,Go中约定使用驼峰记法 MixedCapsmixedCaps,而不是下划线。

未完待续……