Go 博客
包名
简介
Go 代码以包为单位进行组织。在一个包内部,代码可以引用包内定义的任何标识符(名称),而包的客户端只能引用包导出的类型、函数、常量和变量。这类引用总是包含包名作为前缀:foo.Bar
指的是导入的名为 foo
的包中导出的名称 Bar
。
好的包名能让代码更好。包名提供了其内容的上下文,使客户端更容易理解包的作用以及如何使用它。该名称还有助于包维护者在包演进过程中确定哪些内容属于或不属于该包。命名良好的包使得查找所需的代码更加容易。
高效 Go 编程提供了关于包、类型、函数和变量命名的指导方针。本文在此讨论的基础上进行扩展,并考察了标准库中的命名。本文还讨论了不好的包名以及如何改进它们。
包名
好的包名简洁明了。它们使用小写字母,不包含 under_scores
或 mixedCaps
。它们通常是简单的名词,例如
time
(提供时间测量和显示功能)list
(实现双向链表)http
(提供 HTTP 客户端和服务器实现)
另一种语言中典型的命名风格在 Go 程序中可能不符合习惯。以下是两个在其他语言中可能风格良好但在 Go 中不太合适的名称示例
computeServiceClient
priority_queue
一个 Go 包可以导出多种类型和函数。例如,一个 compute
包可以导出一个带有使用该服务方法的 Client
类型,以及用于将计算任务分配到多个客户端的函数。
谨慎地缩写。当缩写为程序员所熟悉时,可以缩写包名。广泛使用的包通常采用压缩名称
strconv
(字符串转换)syscall
(系统调用)fmt
(格式化输入/输出)
另一方面,如果缩写包名使其变得模糊不清,就不要这样做。
不要占用用户常用的好名称。避免给包命名为客户端代码中常用的名称。例如,缓冲 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
在包 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 包同时拥有名称和路径。包名在其源文件的 package 声明中指定;客户端代码将其用作包导出名称的前缀。客户端代码在导入包时使用包路径。按照惯例,包路径的最后一个元素就是包名。
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 程序中良好命名的核心。花时间选择好的包名并妥善组织您的代码。这有助于客户端理解和使用您的包,并有助于维护者使其优雅地成长。
进一步阅读
下一篇文章:Go 中的可测试示例
上一篇文章:错误是值
博客索引