Go 博客

包名

Sameer Ajmani
2015年2月4日

简介

Go 代码被组织成包。在一个包内,代码可以引用包内定义的任何标识符(名称),而包的客户端只能引用包导出的类型、函数、常量和变量。此类引用始终包含包名作为前缀:foo.Bar 指的是名为 foo 的导入包中导出的名称 Bar

良好的包名使代码更易于维护。包的名称为其内容提供了上下文,使客户端更容易理解包的用途和使用方法。该名称还有助于包维护人员在包发展过程中确定哪些内容属于包,哪些内容不属于包。命名良好的包使查找所需的代码变得更容易。

《Go 语言有效实践》提供了关于命名包、类型、函数和变量的指南。本文将进一步探讨该讨论,并调查标准库中发现的名称。它还讨论了不良的包名以及如何修复它们。

包名

良好的包名简短明了。它们是小写字母,没有 under_scoresmixedCaps。它们通常是简单的名词,例如

  • 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.Readerbufio.Readercsv.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" 的源文件。(当然,这种情况应该很熟悉,但明确包的术语和结构很重要。)

目录。标准库使用 cryptocontainerencodingimage 等目录对相关协议和算法的包进行分组。这些目录中的包之间没有任何实际关系;目录只提供了一种组织文件的方式。任何包都可以导入任何其他包,前提是导入不会创建循环。

就像不同包中的类型可以具有相同的名称而不会产生歧义一样,不同目录中的包也可以具有相同的名称。例如,runtime/pprof 提供了 pprof 分析工具期望格式的分析数据,而 net/http/pprof 提供了 HTTP 端点以这种格式呈现分析数据。客户端代码使用包路径导入包,因此不会产生混淆。如果源文件需要导入两个 pprof 包,它可以本地重命名其中一个或两个。重命名导入的包时,本地名称应遵循与包名相同的准则(小写字母,没有 under_scoresmixedCaps)。

不良的包名

不良的包名使代码更难导航和维护。以下是一些识别和修复不良名称的准则。

避免无意义的包名。名为 utilcommonmisc 的包没有向客户端提供任何有关包内容的信息。这使得客户端更难使用该包,也使维护人员更难保持包的重点。随着时间的推移,它们会累积依赖项,这会使编译速度明显且不必要地变慢,尤其是在大型程序中。而且,由于此类包名称是通用的,因此它们更有可能与客户端代码导入的其他包发生冲突,迫使客户端发明名称来区分它们。

分解通用包。要修复此类包,请查找具有共同名称元素的类型和函数,并将它们提取到它们自己的包中。例如,如果您有

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 放入单个包中。许多善意的程序员将程序公开的所有接口都放入名为 apitypesinterfaces 的单个包中,认为这使得更容易找到代码库的入口点。这是一个错误。此类包与名为 utilcommon 的包遇到相同的问题,无限增长,不提供用户指南,累积依赖项并与其他导入发生冲突。将它们分解,也许使用目录将公共包与实现分开。

避免不必要的包名冲突。虽然不同目录中的包可以具有相同的名称,但经常一起使用的包应具有不同的名称。这减少了混淆以及客户端代码中需要本地重命名的次数。出于同样的原因,避免使用与 iohttp 等流行的标准包相同的名称。

结论

包名是 Go 程序中良好命名的核心。花时间选择良好的包名并很好地组织您的代码。这有助于客户端理解和使用您的包,并帮助维护人员优雅地扩展它们。

进一步阅读

下一篇文章:Go 中的可测试示例
上一篇文章:错误是值
博客索引