抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

全文翻译自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
2
3
4
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类型。这是使用该类型的客户代码的标准进入点:

1
q := list.New()		// q is a *list.List

当函数返回 pkg.T 类型的值时,其中 T 不是 Pkg ,函数名称可以包含 T 以生成客户端代码更容易理解。常见的情况是一个包有多个类似 New 的函数:

1
2
3
4
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包同时拥有名字和路径。名字是在其源代码的package语句中指定的,同时客户代码会用它作为包名导出的前缀。客户端代码导入包时,则使用包的路径。按照约定,包路径的最后一个元素就是包名。

1
2
3
4
5
6
import (
"context" // package context
"fmt" // package fmt
"golang.org/x/time/rate" // package rate
"os/exec" // package exec
)

构建工具会将包路径映射到目录上(这一段是关于Go老版本GOPATH的东西,略过)。

目录。标准库使用 cryptocontainerencodingimage 等目录,对相关协议和算法的包进行分组。在这些目录中的包没有什么实际的关系。目录只是提供一种排列文件的方法。在不创建循环依赖的情况下,任何包都可以导入其他包。

此处我可以给出个人测试的案例。假设有这样一个路径和这样一段代码(这个包是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 包,它可以在本地重命名一个或两个包。重命名导入的包时,本地名称应遵循与包名称相同的准则(小写,没有下划线或驼峰 )。

错误的包名

错误的包名称会使代码更难导航和维护。以下是一些识别和修复不良名称的指南。

避免无意义的包名称。名为 utilcommonmisc 的包让客户端不知道包包含什么内容(天呐,这简直就是我)。这使得客户更难使用该包,也使得维护人员更难保持该包的重点。随着时间的推移,它们会积累依赖关系,从而使编译速度明显变慢,尤其是在大型程序中。由于此类包名称是通用的,因此它们更有可能与客户端代码导入的其他包发生冲突,从而迫使客户端发明名称来区分它们。

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

1
2
3
package util
func NewStringSet(...string) map[string]bool {...}
func SortStringSet(map[string]bool) []string {...}

那么客户的代码就会看起来像

1
2
set := util.NewStringSet("c", "a", "b")
fmt.Println(util.SortStringSet(set))

我们将这些函数从util提取到一个新的包并想一个合适的名字:

1
2
3
package stringset
func New(...string) map[string]bool {...}
func Sort(map[string]bool) []string {...}

这样客户端代码就会变成:

1
2
set := stringset.New("c", "a", "b")
fmt.Println(stringset.Sort(set))

进行此更改后,可以更轻松地了解如何扩展增强这个新包:

1
2
3
4
package stringset
type Set map[string]bool
func New(...string) Set {...}
func (s Set) Sort() []string {...}

这样客户的代码就会更加简单:

1
2
set := stringset.New("c", "a", "b")
fmt.Println(set.Sort())

包的名称是其设计的关键部分。应当努力从项目中消除无意义的包名称。

不要对你的所有API使用一个单独的包。许多善意的程序员将他们的程序公开的所有接口放入一个名为 apitypesinterfaces 的包中,认为这样更容易找到入口点指向他们的代码库。这是个错误。此类包与那些名为 utilcommon 的包存在相同的问题,无限制地增长、不向用户提供指导、累积依赖项以及与其他导入发生冲突。将它们分解,也许使用目录将公共包与实现分开。

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

结论

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

评论