本系列为 Go 进阶训练营 笔记,预计 2021Q1 完成更新,访问 博客: Go 进阶训练营 即可查看当前更新进度,部分文章篇幅较长,使用 PC 大屏浏览体验更佳
工程化这一节说简单看似简单,无非就是目录结构,代码分层,依赖注入等等。但是其中很多坑如果没踩过是不知道这里面的痛点的。除此之外这里面也会有很多架构的思想在里面,这也就是为什么我会把架构整洁之道的阅读笔记放在第一小节的原因。
接来下包含这一篇文章在内,我会先用几篇文章结合参考材料以及个人的理解整理一下毛老师课上讲的内容。然后恰好在这个课程前,我也在对我们之前的一些项目做重构,所以会再用一到两篇文章大概说一些我最后选择的方式,已经在实践过程中的一些取舍,就工程化这个事情来说大概原理上基本都是相通的,但是每个团队甚至每个人所面临的一些问题都各不相同,所以最后出来的东西肯定不是完全一致的。
注意,你如果是只是需要写一个脚本,或者是做一些简单的 demo 大可不必像文章接下来介绍的这样搞的这么麻烦,直接一个 main.go 简单快捷方便即可,但是如果你这是一个长期维护的项目,甚至涉及到的多个人之间的合作,那么接下来的几篇文章就不能错过了,可以仔细阅读,希望可以对你有所帮助。
这一部分的内容主要来自于 github 的高星项目:golang-standards/project-layout 通过这个我们可以大概的了解到在 Go 中一些约定俗成的目录含义,虽然这些不是强制性的,但是如果有去看官方的源码或者是一些知名的项目可以发现大多都是这么命名的,所以我们最好和社区保持一致,大家保持同样的语言。
我们一般采用 /cmd/[appname]/main.go
的形式进行组织
internal 目录下的包,不允许被其他项目中进行导入,这是在 Go 1.4 当中引入的 feature,会在编译时执行
/internal/app
/internal/pkg
当中举个 🌰 下面的是我们当前的目录结构,其中的代码很简单,在 t.go
当中导出了一个变量 I
然后在 a/cmd/a/main.go
和 b/cmd/b/main.go
当中分别导入输出这个变量的值
❯ tree
.
├── a
│ ├── cmd
│ │ └── a
│ │ └── main.go
│ └── internal
│ └── pkg
│ └── t
│ └── t.go
└── b
└── cmd
└── b
└── main.go
我们可以发现, a
目录下可以直接输出 I
的值
❯ go run ./a/cmd/a/main.go
1
但是在 b
目录下,编译器会直接报错说导入了 a
的私有包
❯ go run ./b/cmd/b/main.go
package command-line-arguments
b/cmd/b/main.go:3:8: use of internal package github.com/mohuishou/go-training/Week04/blog/02_project_layout/01_internal_example/a/internal/pkg/t not allowed
一般而言,我们在 pkg 目录下放置可以被外部程序安全导入的包,对于不应该被外部程序依赖的包我们应该放置到 internal
目录下, internal
目录会有编译器进行强制验证
/pkg/cache
、 /pkg/conf
等pkg
目录,直接把上面的这些包放在最上层即可.gitlab-ci.yml
makefile
.gitignore
等等,这种时候顶层目录会很多并且会有点杂乱,建议还是放到 /pkg
目录比较好kit 库其实也就是一些基础库
减少依赖和持续维护是我后面补充的,这一点其实很遗憾,我们部门刚进来的时候方向是对的也建立了一套基础库,然后大家都使用这同一套库,但是很遗憾,我们这一套库一是没人维护,二是没有一套机制来进行迭代,到现在很多团队和项目已经各搞各的了。这样其实会导致做很多重复工作以及后续的一些改动很难推进,前车之鉴,如果有类似的情况一定要在小火苗出来的时候先摁住,从大的角度来讲统一有时候比好用重要,不好用应该参与贡献而不是另起炉灶。
在这一小节我们会先看到毛老师在课上讲解的他们的应用程序目录的迭代变化,然后说一些我最后的采用的目录结构以及里面的取舍,关于具体怎么演进来的当中遇到了什么问题,我们会在 Go 工程化这个系列的最后一篇文章详细说明。
API 定义的目录,如果我们采用的是 grpc 那这里面一般放的就是 proto 文件,除此之外也有可能是 openapi/swagger 定义文件,以及他们生成的文件。
下面给出一个我现在使用的 api 目录的定义,其实和毛老师课上讲的类似,后面还有一篇文章会专门讲 api 的设计会讲到这里就不详细讲了
.
└── api
└── product_name // 产品名称
└── app_name // 应用名称
└── v1 // 版本号
└── v1.proto
为什么加个(s) 是课上讲的还有参考材料中很多都叫 configs 但是我们习惯使用 config 但是含义上都是一样的 这里面一般放置配置文件文件和默认模板
额外的外部测试应用程序和测试数据。一般会放测试一些辅助方法和测试数据
微服务中的 app 服务类型分为 4 类:interface、service、job、admin。
这上面是毛老师课上讲解的类型,和我们常用的做法类似,但是有点区别,同样假设我们有一个应用叫
myapp
大多大同小异,主要是 BFF 层我们一般是一个独立的应用,不会放在同一个仓库里面,
项目的依赖路径为: model -> dao -> service -> api,model struct 串联各个层,直到 api 需要做 DTO 对象转换。
/internal/app
, /internal/job
示例可以参考 kratos v2 的 example
.
├── api
├── cmd
│ └── app
├── config
├── internal
│ ├── domain
│ ├── repo
│ ├── service
│ └── usecase
└── pkg
「internal:」 是为了避免有同业务下有人跨目录引用了内部的对象
我们这里的定义和上面 v2 最大的区别是多了一个 domain 层,这里面有一个原因是我们对于单元测试的要求比较高,如果按照上面 v2 的代码进行组织,service 层直接依赖 usecase 的实现,service 的代码不太好进行单元测试。如果依赖 interface 会导致循环依赖,所以采用类似 go-clean-arch 的组织,单独抽象一层 domain 层
一般而言,在 Go 项目当中不应该出现 src 目录,Go 和 Java 不同,在 Go 中每一个目录都是一个包,每一个包都是一等公民,我们不需要将项目代码放到 src 当中,不要用写其他语言的方式来写 Go
不要在项目中出现 utils 和 common 这种包,如果出现这种包,因为我们并不能从包中知道你这个包的作用,长久之后这个包就会变成一个大杂烩,所有东西都往这里面扔。有的同学这个时候会问说,那我们的工具函数应该放到哪里?怎么放?举个例子,我们当前使用 gin
作为路由框架,但是 gin
的 handler 注册其实不是很方便,所以我们做了一层封装,这个时候这个工具方法我们一般放在 /pkg/ginx
目录下,表示这个是对 gin
增强的包,不直接使用 gin
作为包名的原因是因为我们在项目中也会引用 gin
相同的命名一个是会导致误解,另一个是在同时导入的时候也会需要去进行重命名会比较麻烦
关于项目目录结构这种真的算是见仁见智,不同的理论有不同的方法,但是我觉得有两件事比较重要,就服务应用而言需要灵活应用,就基础库而言一定要统一,做的好不好和要不要做是两件事情,如果因为当前做的不够好而不做,那么越到后面就越做不了。下一篇文章会讲一讲依赖注入框架 wire 的使用与最佳(?)实践
点击“阅读原文”查看参考文献等信息