【golang】golang面向包的设计

郑星辰
2023-12-01

最近在做重构,并且是一个基础组件的重构,所以想写点关于如何写代码的东西。如何写代码是一个很大很大的话题,可以涉及到的内容很多,比如相对基础的有设计原则、设计模式、代码规范等,相对高阶一点指导代码架构的简洁架构、领域驱动等。

本篇的内容会聚焦于代码架构层的内容。同时因为作为基础组件,不包含复杂的业务,所以类似领域驱动的内容也不会涉及。所以本篇的内容可以定位为中等复杂程度的golang项目应该如何组织代码。

目录结构

golang项目的目录结构并没有统一的规定,但是社区中还是存在一些比较常见的约定,理解并使用约定的目录结构有助于别人理解我们的代码。常见的golang项目的都会包含/cmd、/internal、/pkg三个目录。

/cmd

/cmd目录包含项目的可执行文件的入口。通常建议的做法是每个服务在/cmd目录下创建一个单独的文件,并各自拥有main.go文件。目录结构如下。该项目中包含report服务和project服务,在/cmd下通过文件夹隔离,并各自拥有main.go文件。

├── cmd/
│   └── report/
│       └── main.go
│   └── project/
│       └── main.go

当然并不一定要采取这种方式。在我的项目中,项目在根目录下有统一的入口main.go,/cmd下通过cobra的命令行构建工具来管理不同服务的入口,目录结构如下。虽然和推荐的方式略有不同,但是代码的组织思路上是一致的,同样足够清晰。

├── cmd/
│   └── report/
│       └── cmd.go
│   └── project/
│       └── cmd.go
│......
│......
├── main.go

另外,作为程序的入口,/cmd中不应该存在太多的代码,其应该简洁清晰、一目了然。

/internal

/internal保存了golang项目中的私有代码。如果你的项目是一个框架或者sdk,那么/internal中应该存放你不希望被别人引用的部分;如果你的项目是一个业务服务,那么你的绝大部分甚至全部的代码都应该在/internal中。下面是一个包含了report和project服务的项目的/internal文件夹的目录。

├── cmd
├── internal/
│   └── app/
│       └── report
│       └── project
│   └── pkg/
│       └── log
│       └── trace
│       └── db
│......
│......

在/internal下/app中是具体的业务代码,不同服务之间通过文件夹隔离,这里通常就是具体的业务逻辑。/internal/pkg下是一些不同服务之间都可以引用的代码,例如log、db等。

/internal内部的目录结构的划分完全是依赖我们对具体的业务的理解,例如report和project的划分是按照功能职责进行划分,各自持有独立的不可共享的内容。/internal/pkg中放置的是report和project的共享的代码。从划分上看,/internal/app属于上层模块,/internal/pkg属于下层模块。通常来说,上层模块可以引用下层模块,下层模块不可以引用上层模块。然后report和project中的业务代码同样可以按照分层的思想进行划分。

按层划分的细节可参见Clean Coder Blog,简洁架构的核心就是软件分层,通常上层依赖下层,下层不能依赖上层,建立单向的依赖关系,并通过接口等方式把强依赖变为弱依赖。

/pkg

/pkg下的目录是别人可以直接引用。就像在/internal中提到那样,“如果你的项目是一个业务服务,那么你的绝大部分甚至全部的代码都应该在/internal中”。同样的,如果你的项目是一个业务服务,那么你的项目中绝大部分情况下不需要/pkg目录。

如果你的项目中是开源的框架或者SDK,那么/pkg可能是需要的。另外也有一种方式是不使用/pkg,直接讲可以外部引用的文件平铺在根目录下,这种方式在一些开源的项目中也很常见。

关于目录的结构的更详细的内容可以参见project-layout

分层的架构

在/internal中我们提到了简洁架构的分层的思想。在golang中,在模块划分上,其实更倾向于按照功能指责进行划分。但是在整体的架构上,有序的分层能帮我们建立清晰有序的依赖关系,提高代码的可维护性以及可读性。

具体到我们上面提到的目录结构中。层级划分从上到下依次是/cmd、/internal、/pkg,上层可以依赖下层,下层不能依赖上层。具体到/internal内部的划分也是这样的。

另外我在说上下层的依赖关系时都加了通常,因为有的情况下真的有可能下层依赖上层。这种情况下Clean Coder Blog也有提到过,首先通过接口来减轻上下层直接的依赖,上层模块和下层模块都依赖接口,而不是依赖实现;然后通过依赖注入的方式来将具体的实现注入,从而实现解耦。这也是常有的设计原则。

以上。

 类似资料: