[3分钟] GO:耐看的注释都是如何被创造出来的

发布时间 2023-12-07 15:28:34作者: 姜七七

一、如何设计友好的标识符

标识符是一个用来表示名称的花哨单词; 变量的名称函数的名称方法的名称类型的名称包的名称等。

可读性是代码评审的标准之一。好的代码需要简单易读、通俗易懂。标识符是具体数据在代码中的抽象代表,友好的标识符会大大提高读代码的效率。反之,杂乱的标识符会混淆视听,打乱代码的逻辑,严重影响读代码的效率。

Good naming is like a good joke. If you have to explain it, it’s not funny. (好的命名就像一个好笑话。如果你必须解释它,那就不好笑了。) — Dave Cheney

1、什么是清晰的标识符

简洁: 清晰的标识符是简洁的,简洁包括简短,但不是简短,而是包含了大量有效信息的简短,不会将长度浪费到无用的信息上。

描述性:

  • 1)好的名字会描述变量或常量的应用,而不是它们的内容。

  • 2)好的名字应该描述函数的结果或方法的行为,而不是它们的操作。

  • 3)好的名字应该描述包的目的而非它的内容。描述东西越准确的名字就越好。

可预测的: 你能够从名字中推断出使用方式。

1.1 标识符长度

The greater the distance between a name’s declaration and its uses, the longer the name should be. (名字的声明与其使用之间的距离越大,名字应该越长。) — Andrew Gerrand [2]

短变量名称在声明和上次使用之间的距离很短时效果很好。

长变量名称需要证明自己的合理性; 名称越长,需要提供的价值越高。

变量名称中请勿包含类型名称

常量应该描述它们持有的值,而不是该如何使用。

对于循环分支使用单字母变量,参数和返回值使用单个字,函数和包级别声明使用多个单词

方法接口使用单个词。

请记住,包的名称是调用者用来引用名称的一部分,因此要好好利用这一点。

我们举个例子:

package main

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
}

以上例子应用到了:

  • for循环中使用单字变量 p;
  • sum、count 符合简洁、描述性、可推测的特点;
  • people 符合简洁、描述性的特点;

贴士: 与使用段落分解文档的方式一样用空行来分解函数。 在 AverageAge
中,按顺序共有三个操作。 第一个是前提条件,检查 people 是否为空,第二个是 sum 和 count 的累积,最后是平均值的计算。

1.2 上下文是关键

重要的是要意识到关于命名的大多数建议都是需要考虑上下文的。 我想说这是一个原则,而不是一个规则。

关于i和index。对于下列两种情形,在不同上下文中,很明显,情景一使用i更简洁,同时也不会使程序的可读性变差;但对于情景二,使用index效果更好,使用o和i来代替 oid 和 index
会对可读性造成很恶劣的影响。

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

for i := 0; i < len(s); i++ { // } 
	
// 情景二: 
func (s *SNMP) Fetch(oid []int, index int) (int, error)

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

贴士: 在同一声明中长和短形式的参数不能混搭。

1.3 不要用变量类型命名你的变量

你不应该用变量的类型来命名你的变量, 就像您不会将宠物命名为“狗”和“猫”。 出于同样的原因,您也不应在变量名字中包含类型的名字。
变量的名称应描述其内容,而不是内容的类型。 例如:

var usersMap map[string]*User

var users map[string]*User

上述第一行变量有两个信息。1)userMap是存储user的;2)userMap是一个Map

第三行变量有一个信息。1)users是存储user的

那么有人就会想了,usersMap不是更好吗?其实不然,我以为有以下两点原因 :

  • 1)变量名的差异不会影响该变量的类型是不是一个map;
  • 2)go语言是一种静态类型语言,如果你错误的使用该变量,编译器将抛出错误异常。

很明显,后缀多一个Map并没有使代码更清晰。正如我们不会对一个int类型的变量加上int后缀,我们也不应该对该变量加上Map后缀。

贴士: 如果 users 的描述性都不够用,那么 usersMap 也不会。

此建议也适用于函数参数,例如

type Config struct { // }

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

命名 *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)

1.4 使用一致的命名方式

一个好名字的另一个属性是它应该是可预测的。 在第一次遇到该名字时读者就能够理解名字的使用。 当他们遇到常见的名字时,他们应该能够认为自从他们上次看到它以来它没有改变意义。

例如,如果您的代码在处理数据库请确保每次出现参数时,它都具有相同的名称。 与其使用 d * sql.DBdbase * sql.DBDB * sql.DBdatabase * sql.DB 的组合,倒不如统一使用:

db *sql.DB

同样的,对于方法接收器,在该类型的每个方法行使用相同的接收者名称。统一变量会提高代码的可读性,加入每个方法的接受者名称都不一致,想想就令人头大。 同样的,在编程中,某些变量已经变成了行业内默认的"标准"。

  • 比如 i j k :通常是简单 for 循环的循环归纳变量
  • n:通常与计数器或累加器相关联
  • v:是通用编码函数中值的常用简写
  • k:通常用于 map 的键
  • s:通常用作字符串类型参数的简写

贴士: 如果你发现自己有如此多的嵌套循环,i,j 和 k
变量都无法满足时,这个时候可能就是需要将函数分解成更小的函数。

1.5 使用一致的声明样式 Go

至少有六种不同的方式来声明变量,多种声明变量的方式会造成Go程序员每个人都可能有不同的选择,当不同的方式在同一份代码中出现时,会极大的阻碍代码的阅读。

var x int = 1 
var x = 1 
var x int; 
x = 1 
var x = int(1)
x := 1 

一般的用法是:

  • 在声明但不初始化的时候,使用 var。var 表示此变量已被声明为指定类型的零值。var 会指定类型;
  • 在声明和初始化时,使用 :=
  • 对于slice、map、channel等类型,建议使用make

这里博客还有更详细的介绍,但我认为讲的过于繁杂,这里我只写结论,读者如果有兴趣可以自己去看原文章,我会贴到文章末尾。

1.6 成为团队合作者

在项目中,建议遵循项目的代码风格,这样更容易维护以及团队成员阅读及理解。


内容学习于该博客:英文博客

同时借鉴于该翻译:中文翻译