小马别过河

V1

2022/11/02阅读:40主题:默认主题

Go[译]:包的命名

介绍

Go 是以包(package)的形式组织代码的。包内部的类型可以相互引用,而包的调用者只能引用公开类型:函数、常量和变量等。这些公开类型的引用会以包名作为前缀:foo.Bar 表示 foo 包的 Bar 变量。

好的包名会使代码更优雅。一个包名为它的内容提供上下文,便于使用者理解包的用途与用法。包名还替开发者明确了,将来的更新迭代,哪些新功能属于这个包,哪些不属于。命名良好的包更便于你找到你要的代码。

Effective Go (https://go.dev/doc/effective_go#names)对包、类型、函数和变量的命名的有一些引导。本文对此进行扩展,并以标准库为例。同时也列举了一些不好的命名,和优化的方法。

包名

良好的包名清晰且简短。它们是小写的,不是蛇形(如under_scores)或者驼峰(如mixedCaps)。往往是简单的名词,如:

  • time(使用或展示时间)
  • list(实现双向列表)
  • http(实现HTTP客户端和服务端)

其他语言常见命名风格可能不适用于 Go。比如以下的两个例子,别的语言可能很常见,但是作为 Go 的包名是不合适的:

  • computeServiceClient
  • priority_queue

适当地缩写。如果某些缩写被广泛接受,包名可以适当缩写。很多常见的包也都使用缩写:

  • strconv(string conversion)
  • syscall(system call)
  • fmt(formatted I/O)

另一方面,如果包名缩写后变得有歧义,就别这么做了。

不要占用调用者的好名字。包的命名,要避免使用客户端也会使用的名字。比如,buffered I/O包命名为bufio,而不是buf,因为buf是 buffer 的一个常见的名称。

包内容命名

包的名称和它的内容是耦合的,因为使用者的代码会一起引用他们。设计一个包时,请从使用者的角度出发。

避免重复。由于调用者引用包内容的时候,使用包名作为前缀,所以内容不需要再重复包名。http包提供的 Http Server 名称为 Server,而不是 HttpServer。调用者引用它的方式是 http.Server(而不是http.HttpServer),所以不会重复。

简化函数名。当pkg包的一个函数返回值为pkg.Pkg(或*pkg.Pkg),函数名的类型名称一般可以忽略:

start := time.Now()                                  // 而不是 time.NowTime()
t, err := time.Parse(time.Kitchen, "6:06PM")         // 而不是 time.ParseTime()
ctx = context.WithTimeout(ctx, 10*time.Millisecond)  // 而不是 context.WithTimeoutContext()
ip, ok := userip.FromContext(ctx)                    // ip is a net.IP

pkg包的New方法返回一个pkg.Pkg,这是调用者使用这个类型的标准入口点:

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

当一个函数返回一个pkg.TT不是Pkg,函数名包含 T 可以使调用者代码更易于理解。一个常用的场景是一个包内有多个 NewXXX 函数:

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都是好的命名方式。

如果你不能根据包的内容想出一个合适的包名,那么包的抽象边界可能是错的。试着编写客户端代码,如果结果看起来很差,请重新组织代码。这个方法会使包更易于使用和维护。

包的路径

一个包拥有包名和路径。包名在源文件中指定,调用者使用包名作为包内容的前缀。调用者使用路径引入一个包。按照惯例,包路径的最后一个元素就是包名:

import (
    "context"                // package context
    "fmt"                    // package fmt
    "golang.org/x/time/rate" // package rate
    "os/exec"                // package exec
)

构建工具将包映射到目录。go 工具使用GOPATH环境变量来找源文件,如 github.com/user/hello 的目录为 $GOPATH/src/github.com/user/hello

目录。标准库使用类似cryptocontainerencodingimage之类的目录来为对应的协议和算法组织包。这个目录里面的包并没有实际的关系。目录只是提供一个放置文件的方法。只要不出现依赖循环,任何包都可以引入其他的包。

正如不同的包的类型可以同名,不同目录的包也是可以同名。比如,runtime/pprof生成适合 pprof 分析工具格式的数据,而 net/http/pprof提供 HTTP 端的分析数据。调用者使用包目录来引入包,所以不会冲突。如果一个源文件需要引入这两个pprof,它可以在本地重命名其中一个或者两者。当重命名一个引入的包,重命名的包名也要符合上诉的标准(小写、不要 under_scoresmixedCaps)。

不良的包名

不好的包名会导致代码更难使用和维护。这里列举一些组织或命名不当的情况。

避免无意义的包名。命名为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))

把这些函数归并到一个新的包里面,并选择一个合适的命名:

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程序中良好命名的核心。花时间选择好的包名并组织代码,这会让使用者了解你的包,同时方便维护,使其更加优雅。

分类:

后端

标签:

Golang

作者介绍

小马别过河
V1