database *sql.DB
,还不如都统一为:
db *sql.DB
这样做可以增进熟悉度:如果您看到db
,那么您就知道那是个*sql.DB
,并且已经在本地定义或者由调用者提供了。
对于方法接收者也类似,在类型的每个方法中使用相同的接收者名称,这样可以让阅读者在跨方法阅读和理解时更容易主观推断。
Austin Luo:“接收者”是一种特殊类型的参数。^2 比如func (b *Buffer) Read(p []byte) (n int, err error)
,它通常只用一到两个字母来表示,但在不同的方法中仍然应当保持一致。 注意:Go 中对接收者的短命名规则惯例与目前提供的建议不一致。这只是早期做出的选择之一,并且已经成为首选的风格,就像使用CamelCase
而不是snake_case
一样。 小窍门:Go 的命名风格规定接收器具有单个字母名称或其派生类型的首字母缩略词。有时您可能会发现接收器的名称有时会与方法中参数的名称冲突,在这种情况下,请考虑使参数名称稍长,并且仍然不要忘记一致地使用这个新名称。
最后,某些单字母变量传统上与循环和计数有关。例如,i
,j
,和k
通常是简单的for
循环变量。n
通常与计数器或累加器有关。 v
通常是某个值的简写,k
通常用于映射的键,s
通常用作string
类型参数的简写。
与上面db
的例子一样,程序员期望i
是循环变量。如果您保证i
始终是一个循环变量——而不是在for
循环之外的情况下使用,那么当读者遇到一个名为i
或者j
的变量时,他们就知道当前还在循环中。
小窍门:如果您发现在嵌套循环中您都使用完i
,j
,k
了,那么很显然这已经到了将函数拆得更小的时候了。
使用一致的声明风格
Go 中至少有 6 种声明变量的方法(Austin Luo:作者说了 6 种,但只列了 5 种)
var x int = 1
var x = 1
var x int; x = 1
var x = int(1)
x := 1
我敢肯定还有更多我没想到的。这是 Go 的设计师认识到可能是一个错误的地方,但现在改变它为时已晚。有这么多不同的方式来声明变量,那么我们如何避免每个 Go 程序员选择自己个性独特的声明风格呢?
我想展示一些在我自己的程序里声明变量的建议。这是我尽可能使用的风格。
- 只声明,不初始化时,使用*
var
*。在声明之后,将会显式地初始化时,使用var
关键字。
var players int // 0
var things []Thing // an empty slice of Things
var thing Thing // empty Thing struct
json.Unmarshall(reader, &thing)
var
关键字表明这个变量被有意地声明为该类型的零值。这也与在包级别声明变量时使用var
而不是短声明语法(Austin Luo::=
)的要求一致——尽管我稍后会说您根本不应该使用包级变量。
- 既声明,也初始化时,使用*
:=
*。当同时要声明和初始化变量时,换言之我们不让变量隐式地被初始化为零值时,我建议使用短声明语法的形式。这使得读者清楚地知道:=
左侧的变量是有意被初始化的。
为解释原因,我们回头再看看上面的例子,但这一次每个变量都被有意初始化了:
var players int = 0
var things []Thing = nil
var thing *Thing = new(Thing)
json.Unmarshall(reader, thing)
第一个和第三个示例中,因为 Go 没有从一种类型到另一种类型的自动转换,赋值运算符左侧和右侧的类型必定是一致的。编译器可以从右侧的类型推断出左侧所声明变量的类型。对于这个示例可以更简洁地写成这样:
var players = 0
var things []Thing = nil
var thing = new(Thing)
json.Unmarshall(reader, thing)
由于0
是players
的零值,因此为players
显式地初始化为0
就显得多余了。所以为了更清晰地表明我们使用了零值,应该写成这样:
var players int
那第二条语句呢?我们不能忽视类型写成:
var things = nil
因为nil
根本就没有类型^2。相反,我们有一个选择,我们是否希望切片的零值?
var things []Thing
或者我们是否希望创建一个没有元素的切片?
var things = make([]Thing, 0)
如果我们想要的是后者,这不是个切片类型的零值,那么我们应该使用短声明语法让阅读者很清楚地明白我们的选择:
things := make([]Thing, 0)
这告诉了读者我们显式地初始化了things
。
再来看看第三个声明:
var thing = new(Thing)
这既显式地初始化了变量,也引入了 Go 程序员不喜欢而且很不常用的new
关键字。如果我们遵循短命名语法的建议,那么这句将变成:
thing := new(Thing)
这很清楚地表明,thing
被显式地初始化为new(Thing)
的结果——一个指向Thing
的指针——但仍然保留了我们不常用的new
。我们可以通过使用紧凑结构初始化的形式来解决这个问题,
thing := &Thing{}
这和new(Thing)
做了同样的事——也因此很多 Go 程序员对这种重复感觉不安。不过,这一句仍然意味着我们为thing
明确地初始化了一个Thing{}
的指针——一个Thing
的零值。
在这里,我们应该意识到,thing
被初始化为了零值,并且将它的指针地址传递给了json.Unmarshall
:
var thing Thing
json.Unmarshall(reader, &thing)
注意:当然,对于任何经验法则都有例外。比如,有些变量之间很相关,那么与其写成这样: var min int max := 1000 不如写成这样更具可读性: min, max := 0, 1000
综上所述:
- 只声明,不初始化时,使用
var
。
- 既声明,也显式地初始化时,使用
:=
。
小窍门:使得机巧的声明更加显而易见。 当某件事本身很复杂时,应当使它看起来就复杂。 var length uint32 = 0x80
这里的length
可能和一个需要有特定数字类型的库一起使用,并且length
被很明确地指定为uint32
类型而不只是短声明形式: length := uint32(0x80)
在第一个例子中,我故意违反了使用var
声明形式和显式初始化程序的规则。这个和我惯常形式不同的决定,可以让读者意识到这里需要注意。
成为团队合作者
我谈到了软件工程的目标,即生成可读,可维护的代码。而您的大部分职业生涯参与的项目可能您都不是唯一的作者。在这种情况下我的建议是遵守团队的风格。
在文件中间改变编码风格是不适合的。同样,即使您不喜欢,可维护性也比您的个人喜好有价值得多。我的原则是:如果满足gofmt
,那么通常就不值得再进行代码风格审查了。
小窍门:如果您要横跨整个代码库进行重命名,那么不要在其中混入其他的修改。如果其他人正在使用 git bisect,他们一定不愿意从几千行代码的重命名中“跋山涉水”地去寻找您别的修改。
代码注释
在我们进行下一个更大的主题之前,我想先花几分钟说说注释的事。
Good code has lots of comments, bad code requi