本文视频地址
日常工作中,命名这件事看似简单,如果在大规模软件开发中,做出好的命名并非易事。
命名是编程语言的要求:好的命名是为了提高程序的可读性和可维护性。什么是好的命名呢?无论哪门编程语言,良好的命名应该遵循一些通用的原则,不同编程语言在命名上还会有一些个性化的命名习惯。
要想做好 Go 标识符命名(包括 package 命名),最少要遵循两个原则:
1 简单且一致
2 利用上下文辅助命名
1. 简单且一致
对于简单,我们最直观地理解就是越短越好,但这里的简单还包含着清晰明确。短意味着能用一个单词命名的,就不要使用单词组合;能用1个字母(在特定上下文)表达标识符的用途,就不用完整单词。
如下是 Go 语言一些常见的命名规范。
1) 包
Go 中的包(package)一般建议以小写形式的单个单词命名,Go 标准库在这方面给我们做出了很好的示范:
在给包命名时不要有是否与其他包重名的顾虑,因为在 Go 中,包名是可以不唯一的,比如 project1 项目有名为 work 的包,project2 项目也可以有自己的名为 work 的包。但是每个包的导入路径是唯一的,对于包名冲突的情况,可以通过包别名(package alias)语法来解决:
import "github.com/i-coder-robot/project1/work"
import barlog "github.com/i-coder-robot/project2/work" // package import alias
Go 语言建议:包名应尽量与包导入路径(import path)的最后一个路径分段保持一致。比如:包导入路径 golang.org/x/text/encoding 的最后路径分段是 encoding,该路径下包名就应该为 encoding。但在实际情况中,包名与导入路径最后分段不同的也有很多,比如:实时分布式消息队列 nsq 的官方客户端包的导入路径为:github.com/nsqio/go-nsq ,但是该路径下面的包名却是 nsq。比如 go-nsq 的代表这是一份 Go 语言实现的 nsq 客户端 API 库,为的是和 nsq-java、pynsq、rust-nsq 等其他语言的客户端 API 做出显式区分。
那如果将 nsq 的 Go 客户端 API 放入 github.com/nsqio/go-nsq/nsq 下面,会是怎样呢?显然在导入路径中出现两次"nsq"字样的现象,不被 Go 官方推荐。今天看来如果能将所有 Go 实现放入 github 账号顶层路径下面的 golang 或 go 路径下应该是更好的方案,比如:
"github.com/nsqio/go/nsq”
"github.com/nsqio/golang/nsq"
不仅要考虑包自身的名字,还要同时考虑兼顾到该包导出的标识符(如变量、常量、类型、函数等)的命名。由于对这些这些包导出标识符的引用是必须以包名作为前缀的,因此对包导出标识符命名时,在名字中不要再包含包名,比如:
strings.Reader 推荐
strings.StringReader 不推荐
strings.ReaderString 不推荐
strings.NewReader 推荐
strings.NewStringReader 不 推荐
strings.ReaderNewString 不 推荐
bytes.Buffer 推荐
bytes.ByteBuffer 不推荐
bytes.BufferByte 不推荐
bytes.NewByteBuffer 不推荐
bytes.BufferNewByte 不推荐
2) 变量、类型、函数和方法
Go 工程中包命名相对少,变量、类型、函数和方法的命名是日常工作的家常便饭。
在 Go 中变量分为包级别的变量和局部变量(函数或方法内的变量)。函数或方法的参数、返回值一定程度上都可以视为局部变量。
Go 语言官方要求标识符命名采用驼峰命名法(CamelCase),以变量名为例,如果变量名由一个以上的词组合构成,那么这些词之间紧密相连,不使用任何连接符(比如:下划线)。驼峰命名法有两种形式,一种是第一个词的首字母小写,后面每个词的首字母大写,叫做“小骆峰拼写法”(lowerCamelCase),这也是在 Go 中最常见的标识符命名法;而第一个词的首字母以及后面每个词的首字母都大写,叫做“大驼峰拼写法”(UpperCamelCase),又称“帕斯卡拼写法”(PascalCase)。由于首母大写的标识符在 Go 语言中被视作包导出标识符,因此只有在涉及包导出的情况下,才会用到大驼峰拼写法。不过首字母缩略词要保持全部大写,比如 HTTP(Hypertext Transfer Protocol)、CBC(Cipher Block Chaining) 等。
给变量、类型、函数和方法命名依然要以简单短小为首要考虑的原则,我们看一下 Go 标准库中标识符名称,很多单字母的标识符命名,这是 Go 在命名上的一个惯例。 Go 标识符一般来说仍以单个单词作为命名首选。不同类别标识符的命名呈现出下面一些特征:
- 循环和条件变量多采用单个字母命名(具体见上面的统计数据);
- 函数/方法的参数和返回值变量一般以单个单词或单个字母为主;
- 方法名由于在调用时会绑定类型信息,因此命名多以单个单词为主;
- 函数名则多以多单词的复合词进行命名;
- 类型名也多以多单词的复合词进行命名;
如下
catSlice []*Cat [不推荐]
cats []*Cat [推荐]
带有类型信息的命名除了让变量看起来更长之外,让阅读代码的人感觉臃肿。
有人会问:catSlice 可以得知变量所代表的底层存储是一个切片,这样便可以在 catSlice 上应用切片的各种操作了。有这样疑问的人然忘记了命名的通用惯例:保持变量声明与使用之间的距离越近越好或者说将变量声明在第一次使用该变量之前。如果在一屏之内能看到 cats 的声明,那-Slice 这个类型信息显然是不必放在变量的名称中了。
- 保持简短命名变量含义上的一致性
Go 标准库中常见短变量名字所代表的含义,在整个标准库范围内不会产生歧义。
变量v, k, i的常用含义:
// 循环语句中的变量
for i, v := range s { ... } // i: 下标变量; v:元素值
for k, v := range m { ... } // k: key变量;v: 元素值
for v := range r { ... } // v: 元素值
// if、switch/case分支语句中的变量
if v := foods[name]; v != "" { } // v: 元素值
switch v := e.Elem(); v.Kind() {
... ...
}
case v := <-c: // v: 元素值
// 反射的结果值
v := reflect.ValueOf(y)
变量t的常用含义:
t := time.Now()
t := &Timer{}
if t := md.map[k]; t != nil { }
变量b的常用含义:
b := make([]byte, n)
b := new(bytes.Buffer)
3) 常量
在 Go 语言中,常量在命名方式上与变量并无较大差别,并不要求全部大写。只是考虑其含义的准确传递,常量多使用多单词组合的命名。下面是标准库中的例子:
// $GOROOT/src/net/http/request.go
const (
defaultMaxTimes = 6
)
const (
keepAlive = false
)
当然,你也可以为本身就有着大写名称的特定常量使用全大写的名字,比如数学计算中的 PI。
在 Go 中数值型常量无需显式赋予类型,常量会在使用时根据左值类型和其他运算操作数的类型做自动的转换。
4) 接口
Go 语言中的 interface 是 Go 在编程语言层面上的一个创新,它为 Go 代码提供了强大的“解耦合”能力,因此良好的接口类型设计和接口组合是 Go 程序设计的静态骨架和基础。良好的接口设计必须有好的接口命名。在 Go 语言中 interface 名字仍然以单个词为优先。对于拥有唯一方法(method)或通过多个拥有唯一方法的接口组合而成的接口,Go 语言的惯例是一般用"方法名+er"的方式为 interface 命名。比如:
type Writer interface {
Write(p []byte) (n int, err error)
}
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
2. 利用上下文环境,用最短的名字携带足够的信息
Go 在给标识符命名时还有着考虑上下文环境的惯例,即在不影响可读性前提下,结合一致性的原则。在 Go 中分别运用这两个命名方案(index、value)和 (i、 v),并做比对:
for index := 0; index < len(s); index++ {
value := s[index]
... ...
}
vs.
for i := 0; i < len(s); i++ {
v := s[i]
... ...
}
我们看到:至少在 forLoop 这个上下文中,index、value 并没有比 i、v 携带更多额外信息。
有疑问加站长微信联系(非本文作者)