今天,我将重点介绍基本的项目布局,目的是优化可重用性和可测试性。
在编写命令而非库时,命令有三个独特之处影响着我的代码结构:
main包:
这是 go 程序必须拥有的唯一软件包。不过,除了告诉 go 工具生成二进制文件之外,main 包还有一个独特之处--任何人都不能从它那里导入代码。这意味着你放在 package main 中的任何代码都不能被其他项目直接使用,这让 OSS 大神很伤心。因为我编写开源代码的主要原因之一就是为了让其他开发者可以使用它,这直接违背了我的愿望。
我曾多次想过 "我很想把 XX Go 二进制背后的逻辑作为我代码的一部分"。如果该逻辑在软件包 main 中,你就不能使用。
os.Exit
如果你希望生成的二进制文件能满足用户的期望,那么你就应该关心你的二进制文件以什么退出码退出。唯一的办法就是调用 os.Exit(或者调用一些调用 os.Exit 的东西,比如 log.Fatal)。
但是,您不能测试调用 os.Exit 的函数。为什么?因为在测试过程中调用 os.Exit 会退出测试可执行文件。如果你不小心调用了它,就很难弄明白这一点。在运行测试时,实际上并没有测试失败,测试只是比它们应该退出的时间提前了,而你却只能望码兴叹。
最简单的方法就是不要调用 os.Exit。无论如何,你的大部分代码都不应该调用 os.Exit......如果有人导入了你的库,却在某些情况下随机导致他们的应用程序终止,那他一定会非常生气。
因此,只在一个地方调用 os.Exit,尽可能靠近应用程序的 "外部",并尽量减少入口点。说到这里…
func main()
这是所有 go 命令都必须具备的一个函数。你会认为每个人的 func main 都不一样,毕竟每个人的应用程序都不一样,对吗?事实证明,如果你真的想让代码可测试、可重用,那么 "你的主函数里有什么?"的正确答案大概只有一个。
事实上,更进一步,我认为"那你认为的主包应该怎么写?"的正确答案大约只有一个,那就是这个:
// command main documentation here.
package main
import (
"os"
"github.com/you/proj/cli"
)
func main{
os.Exit(cli.Run())
}
就是这样。这大概是有用的软件包 main 中最精简的代码了,这样就不会浪费精力在别人无法重用的代码上了。我们将 os.Exit 独自隔离成一行函数,它是我们项目的核心,实际上无需测试。
工程结构
让我们来看看整个工程的布局:
/home/you/src/github.com/you/proj $ tree
.
├── cli
│ ├── parse.go
│ ├── parse_test.go
│ └── run.go
├── LICENSE
├── main.go
├── README.md
└── run
├── command.go
└── command_test.go
我们知道 main.go 中的内容......事实上,main.go 是主软件包中唯一的 go 文件。LICENSE 和 README.md 应该是不言自明的。(一定要使用许可证!否则很多人都无法使用你的代码)。
现在我们来看两个子目录:run 和 cli。
CLI
cli 软件包包含命令行解析逻辑。您可以在这里定义二进制程序的用户界面。它包含标志解析、参数解析、帮助文本等。
它还包含向 func main 返回退出代码的代码(退出代码会被发送到 os.Exit)。因此,您可以测试这些函数返回的退出代码,而不必测试整个二进制文件产生的退出代码。
Run
运行软件包包含二进制文件逻辑的主要部分。在编写此软件包时,应将其视为一个独立的库。它应该与 CLI、标志等无关。它应该接收结构化数据并返回错误。假设它可能会被其他库、网络服务或其他人的二进制文件调用。尽可能少假设它的使用方式,就像使用通用库一样。
现在,大型项目显然需要不止一个目录。事实上,你可能需要将你的逻辑拆分成一个单独的 repo。这取决于你认为人们重用你的逻辑的可能性有多大。如果你认为这种可能性很高,我建议将逻辑单独放在一个目录下。在我看来,将逻辑单独放在一个目录下,比随意放在某个 repo 中更能体现对质量和稳定性的承诺。
合并
cli 软件包是运行软件包逻辑的命令行前端。如果有其他人看到了你的二进制文件,并想将其背后的逻辑用于网络应用程序接口,他们只需导入运行软件包并直接使用该逻辑即可。同样,如果他们不喜欢你的 CLI 选项,也可以轻松编写自己的 CLI 解析器,并将其用作运行包的前端。
这就是我所说的可重复使用的代码。我永远不希望有人为了获得更多的使用价值而拆开我的代码。而做到这一点的最好方法就是将用户界面与逻辑分开。这是关键部分。不要让用户界面(CLI)概念渗透到逻辑中。这是保持逻辑通用性和用户界面可管理性的最佳方法。
更大的项目
这种布局适合中小型项目。在 repo 的根目录下只有一个二进制文件,因此比在多个子目录下更容易获取。而大型项目则完全不同。它们可能有多个二进制文件,在这种情况下,它们不可能都在版本库的根目录下。不过,这类项目通常也有自定义的构建步骤,需要的不仅仅是 go-get。