Go 博客
包名
引言
Go 代码组织在包中。在包内部,代码可以引用包中定义的任何标识符(名称),而包的客户端只能引用包的导出类型、函数、常量和变量。此类引用始终包含包名作为前缀:foo.Bar
引用了名为 foo
的已导入包中的导出名称 Bar
。
好的包名能让代码更好。包的名称为其内容提供了上下文,使客户端更容易理解包的作用以及如何使用它。名称也有助于包维护者确定包在演进过程中哪些内容属于它,哪些不属于它。命名良好的包使查找所需代码更加容易。
Effective Go 提供了有关命名包、类型、函数和变量的指南。本文扩展了该讨论,并调查了标准库中发现的名称。它还讨论了不良的包名称以及如何修复它们。
包名
好的包名简短而清晰。它们是小写的,没有 下划线
或 驼峰式混合大小写
。它们通常是简单的名词,例如
time
(提供测量和显示时间的功能)list
(实现双向链表)http
(提供 HTTP 客户端和服务器实现)
其他语言中典型的名称风格在 Go 程序中可能不符合惯例。以下是两个在其他语言中可能是良好风格但在 Go 中不适合的名称示例
computeServiceClient
priority_queue
一个 Go 包可以导出几种类型和函数。例如,一个 compute
包可以导出一个 Client
类型,其中包含用于使用该服务的相关方法以及用于在多个客户端之间划分计算任务的函数。
明智地缩写。 当程序员熟悉缩写时,包名可以被缩写。广泛使用的包通常有缩短的名称
strconv
(字符串转换)syscall
(系统调用)fmt
(格式化 I/O)
另一方面,如果缩写包名会使其含糊不清或不明确,则不要这样做。
不要窃取用户的好名字。 避免给包命名一个在客户端代码中常用的名称。例如,缓冲 I/O 包名为 bufio
,而不是 buf
,因为 buf
是一个好的缓冲区变量名。
命名包内容
包名及其内容的名称是耦合的,因为客户端代码将它们一起使用。在设计包时,请从客户端的角度出发。
避免重复。 由于客户端代码在引用包内容时使用包名作为前缀,因此这些内容的名字不必重复包名。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
名为 New
的函数在 pkg
包中返回类型为 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 环境变量来查找路径 "github.com/user/hello"
的源文件,目录是 $GOPATH/src/github.com/user/hello
。(这种情况应该很熟悉,当然,但清楚包的术语和结构很重要。)
目录。 标准库使用 crypto
、container
、encoding
和 image
等目录来分组相关协议和算法的包。这些目录中的包之间没有实际关系;目录只是提供了一种组织文件的方式。任何包都可以导入任何其他包,前提是导入不产生循环。
就像不同包中的类型可以无歧义地拥有相同的名称一样,不同目录中的包也可以拥有相同的名称。例如,runtime/pprof 以 pprof 分析工具期望的格式提供分析数据,而 net/http/pprof 提供 HTTP 端点以该格式呈现分析数据。客户端代码使用包路径导入包,因此没有混淆。如果源文件需要导入两个 pprof
包,它可以本地重命名其中一个或两个。重命名已导入的包时,本地名称应遵循与包名相同的指南(小写,无 下划线
或 驼峰式混合大小写
)。
不良的包名
不良的包名会使代码更难导航和维护。以下是一些识别和修复不良名称的指南。
避免无意义的包名。 命名为 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 程序中良好命名的核心。花时间选择好的包名并妥善组织代码。这有助于客户端理解和使用您的包,并帮助维护者优雅地扩展它们。
延伸阅读
下一篇文章:Go 中的可测试示例
上一篇文章:错误是值
博客索引