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)
另一方面,如果缩写包名使其变得模棱两可或不清楚,则不要这样做。
不要占用用户的良好名称。避免给包起一个在客户端代码中常用的名称。例如,带缓冲的 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 环境变量在目录 $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 中的可测试示例
上一篇文章:错误是值
博客索引