小马别过河
2022/11/02阅读:40主题:默认主题
Go[译]:包的命名
介绍
Go 是以包(package)的形式组织代码的。包内部的类型可以相互引用,而包的调用者只能引用公开类型:函数、常量和变量等。这些公开类型的引用会以包名作为前缀:foo.Bar
表示 foo
包的 Bar
变量。
好的包名会使代码更优雅。一个包名为它的内容提供上下文,便于使用者理解包的用途与用法。包名还替开发者明确了,将来的更新迭代,哪些新功能属于这个包,哪些不属于。命名良好的包更便于你找到你要的代码。
Effective Go (https://go.dev/doc/effective_go#names)对包、类型、函数和变量的命名的有一些引导。本文对此进行扩展,并以标准库为例。同时也列举了一些不好的命名,和优化的方法。
包名
良好的包名清晰且简短。它们是小写的,不是蛇形(如under_scores
)或者驼峰(如mixedCaps
)。往往是简单的名词,如:
-
time
(使用或展示时间) -
list
(实现双向列表) -
http
(实现HTTP客户端和服务端)
其他语言常见命名风格可能不适用于 Go。比如以下的两个例子,别的语言可能很常见,但是作为 Go 的包名是不合适的:
-
computeServiceClient
-
priority_queue
适当地缩写。如果某些缩写被广泛接受,包名可以适当缩写。很多常见的包也都使用缩写:
-
strconv
(string conversion) -
syscall
(system call) -
fmt
(formatted I/O)
另一方面,如果包名缩写后变得有歧义,就别这么做了。
不要占用调用者的好名字。包的命名,要避免使用客户端也会使用的名字。比如,buffered I/O
包命名为bufio
,而不是buf
,因为buf
是 buffer 的一个常见的名称。
包内容命名
包的名称和它的内容是耦合的,因为使用者的代码会一起引用他们。设计一个包时,请从使用者的角度出发。
避免重复。由于调用者引用包内容的时候,使用包名作为前缀,所以内容不需要再重复包名。http
包提供的 Http Server 名称为 Server
,而不是 HttpServer
。调用者引用它的方式是 http.Server
(而不是http.HttpServer
),所以不会重复。
简化函数名。当pkg
包的一个函数返回值为pkg.Pkg
(或*pkg.Pkg
),函数名的类型名称一般可以忽略:
start := time.Now() // 而不是 time.NowTime()
t, err := time.Parse(time.Kitchen, "6:06PM") // 而不是 time.ParseTime()
ctx = context.WithTimeout(ctx, 10*time.Millisecond) // 而不是 context.WithTimeoutContext()
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 可以使调用者代码更易于理解。一个常用的场景是一个包内有多个 NewXXX
函数:
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
都是好的命名方式。
如果你不能根据包的内容想出一个合适的包名,那么包的抽象边界可能是错的。试着编写客户端代码,如果结果看起来很差,请重新组织代码。这个方法会使包更易于使用和维护。
包的路径
一个包拥有包名和路径。包名在源文件中指定,调用者使用包名作为包内容的前缀。调用者使用路径引入一个包。按照惯例,包路径的最后一个元素就是包名:
import (
"context" // package context
"fmt" // package fmt
"golang.org/x/time/rate" // package rate
"os/exec" // package exec
)
构建工具将包映射到目录。go 工具使用GOPATH
环境变量来找源文件,如 github.com/user/hello
的目录为 $GOPATH/src/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))
把这些函数归并到一个新的包里面,并选择一个合适的命名:
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程序中良好命名的核心。花时间选择好的包名并组织代码,这会让使用者了解你的包,同时方便维护,使其更加优雅。
作者介绍