Go 面向包的设计和架构分层

本篇内容主要讲解golang项目的面向包设计准则和基础的架构分层。

信息来自原文

内容进行翻译、加工、整合及结合个人的实践经验,并附有一个真实的例子来解释本篇内容。

当然你也可以直接阅读英文原文。

当然高手如云,只是懒得写罢了。

百年太久,只争朝夕,不负韶华,不枉少年,来日怎方长。

cmdinternalpkggogo
main.gointernalinternalvendor
vendor, go depgo modules

这样的项目架构分层只是一种通用的模式,他不会给go的面向包的设计强加什么东西。面向包设计的理念让开发者在一个 go 项目中确定包的组织和必须要遵守的设计准则。它定义了一个 go 项目应该是什么样的及怎么架构和分层一个 go 项目。它最终的目的是为了提高项目的可读性、代码整洁性和可交流性,便于团队成员沟通。一个很好的大家都理解的架构本身就是一种通用的沟通语言。

面向包设计不局限于项目本身的结构,更多为了表达一个实现合理面向包设计的项目结构是多么的重要。下面将介绍一个面向包设计的项目、之前提到过的相关的准则和基础的架构分层。

项目架构分层

每个公司都会有一个工具包的项目和不同业务的应用项目

工具包项目

考虑到工具包作为公司的一个标准类库,所以应该仅有一个。里面的所有包都需要设计为高可移植性。这些包可以在任何一个项目中都能使用,并且提供的都是很实用、具体的但又非常基础的功能。为了达到这样的目标,工具包项目不能有一个包依赖三方的 vendor。因为如果有包依赖三方包,那就得不断的构建编译随着那些三方包的更新。

同时也不建议把工具包项目的部分包直接复制到你的应用项目中,因为这样本身增加了你对这些包管理、更新的工作,当然你如果真这样做也没毛病。

应用项目

应用项目是包含了很多需要部署在一起的程序集,包括服务、命令行工具和后台运行的程序。每个项目都对应一个含有其所有源代码的仓库,包括所有依赖的三方包。你需要几个应用项目,视情况以你而定,当然是越少越好。

cmd, internal, pkg, vendorpkg

一个典型的应用项目结构应该是这样的:

paper-code/examples/groupevent
├── cmd/
│   └── eventtimer/
│       └── update/
│       └── main.go
│   └── eventserver/
│       └── router/
│           └── handler/
│           └── router.go
│       └── tests/
│       └── main.go
├── internal/
│   └── eventserver/
│       └── biz/
│           └── event/
│           └── member/
│       └── data/
│           └── service/
│   └── eventpopdserver/
│       └── event/
│       └── member/
│   └── pkg/
│       └── cfg/
│       └── db/
│       └── log/
└── vendor/
│   ├── github.com/
│   │   ├── ardanlabs/
│   │   ├── golang/
│   │   ├── prometheus/
│   └── golang.org/
├── go.mod
├── go.sum

cmd/

cmd/dmaininternal/pkg/

示例

├── cmd/
│   └── eventtimer/
│       └── update/
│       └── main.go
│   └── eventserver/
│       └── router/
│           └── handler/
│           └── router.go
│       └── tests/
│       └── main.go
cmdmain

internal/

在go语言中,变量,函数,方法等的存取权限只有exported(全局)和unexported(包可见,局部)2种。

internal
internalgo builduse of internal package ... not allowed
1 package main
2
3 import (
4	"paper-code/examples/groupevent/cmd/eventserver/router/handler"
5	"paper-code/examples/groupevent/cmd/internal"
6	"paper-code/examples/groupevent/internal/eventpopdserver/event"
7	"paper-code/examples/groupevent/pkg/middleware"
8 )
9
10 func main() {
11 	middleware.HandlerConv(nil)
12
13	event.EventsBy("")
14
15	eh := new(handler.EventHandler)
16	eh.Events(nil, nil)
17
18	internal.CmdInternalFunc()
19 }

此代码片段为另一个项目导入paper-code/example/groupevent的代码包

use of internal package paper-code/examples/groupevent/internal/eventpopdserver/event not allowed

第5行的导入也会提示同样的错误

第7行导入就可以的,因为导入的pkg代码包

internalinternal第5行的导入也会提示同样的错误:use of internal package paper-code/examples/groupevent/cmd/internal not allowed
internal.../a/b/c/internal/d/e/f.../a/b/c.../a/b/g
internalinternal/myappinternal/pkg/

internal/pkg/

在同一个项目中不同程序需要访问,但又不能让其他项目访问的代码包,需要放在这里。这些包是比较基础但又提供了很特殊的功能,比如数据库、日志、用户验证等功能。

pkg/

pkgpkgpkginternalinternalpkg

vendor/

vendor文件夹包含了所有依赖的三方的源代码,它是go项目最早的依赖包的管理方式。目前大都用的go mod的依赖包管理,相对vendor,能指定版本,并且你不用特意手动下载更新依赖包,通过正常的go build, go run命令会自动处理。这样会减少项目本身的容量大小。

go mod vendor-mod=vendor

这里不过多介绍go mod的用法和特性。

面向包的设计和验证

面向包设计的准则可以验证项目中包设计的是否合理,下面这些步骤可以帮你发现包设计的问题。

包的位置

kitcmdinternalinternal/pkgpkg

依赖包导入

├── internal/
│   └── eventserver/
│       └── biz/
│           └── event/
│           └── member/
│       └── data/
│           └── service/
│   └── eventpopdserver/
│       └── event/
│       └── member/

应用级别的策略

比如给restful api的handler写中间件、定时更新等策略。

Kitinternal/pkg/, pkg/
cmd/internal/

数据的发送和接收

httpRequesthttpserver

错误处理

错误处理包括错误信息的日志输出,分析和解决错误,并且保证程序能恢复如果发生了错误。

Kitpaniccmd/panicinternal/panicinternal/pkg/panicpkg/panic

测试

cmd/kit/internal/internal/pkg/,pkg/

捕获错误

cmd/kit/, internal/, internal/pkg/,pkg/

不建议的目录

src/model/xxs/

结论

在实际go项目开发中,一定要灵活运用,当然也可以完全不按照这样架构分层、包设计的规则,一切以项目的大小、业务的复杂度、个人专业技能认知的广度和深度、时间的紧迫度为准。

最后以软件大师 Kent Beck 在《重构Refactoring》一书中描述的结尾。

  • 先让代码工作起来-如果代码不能工作,就不能产生价值
  • 然后再试图将它变好-通过对代码进行重构,让我们自己和其他人更好地理解代码,并能按照需求不断地修改代码。
  • 最后再试着让它运行得更快-按照性能提升的需求来重构代码。

谢谢

最后推荐一个工具,几秒内快速创建一个符合本文论述的一个go项目 goslayer