> 本文由Austin发表
# 指导原则
我们要谈论在一个编程语言中的最佳实践,那么我们首先应该明确什么是“最佳”。如果您们听了我昨天那场讲演的话,您一定看到了来自 Go 团队的 Russ Cox 讲的一句话:
> 软件工程,是您在编程过程中增加了工期或者开发人员之后发生的那些事。 — Russ Cox
Russ 是在阐述软件“编程”和软件“工程”之间的区别,前者是您写的程序,而后者是一个让更多的人长期使用的产品。软件工程师会来来去去地更换,团队也会成长或者萎缩,需求也会发生变化,新的特性也会增加,bug 也会被修复,这就是软件“工程”的本质。
我可能是现场最早的 Go 语言用户,但与其说我的主张来自我的资历,不如说我今天讲的是真实来自于 Go 语言本身的指导原则,那就是:
1. 简单性
2. 可读性
3. 生产率
> 您可能已经注意到,我并没有提**性能**或者**并发性**。实际上有不少的语言执行效率比 Go 还要高,但它们一定没有 Go 这么简单。有些语言也以**并发性**为最高目标,但它们的可读性和生产率都不好。 **性能**和**并发性**都很重要,但它们不如**简单性**、**可读性**和**生产率**那么重要。
## 简单性
为什么我们要力求简单,为什么简单对 Go 语言编程如此重要?
我们有太多的时候感叹“这段代码我看不懂”,是吧?我们害怕修改一丁点代码,生怕这一点修改就导致其他您不懂的部分出问题,而您又没办法修复它。
这就是复杂性。复杂性把可读的程序变得不可读,复杂性终结了很多软件项目。
简单性是 Go 的最高目标。无论我们写什么程序,我们都应该能一致认为它应当简单。
## 可读性
> Readability is essential for maintainability. — Mark Reinhold, JVM language summit 2018 可读性对于可维护性至关重要。
为什么 Go 代码的可读性如此重要?为什么我们应该力求可读性?
> Programs must be written for people to read, and only incidentally for machines to execute. — Hal Abelson and Gerald Sussman, Structure and Interpretation of Computer Programs 程序应该是写来被人阅读的,而只是顺带可以被机器执行。
可阅读性对所有的程序——不仅仅是 Go 程序,都是如此之重要,是因为程序是人写的并且给其他人阅读的,事实上被机器所执行只是其次。
代码被阅读的次数,远远大于被编写的次数。一段小的代码,在它的整个生命周期,可能被阅读成百上千次。
> The most important skill for a programmer is the ability to effectively communicate ideas. — Gastón Jorquera ^1 程序员最重要的技能是有效沟通想法的能力。
可读性是弄清楚一个程序是在做什么事的关键。如果您都不知道这个程序在做什么,您如何去维护这个程序?如果一个软件不可用被维护,那就可能被重写,并且这也可能是您公司最后一次在 GO 上面投入了。
如果您仅仅是为自己个人写一个程序,可能这个程序是一次性的,或者使用这个程序的人也只有您一个,那您想怎样写就怎样写。但如果是多人合作贡献的程序,或者因为它解决人们的需求、满足某些特性、运行它的环境会变化,而在一个很长的时间内被很多人使用,那么程序的**可维护性**则必须成为目标。
编写可维护的程序的第一步,那就是确保代码是可读的。
## 生产率
> Design is the art of arranging code to work today, and be changeable forever. — Sandi Metz 设计是一门艺术,要求编写的代码当前可用,并且以后仍能被改动。
我想重点阐述的最后一个基本原则是**生产率**。开发者的生产率是一个复杂的话题,但归结起来就是:为了有效的工作,您因为一些工具、外部代码库而浪费了多少时间。Go 程序员应该感受得到,他们在工作中可以从很多东西中受益了。(Austin Luo:言下之意是,Go 的工具集和基础库完备,很多东西触手可得。)
有一个笑话是说,Go 是在 C++ 程序编译过程中被设计出来的。快速的编译是 Go 语言用以吸引新开发者的关键特性。编译速度仍然是一个不变的战场,很公平地说,其他语言需要几分钟才能编译,而 Go 只需要几秒即可完成。这有助于 Go 开发者拥有动态语言开发者一样的高效,但却不会面临那些动态语言本身可靠性的问题。
Go 开发者意识到代码是写来被阅读的,并且把**阅读**放在**编写**之上。Go 致力于从工具集、习惯等方面强制要求代码必须编写为一种特定样式,这消除了学习项目特定术语的障碍,同时也可以仅仅从“看起来”不正确即可帮助开发者发现潜在的错误。
Go 开发者不会整日去调试那些莫名其妙的编译错误。他们也不会整日浪费时间在复杂的构建脚本或将代码部署到生产中这事上。更重要的是他们不会花时间在尝试搞懂同事们写的代码是什么意思这事上。
当 Go 语言团队在谈论一个语言必须扩展时,他们谈论的就是生产率。
# 标识符
我们要讨论的第一个议题是标识符。标识符是一个名称的描述词,这个名称可以是一个变量的名称、一个函数的名称、一个方法的名称、一个类型的名称或者一个包的名称等等。
> Poor naming is symptomatic of poor design. — Dave Cheney 拙劣的名称是拙劣的设计的表征。
鉴于 Go 的语法限制,我们为程序中的事物选择的名称对我们程序的可读性产生了过大的影响。良好的可读性是评判代码质量的关键,因此选择好名称对于 Go 代码的可读性至关重要。
## 选择清晰的名称,而不是简洁的名称
> Obvious code is important. What you can do in one line you should do in three. — 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 开发者在谈论**惯用语**时,即是说的这个。
接下来让我们深入地讨论一下。
## 标识符长度
有时候人们批评 Go 风格推荐短变量名。正如 Rob Pike 所说,“Go 开发者想要的是合适长度的标识符”。^1
Andrew Gerrand 建议通过使用更长的标识符向读者暗示它们具有更高的重要性。
> The greater the distance between a name’s declaration and its uses, the longer the name should be. — Andrew Gerrand ^2 标识符的声明和使用间隔越远,名称的长度就应当越长。
据此,我们可以归纳一些指导意见:
- 短变量名称在声明和上次使用之间的距离很短时效果很好。
- 长变量名需要证明其不同的合理性:越长的变量名,越需要更多的理由来证明其合理。冗长、繁琐的名称与他们在页面上的权重相比,携带的信息很低。
- 不要在变量名中包含其类型的名称。
- 常量需要描述其存储的值的含义,而不是怎么使用它。
- 单字母变量可用于循环或逻辑分支,单词变量可用于参数或返回值,多词短语可用于函数和包这一级的声明。
- 单词可用于方法、接口和包
- 请记住,包的命名将成为用户引用它时采用的名称,确保这个名称更有意义。
让我们来看一个示例:
```js
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
}
```
在这个示例中,范围变量`p`在定义之后只在接下来的一行使用。`p`在整页源码和函数执行过程中都只生存一小段时间。对`p`感兴趣的读者只需要查看两行代码即可。
与之形成对比的是,变量`people`在函数参数中定义,并且存在了 7 行,同理的还有`sum`和`count`,这他们使用了更长的名称,读者必须关注更广泛的代码行。
我也可以使用`s`而不是`sum`,用`c`(或`n`)而不是`count`,但这会将整个程序中的变量都聚集在相同的重要性上。我也可以使用`p`而不是`people`,但是这样又有一个问题,那就是`for ... range`循环中的变量又用什么?单数的 `person` 看起来也很奇怪,生存时间极短命名却比导出它的那个值更长。
> Austin Luo:这里说的是,若数组`people`用变量名`p`,那么从数组中获取的每一个元素取名就成了问题,比如用`person`,即使使用`person`看起来也很奇怪,一方面是单数,一方面`person`的生存周期只有两行(很短),命名比生存周期更长的`p`(`people`)还长了。 小窍门:跟使用空行在文档中分段一样,使用空行将函数执行过程分段。在函数`AverageAge`中有按顺序的三个操作。第一个是先决条件,检查当`people`为空时我们不会除零,第二个是累加总和和计数,最后一个是计算平均数。
### 上下文是关键
绝大多数的命名建议都是根据上下文的,意识到这一点很重要。我喜欢称之为原则,而不是规则。
`i`和`index` 这两个标识符有什么不同?我们很难确切地说其中一个比另一个好,比如:
```js
for index := 0; index < len(s); index++ {
//
}
```
上述代码的可读性,基本上都会认为比下面这段要强:
```js
for i := 0; i < len(s); i++ {
//
}
```
但我表示不赞同。因为无论是`i`还是`index`,都是限定于`for`循环体的,更冗长的命名,并没有让我们**更容易**地理解这段代码。
话说回来,下面两段代码那一段可读性更强呢?
```js
func (s *SNMP) Fetch(oid []int, index int) (int, error)
```
或者
```js
func (s *SNMP) Fetch(o []int, i int) (int, error)
```
在这个示例中,`oid`是`SNMP`对象 ID 的缩写,因此将其略写为 `o` 意味着开发者必须将他们在文档中看到的常规符号转换理解为代码中更短的符号。同样地,将`index`简略为`i`,减少了其作为`SNMP`消息的索引的含义。
> 小窍门:在参数声明中不要混用长、短不同的命名风格。
## 命名中不要包含所属类型的名称
正如您给宠物取名一样,您会给狗取名“汪汪”,给猫取名为“咪咪”,但不会取名为“汪汪狗”、“咪咪猫”。出于同样的原因,您也不应在变量名称中包含其类型的名称。
变量命名应该体现它的内容,而不是类型。我们来看下面这个例子:
```js
var usersMap map[string]*User
```
这样的命名有什么好处呢?我们能知道它是个 map,并且它与`*User`类型有关,这可能还不错。但是 Go 作为一种静态类型语言,它并不会允许我们在需要标量变量的地方意外地使用到这个变量,因此`Map`后缀实际上是多余的。
现在我们来看像下面这样定义变量又是什么情况:
```js
var (
companiesMap map[string]*Company
productsMap map[string]*Products
)
```
现在这个范围内我们有了三个 map 类型的变量了:`usersMap`,`companiesMap`,以及 `productsMap`,所有这些都从字符串映射到了不同的类型。我们知道它们都是 map,我们也知道它们的 map 声明会阻止我们使用一个代替另一个——如果我们尝试在需要`map[string]*User`的地方使用`companiesMap`,编译器将抛出错误。在这种情况下,很明显`Map`后缀不会提高代码的清晰度,它只是编程时需要键入的冗余内容。(Austin Luo:陈旧的思维方式)
我的建议是,避免给变量加上与类型相关的任何后缀。
> 小窍门:如果`users`不能描述得足够清楚,那`usersMap`也一定不能。
这个建议也适用于函数参数,比如:
```js
type Config struct {
//
}
func WriteConfig(w io.Writer, config *Config)
```
将`*Config`参数命名为`config`是多余的,我们知道它是个`*Config`,函数签名上写得很清楚。
在这种情况建议考虑`conf`或者`c`——如果生命周期足够短的话。
如果在一个范围内有超过一个`*Config`,那命名为`conf1`、`conf2`的描述性就比`original`、`updated`更差,而且后者比前者更不容易出错。
> NOTE:**不要让包名占用了更适合变量的名称。** 导入的标识符是会包含它所属包的名称的。 例如我们很清楚`context.Context`是包`context`中的类型`Context`。这就导致我们在我们自己的包里,再也无法使用`context`作为变量或类型名了。 `func WriteLog(context context.Context, message string)` 这无法编译。这也是为什么我们通常将`context.Context`类型的变量命名为`ctx`的原因,如: `func WriteLog(ctx context.Context, message string)`
## 使用一致的命名风格
一个好名字的另一个特点是它应该是可预测的。阅读者应该可以在第一次看到的时候就能够理解它如何使用。如果遇到一个约定俗称的名字,他们应该能够认为和上次看到这个名字一样,一直以来它都没有改变意义。
例如,如果您要传递一个数据库句柄,请确保每次的参数命名都是一样的。与其使用`d *sql.DB`,`dbase *sql.DB`,`DB *sql.DB`和`database *sql.DB`,还不如都统一为:
```js
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`关键字。
```js
```
var players int // 0
var things []Thing // an empty slice of Things
var thing Thing // empty Thing struct
json.Unmarshall(reader, &thing)
```js
```
`var`关键字表明这个变量被**有意地**声明为该类型的零值。这也与在包级别声明变量时使用`var`而不是短声明语法(Austin Luo:`:=`)的要求一致——尽管我稍后会说您根本不应该使用包级变量。
- **既声明,也初始化时,使用\****`:=`****。**当同时要声明和初始化变量时,换言之我们不让变量隐式地被初始化为零值时,我建议使用短声明语法的形式。这使得读者清楚地知道`:=`左侧的变量是有意被初始化的。
为解释原因,我们回头再看看上面的例子,但这一次每个变量都被有意初始化了:
```js
var players int = 0
var things []Thing = nil
var thing *Thing = new(Thing)
json.Unmarshall(reader, thing)
```
第一个和第三个示例中,因为 Go 没有从一种类型到另一种类型的自动转换,赋值运算符左侧和右侧的类型必定是一致的。编译器可以从右侧的类型推断出左侧所声明变量的类型。对于这个示例可以更简洁地写成这样:
```js
var players = 0
var things []Thing = nil
var thing = new(Thing)
json.Unmarshall(reader, thing)
```
由于`0`是`players`的零值,因此为`players`显式地初始化为`0`就显得多余了。所以为了更清晰地表明我们使用了零值,应该写成这样:
```js
var players int
```
那第二条语句呢?我们不能忽视类型写成:
```js
var things = nil
```
因为`nil`根本就没有类型^2。相反,我们有一个选择,我们是否希望切片的零值?
```js
var things []Thing
```
或者我们是否希望创建一个没有元素的切片?
```js
var things = make([]Thing, 0)
```
如果我们想要的是后者,这不是个切片类型的零值,那么我们应该使用短声明语法让阅读者很清楚地明白我们的选择:
```js
things := make([]Thing, 0)
```
这告诉了读者我们显式地初始化了`things`。
再来看看第三个声明:
```js
var thing = new(Thing)
```
这既显式地初始化了变量,也引入了 Go 程序员不喜欢而且很不常用的`new`关键字。如果我们遵循短命名语法的建议,那么这句将变成:
```js
thing := new(Thing)
```
这很清楚地表明,`thing`被显式地初始化为`new(Thing)`的结果——一个指向`Thing`的指针——但仍然保留了我们不常用的`new`。我们可以通过使用**紧凑结构初始化的形式**来解决这个问题,
```js
thing := &Thing{}
```
这和`new(Thing)`做了同样的事——也因此很多 Go 程序员对这种重复感觉不安。不过,这一句仍然意味着我们为`thing`明确地初始化了一个`Thing{}`的指针——一个`Thing`的零值。
在这里,我们应该意识到,`thing`被初始化为了零值,并且将它的指针地址传递给了`json.Unmarshall`:
```js
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 requires lots of comments. — Dave Thomas and Andrew Hunt, The Pragmatic Programmer 好的代码中附带有大量的注释,坏的代码缺少大量的注释。
代码注释对 Go 程序的可读性极为重要。一个注释应该做到如下三个方面的至少一个:
1. 注释应该解释“做什么”。
2. 注释应该解释“怎么做的”。
3. 注释应该解释“为什么这么做”。
第一种形式适合公开的符号:
```js
// Open opens the named file for reading.
// If successful, methods on the returned file can be used for reading.
```
第二种形式适合方法内的注释:
```js
// queue all dependant actions
var results []chan error
for _, dep := range a.Deps {
results = append(results, execute(seen, dep))
}
```
第三种形式,“为什么这么做”,这是独一无二的,无法被前两种取代,也无法取代前两种。第三种形式的注释用于解释更多的状况,而这些状况往往难以脱离上下文,否则将没有意义,这些注释就是用来阐述上下文的。
```js
return &v2.Cluster_CommonLbConfig{
// Disable HealthyPanicThreshold
HealthyPanicThreshold: &envoy_type.Percent{
Value: 0,
},
}
```
在这个示例中,很难立即弄清楚把`HealthyPanicThreshold`的百分比设置为零会产生什么影响。注释就用来明确将值设置为`0`实际上是禁用了`panic`阈值的这种行为。
## 变量和常量上的注释应当描述它的内容,而非目的
我之前谈过,变量或常量的名称应描述其目的。向变量或常量添加注释时,应该描述变量的**内容**,而不是定义它的**目的**。
```js
const randomNumber = 6 // determined from an unbiased die
```
这个示例的注释描述了“为什么”`randomNumber`被赋值为 6,也说明了 6 这个值是从何而来的。但它没有描述`randomNumber`会被用到什么地方。下面是更多的例子:
```js
const (
StatusContinue = 100 // RFC 7231, 6.2.1
StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2
StatusProcessing = 102 // RFC 2518, 10.1
StatusOK = 200 // RFC 7231, 6.3.1
```
如在 RFC 7231 的第 6.2.1 节中定义的那样,在 HTTP 语境中 100 被当做`StatusContinue`。
> 小窍门:对于那些没有初始值的变量,注释应当描述谁将负责初始化它们 // sizeCalculationDisabled indicates whether it is safe // to calculate Types' widths and alignments. See dowidth. var sizeCalculationDisabled bool 这里,通过注释让读者清楚函数`dowidth`在负责维护`sizeCalculationDisabled`的状态。 小窍门:**隐藏一目了然的东西** Kate Gregory 提到一点^3,有时一个好的命名,可以省略不必要的注释。 // registry of SQL drivers var registry = make(mapstring*sql.Driver) 注释是源码作者加的,因为`registry`没能解释清楚定义它的目的——它是个注册表,但是什么的注册表? 通过重命名变量名为`sqlDrivers`,现在我们很清楚这个变量的目的是存储 SQL 驱动。 var sqlDrivers = make(mapstring*sql.Driver) 现在注释已经多余了,可以移除。
## 总是为公开符号写文档说明
因为 godoc 将作为您的包的文档,您应该总是为每个公开的符号写好注释说明——包括变量、常量、函数和方法——所有定义在您包内的公开符号。
这里是 Go 风格指南的两条规则:
- 任何既不明显也不简短的公共功能必须加以注释。
- 无论长度或复杂程度如何,都必须对库中的任何函数进行注释。
```js
package ioutil
// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
func ReadAll(r io.Reader) ([]byte, error)
```
对这个规则有一个例外:您不需要为实现接口的方法进行文档说明,特别是不要这样:
```js
// Read implements the io.Reader interface
func (r *FileReader) Read(buf []byte) (int, error)
```
这个注释等于说明都没说,它没有告诉您这个方法做了什么,实际上更糟的是,它让您去找别的地方的文档。在这种情况我建议将注释整个去掉。
这里有一个来自`io`这个包的示例:
```js
// LimitReader returns a Reader that reads from r
// but stops with EOF after n bytes.
// The underlying implementation is a *LimitedReader.
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }
// A LimitedReader reads from R but limits the amount of
// data returned to just N bytes. Each call to Read
// updates N to reflect the new amount remaining.
// Read returns EOF when N <= 0 or when the underlying R returns EOF.
type LimitedReader struct {
R Reader // underlying reader
N int64 // max bytes remaining
}
func (l *LimitedReader) Read(p []byte) (n int, err error) {
if l.N <= 0 {
return 0, EOF
}
if int64(len(p)) > l.N {
p = p[0:l.N]
}
n, err = l.R.Read(p)
l.N -= int64(n)
return
}
```
请注意,`LimitedReader`的声明紧接在使用它的函数之后,并且`LimitedReader.Read`又紧接着定义在`LimitedReader`之后,即便`LimitedReader.Read`本身没有文档注释,那和很清楚它是`io.Reader`的一种实现。
> 小窍门:在您编写函数之前先写描述这个函数的注释,如果您发现注释很难写,那就表明您正准备写的这段代码一定难以理解。
### 不要为坏的代码写注释,重写它
> Don’t comment bad code — rewrite it — Brian Kernighan 不要为坏的代码写注释——重写它
为粗制滥造的代码片段着重写注释是不够的,如果您遭遇到一段这样的注释,您应该发起一个问题(issue)从而记得后续重构它。技术债务只要不是过多就没有关系。
在标准库的惯例是,批注一个 TODO 风格的注释,说明是谁发现了坏代码。
```js
// TODO(dfc) this is O(N^2), find a faster way to do this.
```
注释中的姓名并不意味着承诺去修复问题,但在解决问题时,他可能是最合适的人选。其他批注内容一般还有日期或者问题编号。
### 与其为一大段代码写注释,不如重构它
> Good code is its own best documentation. As you’re about to add a comment, ask yourself, 'How can I improve the code so that this comment isn’t needed?' Improve the code and then document it to make it even clearer. — Steve McConnell 好的代码即为最好的文档。在您准备添加一行注释时,问自己,“我要如何改进这段代码从而使它不需要注释?”优化代码,然后注释它使之更清晰。
函数应该只做一件事。如果您发现一段代码因为与函数的其他部分不相关因而需要注释时,考虑将这段代码拆分为独立的函数。
除了更容易理解之外,较小的函数更容易单独测试,现在您将不相关的代码隔离拆分到不同的函数中,估计只有函数名才是唯一需要的文档注释了。
**此文已由作者授权腾讯云+社区发布,更多原文请[点击](https://cloud.tencent.com/developer/article/1367085?fromSource=waitui )**
**搜索关注公众号「云加社区」,第一时间获取技术干货,关注后回复1024 送你一份技术课程大礼包!**
有疑问加站长微信联系(非本文作者)