Go 100 常见错误 12: 项目组织混乱

20 阅读5分钟

组织一个Go项目并不是一项简单的任务。由于Go语言在设计包和模块时提供了很大的自由度,最佳实践并没有像应有的那样普及。本节首先讨论结构化项目的常见方法,然后探讨一些最佳实践,展示如何改进我们组织项目的方式。

12.1 项目结构

Go语言维护者并没有关于如何构建Go项目结构的严格规范。然而,经过多年的发展,一种项目结构逐渐流行起来:project-layout(github.com/golang-stan…) 如果我们的项目足够小(只有少数几个文件),或者我们的组织已经有了自己的标准,那么使用或迁移到project-layout可能并不值得。否则,或许值得考虑一下。让我们来看一下这种布局,并了解其主要目录的作用:

  • /cmd:主代码文件。一个名为foo的应用程序的main.go文件应放在/cmd/foo/main.go中。
  • /internal:私有代码,我们不希望其他人将其导入到他们的应用程序或库中。
  • /pkg:我们希望公开给他人使用的代码。
  • /test:额外的外部测试和测试数据。Go语言的单元测试文件与源代码文件位于同一个包中。然而,公共API测试或集成测试等应放在/test目录中。
  • /configs:配置文件。
  • /docs:设计和用户文档。
  • /examples:应用程序和/或公共库的示例。
  • /api:API契约文件(如Swagger、Protocol Buffers等)。
  • /web:特定于Web应用程序的资源(如静态文件等)。
  • /build:打包和持续集成(CI)文件。
  • /scripts:用于分析、安装等的脚本。
  • /vendor:应用程序的依赖项(例如,Go模块依赖项)。

与某些其他语言不同,这里没有/src目录。原因是/src过于通用,因此这种布局更倾向于使用/cmd/internal/pkg等目录。

2021年,Go语言核心维护者Russ Cox对该布局提出批评。主要争议点在于:虽然这个项目托管在GitHub的golang-standards组织下,但它并非官方标准。无论如何需要谨记,关于项目结构并不存在强制性约定。这种布局可能适合你也可能不适合,但关键点在于:优柔寡断是唯一的错误决定。因此,组织内部应达成布局共识以保持一致性,避免开发者在不同仓库之间切换时浪费时间。

现在,让我们探讨如何组织Go代码仓库的主要逻辑结构。

12.2 包的组织架构

在 Go 语言中,没有子包的概念。不过,我们可以自行决定在子目录中组织包。以标准库为例,net目录就是这样组织的:

/net
    /http
        client.go
       ...
    /smtp
        auth.go
       ...
    addrselect.go
   ...

net既是一个包,也是一个包含其他包的目录。但是net/http并不继承自net,也没有对net包的特定访问权限。net/http内部的元素只能看到net包中导出的元素。使用子目录的主要好处是将紧密相关的包放在一起,提高内聚性。

关于整体架构组织,存在不同的思路。例如,我们应该按上下文还是按层次来组织应用程序呢?这取决于个人偏好。我们可能倾向于按上下文对代码进行分组(比如客户上下文、合同上下文等),或者倾向于遵循六边形架构原则,按技术层次进行分组。只要我们所做的决策适合我们的用例,并且保持一致,就不能算是错误的决策。

关于包,我们应该遵循多个最佳实践。首先,要避免过早封装,因为这可能会导致项目过度复杂。有时候,采用简单的组织结构,等了解项目内容后再让其逐步演进,比一开始就强行构建完美结构要好。

粒度是另一个需要考虑的重要因素。我们应避免出现几十个只包含一两个文件的微型包。如果出现这种情况,很可能是因为我们忽略了这些包之间的一些逻辑联系,导致读者更难理解项目。相反,我们也应避免创建过于庞大的包,以免稀释包名的含义。

包的命名同样需要谨慎考量。作为开发者,我们都清楚命名并非易事。为了帮助用户理解 Go 项目,包的命名应当依据其提供的功能,而非所包含的内容。此外,命名要有实际意义。因此,包名应简洁明了、富有表现力,按照惯例,通常采用单个小写单词。

关于导出内容,规则相当简单直接。我们应尽量减少导出内容,以降低包与包之间的耦合度,并隐藏不必要的导出元素。如果不确定是否要导出某个元素,默认选择不导出。日后若发现需要导出,再对代码进行调整。同时要牢记一些特殊情况,比如为了能使用 encoding/json 对结构体进行反序列化,需将结构体的字段导出。

组织一个项目并非易事,但遵循这些规则有助于提升项目的可维护性。不过要记住,保持一致性对于维护工作的便利性也至关重要。所以,务必确保在代码库中尽可能保持统一。

在下一节,我们将探讨实用工具包。