全文翻译自go.dev官方博客。其中穿插着一些自己的笔记。
介绍
Go代码是以包进行组织的。在包内,代码可以引用其中定义的任何标识符。而包的客户(即import该包的代码)只能引用包的导出类型、函数、常量和变量(以大写字符开头的)。这类引用始终包含了包名作为其前缀:foo.Bar
引用foo
包中的导出名Bar
好的包命名可以让代码更好。包名为其内容提供了上下文(语义),使客户更容易理解包的用途。同时其也能帮助包自身的维护者确定在扩展功能时,哪些内容仍然改属于这个包,哪些内容不该属于这个包。良好的包命名可以帮助你轻松找到所需代码。
Effective Go提供了包、类型、函数和变量的命名指南。本文则扩展包命名的讨论,并调查在标准库中发现的名称。并且,我们将讨论一些错误的包名和修复他们的方法。
包名
好的包名是简短清晰的。小写,无下划线和驼峰。通常是简单名词,举例来说:
- time (提供了测量和显示时间功能的包)
- list (实现了双向链表的包)
- http (提供HTTP客户端/服务端实现的包)
以下是两个名称示例,遵循了另一些语言的典型命名风格。这些名称在其他语言中可能很好,但在 Go 中不太适合:
- computeServiceClient
- priority_queue
一个 Go 包可以导出很多类型和函数。举例来说,compute
包可以导出一个Client
类型,其中包含使用服务的方法,以及跨越多个客户端划分计算任务的函数。
审慎地缩写。当缩写对程序员很熟悉时,包名才可以缩写。常用的包有这些缩写了的名字:
- strconv -> string conversion
- syscall -> system call
- fmt -> formatted I/O
如果缩写会让包名含糊不清,那不如不要缩写。
不要从用户手中偷取好名字。避免给包使用一个客户常用的命名。比如说,缓冲I/O包叫做bufio
而不是buf
,因为客户经常用buf
来命名缓冲区。
命名包内容
考虑的客户会同时使用包和其内容,这两者的命名应是耦合的。设计包时应该站在客户的角度。
避免重复。由于客户代码在引用包内容时使用包名称作为前缀,因此这些内容的名称不需要重复包名称。 http
包提供的 HTTP 服务器称为 Server
,而不是 HTTPServer
。客户端代码将此类型称为 http.Server
,因此没有歧义,不需要再重复一遍。
简化函数名。当包pkg
中的函数返回一个pkg.Pkg
(或*pkg.Pkg
)类型的值时,函数名称可以省略类型名称而不会造成混淆,比如:
1 | start := time.Now() // start is a time.Time |
在包中,一个名字为New
的函数返回一个pkg.Pkg
类型。这是使用该类型的客户代码的标准进入点:
1 | q := list.New() // q is a *list.List |
当函数返回 pkg.T
类型的值时,其中 T
不是 Pkg
,函数名称可以包含 T
以生成客户端代码更容易理解。常见的情况是一个包有多个类似 New 的函数:
1 | d, err := time.ParseDuration("10s") // d is a time.Duration |
不同包中的类型可以具有相同的名称,因为从客户端的角度来看,这些名称是通过包名称来区分的。例如,标准库包含多种名为 Reader
的类型,包括 jpeg.Reader
、 bufio.Reader
和 csv.Reader
。每个包名称都适合 Reader
以产生良好的类型名称。
如果你想不出一个对于包内容来说有意义的包名前缀,那么包的抽象边界可能是有错误的。在编写代码时,考虑客户会如何使用这个包,如果考虑的结果很差就要及时重构。这种方法将产生更容易让客户理解、更容易让开发人员维护的包。
包路径
Go包同时拥有名字和路径。名字是在其源代码的package语句中指定的,同时客户代码会用它作为包名导出的前缀。客户端代码导入包时,则使用包的路径。按照约定,包路径的最后一个元素就是包名。
1 | import ( |
构建工具会将包路径映射到目录上(这一段是关于Go老版本GOPATH的东西,略过)。
目录。标准库使用 crypto
、 container
、 encoding
和 image
等目录,对相关协议和算法的包进行分组。在这些目录中的包没有什么实际的关系。目录只是提供一种排列文件的方法。在不创建循环依赖的情况下,任何包都可以导入其他包。
此处我可以给出个人测试的案例。假设有这样一个路径和这样一段代码(这个包是go init demo):
.
├── go.mod
├── main.go
└── abcdefg
└── code.go其中,
code.go
的定义是:
1
2
3
4
5 package a
func A() {
//do something
}当你试图在
main.go
中使用这个包,你需要:
1
2
3
4
5
6
7
8
9 package main
import (
a "demo/abcdefg"
)
func main() {
a.A()
}也就是说import使用的是路径,而package声明则代表了客户使用包的名字。约定中包名应该和路径最后的名字保持一致。如果没有,
go fmt
会自动在import语句前给你添上那个包名。另外,一个目录里不能有多个不同的package声明。
另另外,package main是一个特殊的包名,标识程序入口点。如果没有package main,那么
go run .
无法运行,func main()
也不会被识别。
正如不同包中的类型可以具有相同的名称而不会产生歧义一样,不同目录中的包也可以具有相同的名称。例如,runtime/pprof 以 pprof 分析工具期望的格式提供分析数据,而 net/http/pprof 则提供 HTTP 端点来以这种格式呈现分析数据。客户端代码使用包路径来导入包,因此不会出现混乱。如果源文件需要导入两个 pprof
包,它可以在本地重命名一个或两个包。重命名导入的包时,本地名称应遵循与包名称相同的准则(小写,没有下划线或驼峰 )。
错误的包名
错误的包名称会使代码更难导航和维护。以下是一些识别和修复不良名称的指南。
避免无意义的包名称。名为 util
、 common
或 misc
的包让客户端不知道包包含什么内容(天呐,这简直就是我)。这使得客户更难使用该包,也使得维护人员更难保持该包的重点。随着时间的推移,它们会积累依赖关系,从而使编译速度明显变慢,尤其是在大型程序中。由于此类包名称是通用的,因此它们更有可能与客户端代码导入的其他包发生冲突,从而迫使客户端发明名称来区分它们。
分解通用包。要修复此类包,请查找具有通用名称元素的类型和函数,并将它们拉入自己的包中。例如,如果有
1 | package util |
那么客户的代码就会看起来像
1 | set := util.NewStringSet("c", "a", "b") |
我们将这些函数从util
提取到一个新的包并想一个合适的名字:
1 | package stringset |
这样客户端代码就会变成:
1 | set := stringset.New("c", "a", "b") |
进行此更改后,可以更轻松地了解如何扩展增强这个新包:
1 | package stringset |
这样客户的代码就会更加简单:
1 | set := stringset.New("c", "a", "b") |
包的名称是其设计的关键部分。应当努力从项目中消除无意义的包名称。
不要对你的所有API使用一个单独的包。许多善意的程序员将他们的程序公开的所有接口放入一个名为 api
、 types
或 interfaces
的包中,认为这样更容易找到入口点指向他们的代码库。这是个错误。此类包与那些名为 util
或 common
的包存在相同的问题,无限制地增长、不向用户提供指导、累积依赖项以及与其他导入发生冲突。将它们分解,也许使用目录将公共包与实现分开。
避免不必要的包名冲突。虽然不同目录中的包可能具有相同的名称,但经常一起使用的包应该具有不同的名称。这减少了混乱以及客户端代码中本地重命名的需要。出于同样的原因,请避免使用与 io
或 http
等流行标准包相同的名称。
结论
包名称是 Go 程序中良好命名的核心。花时间选择好的包名称并组织好您的代码。这有助于客户理解和使用您的包,并帮助维护人员优雅地扩展它们。