package 管理包

在 Go 中使用包来组织代码,将一个系统划分为多个小模块,方便后续的维护、测试。之前在项目中使用过按功能划分包,当功能简单或者在最开始的设计阶段因为已经充分考虑了各个包的功能,所有划分还是很明确的。但是当功能复杂或者后续不断的迭代过程中,新加功能的划分就会出现模糊。最近看到一篇文章,里面介绍了一种使用 Go 标准库的划分包方案,摘抄在下面。

按依赖进行包划分

这是 Go 标准库最常用的方案,比如 io.Readerio.Writerio.Closer 接口,字符串读取(bytes.Readerstrings.Reader),文件读取(os.File),网络读取(net.Conn)等都实现了 io.Reader 接口,我们在使用读取相关功能的时候,只需要导入 io 接口包和对应的 Reader 实现包(如 os.File )即可。这种模型的主要思维按照依赖进行划分:

  • root 包: 声明原型和接口,不包含实现。root 包本身不依赖任何包。比如这里的 io 包。
  • implement 包: 对 root 包中的接口使用和实现。比如这里的 os、net、strings 等。
  • main 包: 导入 root 包和 implement 包,以 root 包接口为原型,实现对 implement 的桥接和依赖注入。

这种布局有几个好处:

  1. 按照依赖划分,更容易适应重构和需求变更
  2. 将依赖独立出去,代码变得很容易测试,比如很容易实现一个模拟 DB 操作的 dep 包,而业务逻辑无需任何变更
  3. 以接口为契约的包划分,要比直接包划分有更清晰的交互边界,前面提到的两种包划分(按功能模块纵向划分和 MVC 横向划分),做得不好很容易最终只是将代码分了几个目录存放,实际交互仍然混乱(比如直接修改其它包数据)

这种布局其实有点像前面提到的以接口包的形式将 A->B->A 的关系变为 IA->B->A,后者针对局部关系,而依赖划分强调从整体上思考这个功能模块的原型,然后围绕这些原型(接口)去扩展实现,最后在 main 包中将这些实现组装起来。关于这种包布局在这篇文章有很好的阐述。

标准库中的示例

针对上面提到的 iofile 包的划分,摘取标准库中的组织结构展示,更加直观。

目录结构

1
2
3
root─┬─{io}
└─{os}

io 包中定义了接口,os.File 实现了接口;os 包并没有将代码放入 io 包内部。