Go 博客
命名的学问
2015/02/04
引言
Go 代码通过包来组织。在包内部,代码可以引用包中定义的任何标识符(名字),
而该包的用户则只能引用其中已导出的类型、函数、常量和变量。
这种引用总是将包名作为前缀:例如 foo.Bar
就引用了已导入包 foo
中的已导出名 Bar
。
好的包名能让代码组织得更好。包名提供了其内容的上下文,让用户更容易理解该包的用途和用法。 包名也有助于维护者在该包的演化过程中决定哪些东西属于它,哪些不属于它。 恰当的包名更易帮你找所需的代码。
实效 Go 编程中提供了一份关于包、类型、函数和变量的 命名指南。 本文拓展了该指南,考察了标准库中的命名,也讨论了不良包名及其改善方法。
包名
好的包名应当简短清晰。它们应该全部小写,没有下划线 under_scores
或混合大小写
mixedCaps
。它们通常是简单的名词,例如:
time
(提供测量和显示时间的功能)list
(实现了双向链表)http
(提供 HTTP 客户端与服务端的实现)
另一种语言中典型的命名风格在 Go 程序中可能并不惯用。 下面两个例子在其它语言中可能是好的风格,但在 Go 中不合适:
computeServiceClient
priority_queue
一个Go包可导出多个类型和函数。例如,一个 compute
包可以导出一个 Client
类型,
它包含使用该服务的方法,以及将一个计算任务划分给多个客户端( Client
)的函数。
慎用缩写。 当程序员对包名缩写比较熟悉时,可直接采用。常用包一般都有简短的名字:
strconv
(字符串转换)syscall
(系统调用)fmt
(格式化 I/O)
反之,若包名缩写会产生歧义,那么请勿使用。
别抢走用户的好名字。
别把用户代码中常用的名字作为包名。例如,带缓冲的 I/O 叫做 bufio
而非 buf
,
因为 buf
用做缓冲区是个不错的变量名。
包内容的命名
包名与其内容的名字是有联系的,因为用户的代码总是一起使用它们。当设计一个包时, 请站在用户的角度考虑。
别啰嗦。
由于用户代码在引用包的内容时会将包名作为前缀,因此这些内容的名字无需重复包名。
http
包提供的 HTTP 服务名为 http.Server
,而非 HTTPServer
。用户代码通过
http.Server
引用该类型,因此没有歧义。
简化函数名。
当 pkg
包中某个函数的返回值类型为 pkg.Pkg
(或 *pkg.Pkg
)时,
函数名就算省略类型名也不会引起混淆。
start := time.Now() // start is a time.Time t, err := time.Parse(time.Kitchen, "6:06PM") // t is a time.Time ctx = context.WithTimeout(ctx, 10*time.Millisecond) // ctx is a context.Context ip, ok := userip.FromContext(ctx) // ip is a net.IP
在 pkg
包中名为 New
的函数会返回一个 pkg.Pkg
类型的值。
这是用户代码使用该类型的标准入口点:
q := list.New() // q is a *list.List
当函数返回的值类型为 pkg.T
且 T
不为 Pkg
时,函数名应包含 T
以便让用户代码更易理解。常见的情况是一个包带有多个类似 New
的函数:
d, err := time.ParseDuration("10s") // d is a time.Duration elapsed := time.Since(start) // elapsed is a time.Duration ticker := time.NewTicker(d) // ticker is a *time.Ticker timer := time.NewTimer(d) // timer is a *time.Timer
不同包中的类型名可以相同,因为客户端可通过包名区分它们。
例如,标准库中含有多个名为 Reader
的类型,包括 jpeg.Reader
、 bufio.Reader
和 csv.Reader
。每个包名搭配 Reader
都是个不错的类型名。
若你为某个包的内容想不出有意义的前缀包名,那这个包的抽象边界大概就错了。 请站在用户的角度写你的包的代码,如果你觉得不太对劲那么请重新组织你的包。 这样得到的包将不仅容易使用,也容易维护。
导入路径
一个 Go 包同时有名称和路径。包名由其源码文件中的包语句指定, 用户代码将其用作包的已导出名的前缀。用户代码通过包路径来导入包。 按照约定,包路径的最后一个元素即为包名:
import ( "context" // package context "fmt" // package fmt "golang.org/x/time/rate" // package rate "os/exec" // package exec )
构建工具将包路径映射到目录。go 工具通过 GOPATH
环境变量在 $GOPATH/src/github.com/user/hello
目录中查找路径 "github.com/user/hello"
内的源文件。
(这种情况你应该熟悉,但弄清这些术语和包结构也很重要。)
目录。
标准库使用像 crypto
、 container
、 encoding
和 image
之类的目录来为相关的协议和算法包分组。其中每个目录内的包之间并没有实际的联系,
目录只是为了便于分类文件。只要不会导致循环引用,任何包都能导入其它的包。
就像不同包中的类型可以同名而不会引起混淆,不同目录中的包也可以同名。
例如,runtime/pprof 提供了
pprof 剖析工具所需格式的剖析数据,
而 net/http/pprof
则提供了按这种格式展示剖析数据的HTTP端点。用户代码通过包路径导入该包,因此不会混淆。
若源文件需要导入两个 pprof
包,那么可以局部地将其中之一或二者都
重命名。在重命名已导入包时,
局部名也应遵循包名的命名准则(小写,不使用下划线 under_scores
或混合大小写 mixedCaps
)。
不良包名
不良包名会让代码难以使用与维护。下面是一些识别及修复不良包名的准则。
避免无意义的包名。
名为 util
、 common
或 misc
的包不仅无法向用户传达其中的内容,还会让它们更难以使用,
维护者也不易保持它们的专用性。如此一来,依赖关系会日渐复杂,徒增编译时间,这在大型程序中尤甚。
由于这类包名过于通用,因此更易与客户代码中导入的其它包名相冲突,用户则必须重新取名来加以区分。
拆分过于通用的包。 修复这样的包需要包含通用名元素的类型和函数,并将它们放到自己的包中。例如,若你有以下代码:
package util func NewStringSet(...string) map[string]bool {...} func SortStringSet(map[string]bool) []string {...}
那么用户代码看起来会是这样:
set := util.NewStringSet("c", "a", "b") fmt.Println(util.SortStringSet(set))
将这些函数从 util
移至新的包中,选一个与其内容相称的包名:
package stringset func New(...string) map[string]bool {...} func Sort(map[string]bool) []string {...}
那么用户代码会变成这样:
set := stringset.New("c", "a", "b") fmt.Println(stringset.Sort(set))
一旦你做出这些改变,便更容易看出如何改进新包了:
package stringset type Set map[string]bool func New(...string) Set {...} func (s Set) Sort() []string {...}
这样以来用户代码将更加简洁:
set := stringset.New("c", "a", "b") fmt.Println(set.Sort())
名字是设计包的关键。请从你的项目中努力消除无意义的包名。
别把所有API都塞进一个包里。
有些好心的程序员会将他们的程序暴露出的所有接口都放到一个包里,取名为 api
、`types` 、或
interfaces
,他们觉着这样会更易于找到代码库的入口点。这是不对的。这种包遭遇到的问题和名为
util
或 common
的包相同,即无节制地增长,不为用户提供指导,积累依赖,以及和其它导入冲突。
请拆分它们,或许可以用目录将公共包从实现中分离出来。
避免不必要的包名冲突。
当不同目录中的包名相同时,经常一起使用的包应该有不同的名字。这能避免混淆,
减少在用户代码中局部重命名的必要。同理,也应当避免将 io
或 http
之类广泛使用的标准包的名字用作包名。
结论
Go 程序的包名是良好命名的核心,请花点时间选好包名并组织好你的代码, 这能帮助用户理解并使用你的包,也有助于维护者优雅地发展它们。