我们要讨论的第一个主题是标识符。 标识符是一个用来表示名称的花哨单词; 变量的名称,函数的名称,方法的名称,类型的名称,包的名称等。
Poor naming is symptomatic of poor design. (命名不佳是设计不佳的症状。) — Dave Cheney
鉴于 Go 语言的语法有限,我们为程序选择的名称对我们程序的可读性产生了非常大的影响。 可读性是良好代码的定义质量,因此选择好名称对于 Go 代码的可读性至关重要。
# 2.1. 选择标识符是为了清晰,而不是简洁
Obvious code is important. What you can do in one line you should do in three. (清晰的代码很重要。在一行可以做的你应当分三行做。(
if/else
吗?)) — Ukiah Smith
Go 语言不是为了单行而优化的语言。 Go 语言不是为了最少行程序而优化的语言。我们没有优化源代码的大小,也没有优化输入所需的时间。
Good naming is like a good joke. If you have to explain it, it’s not funny. (好的命名就像一个好笑话。如果你必须解释它,那就不好笑了。) — Dave Cheney
清晰的关键是在 Go 语言程序中我们选择的标识名称。让我们谈一谈所谓好的名字:
好的名字很简洁。 好的名字不一定是最短的名字,但好的名字不会浪费在无关的东西上。好名字具有高的信噪比。
好的名字是描述性的。 好的名字会描述变量或常量的应用,而不是它们的内容。好的名字应该描述函数的结果或方法的行为,而不是它们的操作。好的名字应该描述包的目的而非它的内容。描述东西越准确的名字就越好。
好的名字应该是可预测的。 你能够从名字中推断出使用方式。~这是选择描述性名称的功能,但它也遵循传统。~这是 Go 程序员在谈到习惯用语时所谈论的内容。
让我们深入讨论以下这些属性。
# 2.2. 标识符长度
有时候人们批评 Go 语言推荐短变量名的风格。正如 Rob Pike 所说,“ Go 程序员想要正确的长度的标识符”。 [1] (opens new window)
Andrew Gerrand 建议通过对某些事物使用更长的标识,向读者表明它们具有更高的重要性。
The greater the distance between a name’s declaration and its uses, the longer the name should be. (名字的声明与其使用之间的距离越大,名字应该越长。) — Andrew Gerrand [2] (opens new window)
由此我们可以得出一些指导方针:
- 短变量名称在声明和上次使用之间的距离很短时效果很好。
- 长变量名称需要证明自己的合理性; 名称越长,需要提供的价值越高。冗长的名称与页面上的重量相比,信号量较小。
- 请勿在变量名称中包含类型名称。
- 常量应该描述它们持有的值,而不是该如何使用。
- 对于循环和分支使用单字母变量,参数和返回值使用单个字,函数和包级别声明使用多个单词
- 方法、接口和包使用单个词。
- 请记住,包的名称是调用者用来引用名称的一部分,因此要好好利用这一点。
我们来举个栗子:
type Person struct {
Name string
Age int
}
// AverageAge returns the average age of people.
func AverageAge(people []Person) int {
if len(people) == 0 {
return 0
}
var count, sum int
for _, p := range people {
sum += p.Age
count += 1
}
return sum / count
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在此示例中,变量 p
的在第 10
行被声明并且也只在接下来的一行中被引用。 p
在执行函数期间存在时间很短。如果要了解 p
的作用只需阅读两行代码。
相比之下,people
在函数第 7
行参数中被声明。sum
和 count
也是如此,他们用了更长的名字。读者必须查看更多的行数来定位它们,因此他们名字更为独特。
我可以选择 s
替代 sum
以及 c
(或可能是 n
)替代 count
,但是这样做会将程序中的所有变量份量降低到同样的级别。我可以选择 p
来代替 people
,但是用什么来调用 for ... range
迭代变量。如果用 person
的话看起来很奇怪,因为循环迭代变量的生命时间很短,其名字的长度超出了它的值。
贴士: 与使用段落分解文档的方式一样用空行来分解函数。 在
AverageAge
中,按顺序共有三个操作。 第一个是前提条件,检查people
是否为空,第二个是sum
和count
的累积,最后是平均值的计算。
# 2.2.1. 上下文是关键
重要的是要意识到关于命名的大多数建议都是需要考虑上下文的。 我想说这是一个原则,而不是一个规则。
两个标识符 i
和 index
之间有什么区别。 我们不能断定一个就比另一个好,例如
for index := 0; index < len(s); index++ {
//
}
2
3
从根本上说,上面的代码更具有可读性
for i := 0; i < len(s); i++ {
//
}
2
3
我认为它不是,因为就此事而论, i
和 index
的范围很大可能上仅限于 for 循环的主体,后者的额外冗长性(指 index
)几乎没有增加对于程序的理解。
但是,哪些功能更具可读性?
func (s *SNMP) Fetch(oid []int, index int) (int, error)
或
func (s *SNMP) Fetch(o []int, i int) (int, error)
在此示例中,oid
是 SNMP
对象 ID
的缩写,因此将其缩短为 o
意味着程序员必须要将文档中常用符号转换为代码中较短的符号。 类似地将 index
替换成 i
,模糊了 i
所代表的含义,因为在 SNMP
消息中,每个 OID
的子值称为索引。
贴士: 在同一声明中长和短形式的参数不能混搭。
# 2.3. 不要用变量类型命名你的变量
你不应该用变量的类型来命名你的变量, 就像您不会将宠物命名为“狗”和“猫”。 出于同样的原因,您也不应在变量名字中包含类型的名字。
变量的名称应描述其内容,而不是内容的类型。 例如:
var usersMap map[string]*User
这个声明有什么好处? 我们可以看到它是一个 map
,它与 *User
类型有关。 但是 usersMap
是一个 map
,而 Go 语言是一种静态类型的语言,如果没有定义变量,不会让我们意外地使用到它,因此 Map
后缀是多余的。
接下来, 如果我们像这样来声明其他变量:
var (
companiesMap map[string]*Company
productsMap map[string]*Products
)
2
3
4
usersMap
,companiesMap
和 productsMap
三个 map
类型变量,所有映射字符串都是不同的类型。 我们知道它们是 map
,我们也知道我们不能使用其中一个来代替另一个 - 如果我们在需要 map[string]*User
的地方尝试使用 companiesMap
, 编译器将抛出错误异常。 在这种情况下,很明显变量中 Map
后缀并没有提高代码的清晰度,它只是增加了要输入的额外样板代码。
我的建议是避免使用任何类似变量类型的后缀。
贴士: 如果
users
的描述性都不够用,那么usersMap
也不会。
此建议也适用于函数参数。 例如:
type Config struct {
//
}
func WriteConfig(w io.Writer, config *Config)
2
3
4
5
命名 *Config
参数 config
是多余的。 我们知道它是 *Config
类型,就是这样。
在这种情况下,如果变量的生命周期足够短,请考虑使用 conf
或 c
。
如果有更多的 *Config
,那么将它们称为 original
和 updated
比 conf1
和 conf2
会更具描述性,因为前者不太可能被互相误解。
贴士: 不要让包名窃取好的变量名。 导入标识符的名称包括其包名称。 例如,
context
包中的Context
类型将被称为context.Context
。 这使得无法将context
用作包中的变量或类型。
func WriteLog(context context.Context, message string)
上面的栗子将会编译出错。 这就是为什么
context.Context
类型的通常的本地声明是ctx
,例如:
func WriteLog(ctx context.Context, message string)
# 2.4. 使用一致的命名方式
一个好名字的另一个属性是它应该是可预测的。 在第一次遇到该名字时读者就能够理解名字的使用。 当他们遇到常见的名字时,他们应该能够认为自从他们上次看到它以来它没有改变意义。
例如,如果您的代码在处理数据库请确保每次出现参数时,它都具有相同的名称。 与其使用 d * sql.DB
,dbase * sql.DB
,DB * sql.DB
和 database * sql.DB
的组合,倒不如统一使用:
db *sql.DB
这样做使读者更为熟悉; 如果你看到db
,你知道它就是 *sql.DB
并且它已经在本地声明或者由调用者为你提供。
类似地,对于方法接收器: 在该类型的每个方法上使用相同的接收者名称。 在这种类型的方法内部可以使读者更容易使用。
注意: Go 语言中的短接收者名称惯例与目前提供的建议不一致。 这只是早期做出的选择之一,已经成为首选的风格,就像使用
CamelCase
而不是snake_case
一样。
贴士: Go 语言样式规定接收器具有单个字母名称或从其类型派生的首字母缩略词。 你可能会发现接收器的名称有时会与方法中参数的名称冲突。 在这种情况下,请考虑将参数名称命名稍长,并且不要忘记一致地使用此新参数名称。
最后,某些单字母变量传统上与循环和计数相关联。 例如,i
,j
和 k
通常是简单 for
循环的循环归纳变量。n
通常与计数器或累加器相关联。v
是通用编码函数中值的常用简写,k
通常用于 map
的键,s
通常用作字符串类型参数的简写。
与上面的 db
示例一样,程序员认为 i
是一个循环归纳变量。 如果确保 i
始终是循环变量,而且不在 for
循环之外的其他地方中使用。 当读者遇到一个名为 i
或 j
的变量时,他们知道循环就在附近。
贴士: 如果你发现自己有如此多的嵌套循环,
i
,j
和k
变量都无法满足时,这个时候可能就是需要将函数分解成更小的函数。
# 2.5. 使用一致的声明样式
Go 至少有六种不同的方式来声明变量
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)
2
3
4
5
6
var
表示此变量已被声明为指定类型的零值。 这也与使用 var
而不是短声明语法在包级别声明变量的要求一致 - 尽管我稍后会说你根本不应该使用包级变量。
- 在声明和初始化时,使用
:=
。 在同时声明和初始化变量时,也就是说我们不会将变量初始化为零值,我建议使用短变量声明。 这使得读者清楚地知道:=
左侧的变量是初始化过的。
为了解释原因,让我们看看前面的例子,但这次是初始化每个变量:
var players int = 0
var things []Thing = nil
var thing *Thing = new(Thing)
json.Unmarshall(reader, thing)
2
3
4
5
6
在第一个和第三个例子中,因为在 Go 语言中没有从一种类型到另一种类型的自动转换; 赋值运算符左侧的类型必须与右侧的类型相同。 编译器可以从右侧的类型推断出声明的变量的类型,上面的例子可以更简洁地写为:
var players = 0
var things []Thing = nil
var thing = new(Thing)
json.Unmarshall(reader, thing)
2
3
4
5
6
我们将 players
初始化为 0
,但这是多余的,因为 0
是 players
的零值。 因此,要明确地表示使用零值, 我们将上面例子改写为:
var players int
第二个声明如何? 我们不能省略类型而写作:
var things = nil
因为 nil
没有类型。 [2] (opens new window)相反,我们有一个选择,如果我们要使用切片的零值则写作:
var things []Thing
或者我们要创建一个有零元素的切片则写作:
var things = make([]Thing, 0)
如果我们想要后者那么这不是切片的零值,所以我们应该向读者说明我们通过使用简短的声明形式做出这个选择:
things := make([]Thing, 0)
这告诉读者我们已选择明确初始化事物。
下面是第三个声明,
var thing = new(Thing)
既是初始化了变量又引入了一些 Go 程序员不喜欢的 new
关键字的罕见用法。 如果我们用推荐地简短声明语法,那么就变成了:
thing := new(Thing)
这清楚地表明 thing
被初始化为 new(Thing)
的结果 - 一个指向 Thing
的指针 - 但依旧我们使用了 new
地罕见用法。 我们可以通过使用紧凑的文字结构初始化形式来解决这个问题,
thing := &Thing{}
与 new(Thing)
相同,这就是为什么一些 Go 程序员对重复感到不满。 然而,这意味着我们使用指向 Thing{}
的指针初始化了 thing
,也就是 Thing
的零值。
相反,我们应该认识到 thing
被声明为零值,并使用地址运算符将 thing
的地址传递给 json.Unmarshall
var thing Thing
json.Unmarshall(reader, &thing)
2
贴士: 当然,任何经验法则,都有例外。 例如,有时两个变量密切相关,这样写会很奇怪:
var min int
max := 1000
2
如果这样声明可能更具可读性
min, max := 0, 1000
综上所述:
在没有初始化的情况下声明变量时,请使用 var
语法。
声明并初始化变量时,请使用 :=
。
贴士: 使复杂的声明显而易见。 当事情变得复杂时,它看起来就会很复杂。例如
var length uint32 = 0x80
这里
length
可能要与特定数字类型的库一起使用,并且length
明确选择为uint32
类型而不是短声明形式:
length := uint32(0x80)
在第一个例子中,我故意违反了规则, 使用
var
声明带有初始化变量的。 这个决定与我的常用的形式不同,这给读者一个线索,告诉他们一些不寻常的事情将会发生。
# 2.6. 成为团队合作者
我谈到了软件工程的目标,即编写可读及可维护的代码。 因此,您可能会将大部分职业生涯用于你不是唯一作者的项目。 我在这种情况下的建议是遵循项目自身风格。
在文件中间更改样式是不和谐的。 即使不是你喜欢的方式,对于维护而言一致性比你的个人偏好更有价值。 我的经验法则是: 如果它通过了 gofmt
,那么通常不值得再做代码审查。
贴士: 如果要在代码库中进行重命名,请不要将其混合到另一个更改中。 如果有人使用
git bisect
,他们不想通过数千行重命名来查找您更改的代码。