Go 语言实践(一)

本文由Austin发表git

指导原则

咱们要谈论在一个编程语言中的最佳实践,那么咱们首先应该明确什么是“最佳”。若是您们听了我昨天那场讲演的话,您必定看到了来自 Go 团队的 Russ Cox 讲的一句话:程序员

软件工程,是您在编程过程当中增长了工期或者开发人员以后发生的那些事。 — Russ Coxsql

Russ 是在阐述软件“编程”和软件“工程”之间的区别,前者是您写的程序,然后者是一个让更多的人长期使用的产品。软件工程师会来来去去地更换,团队也会成长或者萎缩,需求也会发生变化,新的特性也会增长,bug 也会被修复,这就是软件“工程”的本质。数据库

我多是现场最先的 Go 语言用户,但与其说个人主张来自个人资历,不如说我今天讲的是真实来自于 Go 语言自己的指导原则,那就是:编程

  1. 简单性
  2. 可读性
  3. 生产率

您可能已经注意到,我并无提性能或者并发性。实际上有很多的语言执行效率比 Go 还要高,但它们必定没有 Go 这么简单。有些语言也以并发性为最高目标,但它们的可读性和生产率都很差。 性能并发性都很重要,但它们不如简单性可读性生产率那么重要。json

简单性

为何咱们要力求简单,为何简单对 Go 语言编程如此重要?数组

咱们有太多的时候感叹“这段代码我看不懂”,是吧?咱们惧怕修改一丁点代码,生怕这一点修改就致使其余您不懂的部分出问题,而您又没办法修复它。并发

这就是复杂性。复杂性把可读的程序变得不可读,复杂性终结了不少软件项目。app

简单性是 Go 的最高目标。不管咱们写什么程序,咱们都应该能一致认为它应当简单。dom

可读性

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 标识符的声明和使用间隔越远,名称的长度就应当越长。

据此,咱们能够概括一些指导意见:

  • 短变量名称在声明和上次使用之间的距离很短时效果很好。
  • 长变量名须要证实其不一样的合理性:越长的变量名,越须要更多的理由来证实其合理。冗长、繁琐的名称与他们在页面上的权重相比,携带的信息很低。
  • 不要在变量名中包含其类型的名称。
  • 常量须要描述其存储的值的含义,而不是怎么使用它。
  • 单字母变量可用于循环或逻辑分支,单词变量可用于参数或返回值,多词短语可用于函数和包这一级的声明。
  • 单词可用于方法、接口和包
  • 请记住,包的命名将成为用户引用它时采用的名称,确保这个名称更有意义。

让咱们来看一个示例:

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 行,同理的还有sumcount,这他们使用了更长的名称,读者必须关注更普遍的代码行。

我也可使用s而不是sum,用c(或n)而不是count,但这会将整个程序中的变量都汇集在相同的重要性上。我也可使用p而不是people,可是这样又有一个问题,那就是for ... range循环中的变量又用什么?单数的 person 看起来也很奇怪,生存时间极短命名却比导出它的那个值更长。

Austin Luo:这里说的是,若数组people用变量名p,那么从数组中获取的每个元素取名就成了问题,好比用person,即便使用person看起来也很奇怪,一方面是单数,一方面person的生存周期只有两行(很短),命名比生存周期更长的ppeople)还长了。 小窍门:跟使用空行在文档中分段同样,使用空行将函数执行过程分段。在函数AverageAge中有按顺序的三个操做。第一个是先决条件,检查当people为空时咱们不会除零,第二个是累加总和和计数,最后一个是计算平均数。

上下文是关键

绝大多数的命名建议都是根据上下文的,意识到这一点很重要。我喜欢称之为原则,而不是规则。

iindex 这两个标识符有什么不一样?咱们很难确切地说其中一个比另外一个好,好比:

for index := 0; index < len(s); index++ {
  //
}

上述代码的可读性,基本上都会认为比下面这段要强:

for i := 0; i < len(s); i++ {
  //
}

但我表示不赞同。由于不管是i仍是index,都是限定于for循环体的,更冗长的命名,并无让咱们更容易地理解这段代码。

话说回来,下面两段代码那一段可读性更强呢?

func (s *SNMP) Fetch(oid []int, index int) (int, error)

或者

func (s *SNMP) Fetch(o []int, i int) (int, error)

在这个示例中,oidSNMP对象 ID 的缩写,所以将其略写为 o 意味着开发者必须将他们在文档中看到的常规符号转换理解为代码中更短的符号。一样地,将index简略为i,减小了其做为SNMP消息的索引的含义。

小窍门:在参数声明中不要混用长、短不一样的命名风格。

命名中不要包含所属类型的名称

正如您给宠物取名同样,您会给狗取名“汪汪”,给猫取名为“咪咪”,但不会取名为“汪汪狗”、“咪咪猫”。出于一样的缘由,您也不该在变量名称中包含其类型的名称。

变量命名应该体现它的内容,而不是类型。咱们来看下面这个例子:

var usersMap map[string]*User

这样的命名有什么好处呢?咱们能知道它是个 map,而且它与*User类型有关,这可能还不错。可是 Go 做为一种静态类型语言,它并不会容许咱们在须要标量变量的地方意外地使用到这个变量,所以Map后缀其实是多余的。

如今咱们来看像下面这样定义变量又是什么状况:

var (
  companiesMap map[string]*Company
  productsMap map[string]*Products
)

如今这个范围内咱们有了三个 map 类型的变量了:usersMapcompaniesMap,以及 productsMap,全部这些都从字符串映射到了不一样的类型。咱们知道它们都是 map,咱们也知道它们的 map 声明会阻止咱们使用一个代替另外一个——若是咱们尝试在须要map[string]*User的地方使用companiesMap,编译器将抛出错误。在这种状况下,很明显Map后缀不会提升代码的清晰度,它只是编程时须要键入的冗余内容。(Austin Luo:陈旧的思惟方式)

个人建议是,避免给变量加上与类型相关的任何后缀。

小窍门:若是users不能描述得足够清楚,那usersMap也必定不能。

这个建议也适用于函数参数,好比:

type Config struct {
  //
}

func WriteConfig(w io.Writer, config *Config)

*Config参数命名为config是多余的,咱们知道它是个*Config,函数签名上写得很清楚。

在这种状况建议考虑conf或者c——若是生命周期足够短的话。

若是在一个范围内有超过一个*Config,那命名为conf1conf2的描述性就比originalupdated更差,并且后者比前者更不容易出错。

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.DBdbase *sql.DBDB *sql.DBdatabase *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 的命名风格规定接收器具备单个字母名称或其派生类型的首字母缩略词。有时您可能会发现接收器的名称有时会与方法中参数的名称冲突,在这种状况下,请考虑使参数名称稍长,而且仍然不要忘记一致地使用这个新名称。

最后,某些单字母变量传统上与循环和计数有关。例如,ij,和k一般是简单的for循环变量。n一般与计数器或累加器有关。 v一般是某个值的简写,k一般用于映射的键,s一般用做string类型参数的简写。

与上面db的例子同样,程序员指望i是循环变量。若是您保证i始终是一个循环变量——而不是在for循环以外的状况下使用,那么当读者遇到一个名为i或者j的变量时,他们就知道当前还在循环中。

小窍门:若是您发如今嵌套循环中您都使用完ijk了,那么很显然这已经到了将函数拆得更小的时候了。

使用一致的声明风格

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)

因为0players的零值,所以为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 requires lots of comments. — Dave Thomas and Andrew Hunt, The Pragmatic Programmer 好的代码中附带有大量的注释,坏的代码缺乏大量的注释。

代码注释对 Go 程序的可读性极为重要。一个注释应该作到以下三个方面的至少一个:

  1. 注释应该解释“作什么”。
  2. 注释应该解释“怎么作的”。
  3. 注释应该解释“为何这么作”。

第一种形式适合公开的符号:

// Open opens the named file for reading.
// If successful, methods on the returned file can be used for reading.

第二种形式适合方法内的注释:

// queue all dependant actions
var results []chan error
for _, dep := range a.Deps {
        results = append(results, execute(seen, dep))
}

第三种形式,“为何这么作”,这是独一无二的,没法被前两种取代,也没法取代前两种。第三种形式的注释用于解释更多的情况,而这些情况每每难以脱离上下文,不然将没有意义,这些注释就是用来阐述上下文的。

return &v2.Cluster_CommonLbConfig{
  // Disable HealthyPanicThreshold
  HealthyPanicThreshold: &envoy_type.Percent{
    Value: 0,
  },
}

在这个示例中,很难当即弄清楚把HealthyPanicThreshold的百分比设置为零会产生什么影响。注释就用来明确将值设置为0其实是禁用了panic阈值的这种行为。

变量和常量上的注释应当描述它的内容,而非目的

我以前谈过,变量或常量的名称应描述其目的。向变量或常量添加注释时,应该描述变量的内容,而不是定义它的目的

const randomNumber = 6 // determined from an unbiased die

这个示例的注释描述了“为何”randomNumber被赋值为 6,也说明了 6 这个值是从何而来的。但它没有描述randomNumber会被用到什么地方。下面是更多的例子:

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(mapstringsql.Driver) 注释是源码做者加的,由于registry没能解释清楚定义它的目的——它是个注册表,可是什么的注册表? 经过重命名变量名为sqlDrivers,如今咱们很清楚这个变量的目的是存储 SQL 驱动。 var sqlDrivers = make(mapstringsql.Driver) 如今注释已经多余了,能够移除。

老是为公开符号写文档说明

由于 godoc 将做为您的包的文档,您应该老是为每一个公开的符号写好注释说明——包括变量、常量、函数和方法——全部定义在您包内的公开符号。

这里是 Go 风格指南的两条规则:

  • 任何既不明显也不简短的公共功能必须加以注释。
  • 不管长度或复杂程度如何,都必须对库中的任何函数进行注释。
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)

对这个规则有一个例外:您不须要为实现接口的方法进行文档说明,特别是不要这样:

// Read implements the io.Reader interface
func (r *FileReader) Read(buf []byte) (int, error)

这个注释等于说明都没说,它没有告诉您这个方法作了什么,实际上更糟的是,它让您去找别的地方的文档。在这种状况我建议将注释整个去掉。

这里有一个来自io这个包的示例:

// 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 风格的注释,说明是谁发现了坏代码。

// 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 好的代码即为最好的文档。在您准备添加一行注释时,问本身,“我要如何改进这段代码从而使它不须要注释?”优化代码,而后注释它使之更清晰。

函数应该只作一件事。若是您发现一段代码由于与函数的其余部分不相关于是须要注释时,考虑将这段代码拆分为独立的函数。

除了更容易理解以外,较小的函数更容易单独测试,如今您将不相关的代码隔离拆分到不一样的函数中,估计只有函数名才是惟一须要的文档注释了。

此文已由做者受权腾讯云+社区发布,更多原文请点击

搜索关注公众号「云加社区」,第一时间获取技术干货,关注后回复1024 送你一份技术课程大礼包!

相关文章
相关标签/搜索