Go 的设计哲学之一就是追求简单,因此在命名上一样秉承着简单的总体原则。但简单并不意味着一味地为标识符选择短小的名字,而是要选择那种可以在标识符所在上下文中保持其用途清晰明确的名字。Go 自身以及标准库的实现是 Go 命名惯例形成的最初源头,因此如果要寻找良好命名的示范,Go 标准库是一个不错的地方。

要想做好 Go 标识符命名(包括 package 命名),至少要遵循两个原则:

简单且一致

对于简单,我们最直观地理解就是“短小”,但这里的简单还包含着清晰明确的前提。短小意味着能用一个单词命名的,就不要使用单词组合;能用单个字母(在特定上下文)表达标识符用途的,就不用完整单词。 甚至在某种情况下,Go 命名惯例选择了简洁命名+注释辅助解释的方式,而不是一个长长的名字。

下面是 Go 语言一些常见类别标识符命名惯例:

Go 中的包(package)一般建议以小写形式的单个单词命名

image.png

对于包名冲突的情况,可以通过包别名(package alias)语法来解决:

  1. import "github.com/bigwhite/foo/log"
  2. import barlog "github.com/bigwhite/bar/log" // package import alias
  • Go 语言建议:包名应尽量与包导入路径(import path)的最后一个路径分段保持一致

更好的路径命名方案:

  1. "github.com/nsqio/go/nsq""github.com/nsqio/golang/nsq"
  2. "github.com/bigwhite/go/cmpp""github.com/bigwhite/golang/cmpp"
  • 在给包命名的时候,不仅要考虑包自身的名字,还要同时考虑兼顾到该包导出的标识符(如变量、常量、类型、函数等)的命名

由于对这些这些包导出标识符的引用是必须以包名作为前缀的,因此对包导出标识符命名时,在名字中不要再包含包名:

  1. strings.Reader [good]
  2. strings.StringReader [bad]
  3. strings.NewReader [good]
  4. strings.NewStringReader [bad]
  5. bytes.Buffer [good]
  6. bytes.ByteBuffer [bad]
  7. bytes.NewByteBuffer [bad]

变量、类型、函数和方法

Go 语言官方要求标识符命名采用驼峰命名法(CamelCase):

  • “小骆峰拼写法”(lowerCamelCase)
  • “大驼峰拼写法”(UpperCamelCase),又称“帕斯卡拼写法”(PascalCase)
  • 首字母缩略词要保持全部大写,比如 HTTP(Hypertext Transfer Protocol)、CBC(Cipher Block Chaining) 等

从 Go 标准库代码的不完全统计结果来看,不同类别标识符的命名呈现出下面一些特征:

  1. 循环和条件变量多采用单个字母命名(具体见上面的统计数据);
  2. 函数/方法的参数和返回值变量一般以单个单词或单个字母为主;
  3. 方法名由于在调用时会绑定类型信息,因此命名多以单个单词为主;
  4. 函数名则多以多单词的复合词进行命名;
  5. 类型名也多以多单词的复合词进行命名。

除了上述特征,还有一些惯例是在命名时常用的:

  • 变量名字中不要带有类型信息
    • 一个名字的声明和使用之间的距离越大,这个名字的长度就越长
  • 保持简短命名变量含义上的一致性

Go 标准库中常见短变量名字所代表的含义:

  • 变量v, k, i的常用含义
  1. // 循环语句中的变量
  2. for i, v := range s { ... } // i: 下标变量; v:元素值
  3. for k, v := range m { ... } // k: key变量;v: 元素值
  4. for v := range r { // channel ... } // v: 元素值
  5. // if、switch/case分支语句中的变量
  6. if v := mimeTypes[ext]; v != "" { } // v: 元素值
  7. switch v := ptr.Elem(); v.Kind() {
  8. ... ...
  9. }
  10. case v := <-c: // v: 元素值
  11. // 反射的结果值
  12. v := reflect.ValueOf(x)
  • 变量t的常用含义:
  1. t := time.Now() // 时间
  2. t := &Timer{}// 定时器
  3. if t := md.typemap[off]; t != nil { }// 类型
  • 变量b的常用含义
  1. b := make([]byte, n) // byte切片
  2. b := new(bytes.Buffer) // byte缓存

常量

Go 语言中,常量在命名方式上与变量并无较大差别,并不要求全部大写。只是考虑其含义的准确传递,常量多使用多单词组合的命名。下面是标准库中的例子:

  1. // $GOROOT/src/net/http/request.go
  2. const (
  3. defaultMaxMemory = 32 << 20 // 32 MB
  4. )
  5. const (
  6. deleteHostHeader = true
  7. keepHostHeader = false
  8. )
  9. // $GOROOT/src/math/sin.go
  10. const (
  11. PI4A = 7.85398125648498535156E-1 // 0x3fe921fb40000000, Pi/4 split into three parts
  12. PI4B = 3.77489470793079817668E-8 // 0x3e64442d00000000,
  13. PI4C = 2.69515142907905952645E-15 // 0x3ce8469898cc5170,
  14. )
  15. // $GOROOT/src/syscall/zerrors_linux_amd64.go
  16. // Errors
  17. const (
  18. E2BIG = Errno(0x7)
  19. EACCES = Errno(0xd)
  20. EADDRINUSE = Errno(0x62)
  21. EADDRNOTAVAIL = Errno(0x63)
  22. EADV = Errno(0x44)
  23. EAFNOSUPPORT = Errno(0x61)
  24. EAGAIN = Errno(0xb)
  25. EALREADY = Errno(0x72)
  26. EBADE = Errno(0x34)
  27. EBADF = Errno(0x9)
  28. EBADFD = Errno(0x4d)
  29. EBADMSG = Errno(0x4a)
  30. EBADR = Errno(0x35)
  31. EBADRQC = Errno(0x38)
  32. ... ...
  33. )
  34. // Signals
  35. const (
  36. SIGABRT = Signal(0x6)
  37. SIGALRM = Signal(0xe)
  38. SIGBUS = Signal(0x7)
  39. SIGCHLD = Signal(0x11)
  40. SIGCLD = Signal(0x11)
  41. SIGCONT = Signal(0x12)
  42. SIGFPE = Signal(0x8)
  43. SIGHUP = Signal(0x1)
  44. SIGILL = Signal(0x4)
  45. SIGINT = Signal(0x2)
  46. SIGIO = Signal(0x1d)
  47. ... ...
  48. )

接口

在 Go 语言中 interface 名字仍然以单个词为优先。对于拥有唯一方法(method)或通过多个拥有唯一方法的接口组合而成的接口,Go 语言的惯例是一般用”方法名+er”的方式为 interface 命名。比如:

  1. // $GOROOT/src/io/io.go
  2. type Writer interface {
  3. Write(p []byte) (n int, err error)
  4. }
  5. type Reader interface {
  6. Read(p []byte) (n int, err error)
  7. }
  8. type Closer interface {
  9. Close() error
  10. }
  11. type ReadWriteCloser interface {
  12. Reader
  13. Writer
  14. Closer
  15. }

利用上下文环境,用最短的名字携带足够的信息

Go 在给标识符命名时还有着考虑上下文环境的惯例,即在不影响可读性前提下,结合一致性的原则,尽可能地用长度短小的名字命名标识符。这与其他一些主流语言在命名上的建议有不同,比如 Java 建议遵循“见名知义”的命名原则。我们可以对比一下 Java 和 Go 在循环变量起名上的差异:

  1. java vs. go
  2. "index" vs. "i"
  3. "value" vs. "v"

我们在 Go 代码来中分别运用这两个命名方案(index、value)和 (i、 v),并做比对:

  1. for index := 0; index < len(s); index++ {
  2. value := s[index]
  3. ... ...
  4. }
  5. vs.
  6. for i := 0; i < len(s); i++ {
  7. v := s[i]
  8. ... ...
  9. }

这里我引用一下安德鲁·杰拉德在 2014 年的中的代码,我们再来对比感受一下 Go 命名惯例所带来的效果:

  1. [bad]
  2. func RuneCount(buffer []byte) int {
  3. runeCount := 0
  4. for index := 0; index < len(buffer); {
  5. if buffer[index] < RuneSelf {
  6. index++
  7. } else {
  8. _, size := DecodeRune(buffer[index:])
  9. index += size
  10. }
  11. runeCount++
  12. }
  13. return runeCount
  14. }
  15. [good]
  16. func RuneCount(b []byte) int {
  17. count := 0
  18. for i := 0; i < len(b); {
  19. if b[i] < RuneSelf {
  20. i++
  21. } else {
  22. _, n := DecodeRune(b[i:])
  23. i += n
  24. }
  25. count++
  26. }
  27. return count
  28. }

小结