对于从其他语言过渡到 Go 语言的程序员来说,我倾向于在代码审查中提到的一件事是他们会过度使用包。
Go 语言没有提供有关可见性的详细方法; Java有 public
、protected
、private
以及隐式 default
的访问修饰符。 没有 C++
的 friend
类概念。
在 Go 语言中,我们只有两个访问修饰符,public
和 private
,由标识符的第一个字母的大小写表示。 如果标识符是公共的,则其名称以大写字母开头,该标识符可用于任何其他 Go 语言包的引用。
注意:
你可能会听到人们说exported
与not exported
, 跟public
和private
是同义词。
鉴于包的符号的访问有限控件,Go 程序员应遵循哪些实践来避免创建过于复杂的包层次结构?
贴士:
除cmd/
和internal/
之外的每个包都应包含一些源代码。
我的建议是选择更少,更大的包。 你应该做的是不创建新的程序包。 这将导致太多类型被公开,为你的包创建一个宽而浅的API。
以下部分将更为详细地探讨这一建议。
贴士:
来自Java
?
如果您来自Java
或C#
,请考虑这一经验法则 –Java
包相当于单个.go
源文件。 - Go 语言包相当于整个Maven
模块或.NET
程序集。
通过 import
语句将代码排列到文件中
如果你按照包提供的内容来安排你的程序包,是否需要对 Go 包中的文件也执行相同的操作?什么时候应该将 .go
文件拆分成多个文件?什么时候应该考虑整合 .go
文件?
以下是我的经验法则:
- 开始时使用一个
.go
文件。为该文件指定与文件夹名称相同的名称。例如:package http
应放在名为http
的目录中名为http.go
的文件中。 - 随着包的增长,您可能决定将各种职责任务拆分为不同的文件。例如:
messages.go
包含Request
和Response
类型,client.go
包含Client
类型,server.go
包含Server
类型。 - 如果你的文件中
import
的声明类似,请考虑将它们组合起来。或者确定import
集之间的差异并移动它们。 - 不同的文件应该负责包的不同区域。
messages.go
可能负责网络的HTTP
请求和响应,http.go
可能包含底层网络处理逻辑,client.go
和server.go
实现HTTP
业务逻辑请求的实现或路由等等。
贴士: 首选名词为源文件命名。
注意:
Go编译器并行编译每个包。 在一个包中,编译器并行编译每个函数(方法只是 Go 语言中函数的另一种写法)。 更改包中代码的布局不会影响编译时间。
优先内部测试再到外部测试
go tool
支持在两个地方编写 testing
包测试。假设你的包名为 http2
,您可以编写 http2_test.go
文件并使用包 http2
声明。这样做会编译 http2_test.go
中的代码,就像它是 http2
包的一部分一样。这就是内部测试。
go tool
还支持一个特殊的包声明,以 test
为结尾,即 package http_test
。这允许你的测试文件与代码一起存放在同一个包中,但是当编译时这些测试不是包的代码的一部分,它们存在于自己的包中。就像调用另一个包的代码一样来编写测试。这被称为外部测试。
我建议在编写单元测试时使用内部测试。这样你就可以直接测试每个函数或方法,避免外部测试干扰。
但是,你应该将 Example
测试函数放在外部测试文件中。这确保了在 godoc
中查看时,示例具有适当的包名前缀并且可以轻松地进行复制粘贴。
贴士:
避免复杂的包层次结构,抵制应用分类法
Go 语言包的层次结构对于go tool
没有任何意义除了下一节要说的。 例如,net/http
包不是一个子包或者net
包的子包。如果在项目中创建了不包含
.go
文件的中间目录,则可能无法遵循此建议。
使用 internal
包来减少公共API
如果项目包含多个包,可能有一些公共的函数,这些函数旨在供项目中的其他包使用,但不打算成为项目的公共API的一部分。 如果你发现是这种情况,那么 go tool
会识别一个特殊的文件夹名称 - 而非包名称 - internal/
可用于放置对项目公开的代码,但对其他项目是私有的。
要创建此类包,请将其放在名为 internal/
的目录中,或者放在名为 internal/
的目录的子目录中。 当 go
命令在其路径中看到导入包含 internal
的包时,它会验证执行导入的包是否位于 internal
目录。
例如,.../a/b/c/internal/d/e/f
的包只能通过以 .../a/b/c/
为根目录的代码被导入。 它无法通过 .../a/b/g
或任何其他仓库中的代码导入。[5]