Go 语言官方文档 effective go 的命名、分号部分
资料
effective go 源文档地址 本文的 bilibili 视频地址
名称
和其他编程语言类似,命名在 Go 语言中也很讲究。Go 中的命名甚至属于语法的一部分:名称的首字母是否大写决定这个名称在包外的可见性。因此很有必要花一些时间来了解 Go 程序中的命名规范。
包名
当包被导入(import)后,包名会成为包的访问器。比如,在 import "bytes"
后,导入此包的文件就可以使用 bytes.Buffer
来访问 bytes
包内的 Buffer
结构体了。如果使用某包的所有源代码都通过包名调用这个包的内容,这就意味着这个包的包名名称应该足够好:短、简洁且释义。按照惯例,包名应该是小写字符、且应该是一个独立的单词;包名里不应该包含下划线,也不应该是驼峰样式的名称。如果你定义了一个包,包名应该尽可能地简单,因为每个使用这个包的人都需要一个字符一个字符地把包名敲出来(字符太多敲起来麻烦);我们不需要担心我们的包名和已经存在的包名称上会冲突;对于导入某个包的源码来说,包名指定了默认的访问器名称,不过没有必要在所有导入这个包的源码中都使用这个默认的名称。
很少的情况下,同一个源文件中导入的两个包可能有相同的包名,这时候给冲突的包定义一个新的本地访问器名称就可以了。
import (
"github.com/chalvern/sugar"
log "github.com/chalvern/sugar"
)
// 同样都是 "github.com/chalvern/sugar",但是可以设置不同的别名
// 如果不设置别名,默认是 `sugar`
sugar.Info("hello sugar")
log.Info("hello log")
导入包的时候使用的是包的全路径(比如 "github.com/chalvern/sugar"
),它唯一决定了使用的是哪个包,因此即使出现了另一个名为 sugar
的包,使用上也不会出现混淆。
另一个约定是,包名一般设置为它的源文件所在的目录名,比如目录 src/encoding/base64
定义的包引入时的路径是 "encoding/base64"
,我们应该把包名定义为base64
,不应该写成 encoding_base64,也不应该写成 encodingBase64。
一个包被导入后一般通过它的包名来访问其内容,因此在包中导出的名字可以借助包名来避免含糊不清(不要在源文件中使用 import . package/name
的方式导入包,否则包里的名称很容易与源文件夹中的名称混淆)。比如,在 bufio
包里带缓存的读类型应该是 Reader 而不是 BufReader, 因为当开发者看到 bufio.Reader
的时候就已经知道这是个带缓存的 Reader 了,这种方式的定义不仅简洁而且不失准确性。进一步讲,因为被导入的实体经常与他们的包名一起取用,因此 bufio.Reader
和 io.Reader
虽然都是 Reader
但是二者是不会冲突的。同样的道理,对于包 ring,包里包含唯一导出的 Ring
结构体,创建 Ring
的新对象的函数按一般的思维可以被命名为 NewRing()
,但是因为 Ring
是包里唯一导出的类型,并且因为包名叫 ring
,因此这个创建新对象的方法可以命名为 New()
,这样使用这个包的调用方就可以通过 ring.New()
来创建 Ring
的对象了(是不是感觉很简单)。总之,根据包的这些使用规范来选择一个好名称吧。
另一个短小精悍的例子是 once.Do
,once.Do(setup)
读起来自然且流畅,如果把名字改成 once.DoOrWaitUntilDone(setup)
好像并没有必要。长名称并不会让事物更具有可读性。相对于长的命名方式,注释文档往往具有有更高的价值。
获取器(Getters)
Go 并没有对获取器(getter)和设置器(setter)提供自动的支持;不过我们可以自己提供获取器和设置器,而且一般也推荐这么做;但是对 Go 语言来说把 Get 放进获取器的名字里显得既不明智也没太大必要。比如一个结构体有一个名为 owner
的字段(小写,意味着非导出,在包外不能直接访问这个变量),它的获取器的名字应该叫 Owner()
(大写字母,可导出),而不是 GetOwner()
。在 Go 语言中大写字母名称可以导出,这种机制使方法名与字段名可以不冲突,不需要添加 Get
就可以满足我们的需求了。假如设置器函数是必须的,设置器函数可以取名为 SetOwner()
,这个时候就要加上 Set
这个单词了(这里或许有点过头了。。)。如此,下面的代码读起来都很流畅:
owner := obj.Owner()
if owner != user {
obj.SetOwner(user)
}
接口名
通常情况下,只有一个方法的接口的名称是方法名加 “er” 后缀,或者类似构造而成的代理名词,比如:Reader, Writer, Formatter, CloseNotifier 等。这种命名方式有许多被开发者熟知的接口以及方法。
以 Read, Write, Close, Flush, String 等命名方法都有规范的签名(声明)和含义;为了避免混淆,你自己的方法除非有相同的签名和含义,否则不要以这些名字命名。而假如自定义的类型存在一个方法和某个已存的类型的方法功能相似,这个时候取相同的名字就可以了。比如假如你定义了一个字符串转换的方法,直接命名为 String
而不要命名为 ToString
。
混合驼峰式命名
最后,Go 惯例中使用混合驼峰 MixedCaps 或 mixedCaps 来组织多个单词组合的名称,不推荐使用下划线的方式。
分号
像 C 语言那样,Go 语言使用分号来结束语句;但是和 C 不同的是,这些分号并不在源码中出现。实际上,词法分析器通过一种简单的规则自动插入分号,从而允许在源码中不写分号。
分号插入的规则是:如果 ① 行尾符号的最后一个符号是一个标识符(包括 int 和 float64 等)、② 数字或字符串常量之类的基本字面、或者是 ③ break continue fallthrough return ++ -- ) }
其中任意一个标记,词法分析器就会在这些标记后面插入一个分号。可以简单总结为:“如果一个标识符的下一个字符是行尾符,且这个标识符可以终止一条语句,就插入一个分号”。
紧接在右大括号前面的分号可以省略,因此表达式 go func() { for { dst <- <-src } }()
不会插入任何的分号。Go 程序中只有一种显式使用分号的情况就是 for
循环语句,通过分号来分割初始化语句、条件语句和迭代变量修改语句。当然,如果你想在一行里写多条语句,应该显式地使用分号来分割这些语句。
上面讲到的分号插入规则存在一个局限性:你不可以把控制结构语句(if
, for
, switch
, 或者 select
)的左大括号放在新的一行,而是应该放在同一行中。如果你把 if
, for
, switch
, 或者 select
紧跟着的大括号放在新的一行,根据分号的插入规则,左大括号前面会被插入一个分号,这肯定不是我们想要的效果。可以看下面的例子及其反例:
if i < f() {
g()
}
不应该写成下面的样子:
if i < f() // wrong!
{ // wrong!
g()
}