项目结构混乱
创建一个好的Go项目结构并不是一件容易的事情,由于Go语言在设计包和模块方面提供了很大的自由度,因此在这方面没有通用的最佳实践。本文将首先讨论创建项目的常用组织结构,然后讨论一些最佳实践,给出改进项目组织方式的方法。
项目结构
Go语言维护者对构建Go项目结构没有严格的约定,在github上有一个称为标准Go项目结构的模板(https://github.com/golang-standards/project-layout)),注意该模板不是Go官方提供的。如果我们的项目很小(只有几个文件),或者公司和项目组已经指定了项目结构规范,重新调整或迁移到上述模板格式可能不值得。如果项目还没有结构规范,那前面这个结构值得参考借鉴。现在我们来看看这个结构模板的布局,都有些什么内容:
- /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这种目录。
「NOTE:在2021年,Go语言的核心维护者 Russ Cox对上面的项目结构表达反对意见。尽管它号称是Go项目标准结构,但不是官方的标准,有误导人嫌疑。对于项目结构,没有强制性约定必须采用上述模板。我们必须意识到这一个点,唯一注意的是项目中的各个模块结构要保持一致,达成统一。避免在不同的结构之间发生迁移,这会浪费时间。」
包组织结构
在Go语言中,没有子包的概念。但是,我们可以在子目录中创建包。下面是标准库net中的目录结构。net既充当包,又充当包含其他包的目录。但是net/http包不继承net或对net包具有特定的访问权限。外界能看到net/http中可导出的元素。子目录的主要好处是将包中代码保存在具有高内聚性的地方。
代码语言:javascript复制/net
/http
client.go
...
/smtp
auth.go
...
addrselect.go
...
对于Go包的组织形式,有不同的观点。例如,我们应该按业务类型还是按层来组织应用程序,这取决于自己的喜好。我们可能倾向于按业务类型(例如客户业务,合同业务等)对代码进行分组,或者我们倾向于遵循六边形原则对其进行分组。只要选择出了适合我们的方法,保持统一即可。
对于软件包,我们应该遵循一些最佳实践。首先,应该避免过渡设计,因为这可能会使得项目过于复杂。当我们搞清楚了项目包含的内容后,最好使用一个简单的形式组织并让项目不断的发展,而不是强迫自己预先制定完美的结构。
包的粒度是另一个需要考虑的重要因素,我们应该避免有几十个包含一两个文件的小包。如果这样设计,可能错过了这些包之间的一些逻辑联系,使得项目更难让人理解。此外,我们也应该避免使用包含很多文件的大包。总之,对于包的粒度,我们不应该走极端,导致包极小或极大。
包的命名也应该谨慎考虑。众所周知,命名是程序开发中一件困难的事情。为了帮助用户理解Go项目,我们应该根据它提供的内容命名包,而不是它包含的内容。此外,包名要有意义。因此,包的名称应该简短、简洁和富有表现力,按照惯例,应该是一个小写单词。
对于包导出什么,规则非常简单。我们应该尽可能减少应该导出的内容,以减少包之间的耦合并隐藏不必要导出的元素。如果不确定是否要导出一个元素,应该默认它不导出,在后面发现需要导出时,再调整代码支持将其导出。我们还要注意一些特殊情况,例如,当我们对一个结构体对象调用 encoding/json 标准库对其进行序列化或反序列化时,该结构体对象的字段需要是可导出的(即首字母要大写),否则会忽略该字段。
组织好一个项目结构并不是一件简单的事情,遵循上述这些规则有助于我们更容易维护。记住一点,保持结构一致对于简化可维护非常有帮助。因此,应确保代码库中的代码尽可能保持一致。