本系列为Go 进阶训练营 笔记,预计 2021Q1 完成更新,访问博客: Go 进阶训练营 即可查看当前更新进度,部分文章篇幅较长,使用 PC 大屏浏览体验更佳

工程化这一节说简单看似简单,无非就是目录结构,代码分层,依赖注入等等。但是其中很多坑如果没踩过是不知道这里面的痛点的。除此之外这里面也会有很多架构的思想在里面,这也就是为什么我会把架构整洁之道的阅读笔记放在第一小节的原因。

接来下包含这一篇文章在内,我会先用几篇文章结合参考材料以及个人的理解整理一下毛老师课上讲的内容。然后恰好在这个课程前,我也在对我们之前的一些项目做重构,所以会再用一到两篇文章大概说一些我最后选择的方式,已经在实践过程中的一些取舍,就工程化这个事情来说大概原理上基本都是相通的,但是每个团队甚至每个人所面临的一些问题都各不相同,所以最后出来的东西肯定不是完全一致的。

注意,你如果是只是需要写一个脚本,或者是做一些简单的 demo 大可不必像文章接下来介绍的这样搞的这么麻烦,直接一个 main.go 简单快捷方便即可,但是如果你这是一个长期维护的项目,甚至涉及到的多个人之间的合作,那么接下来的几篇文章就不能错过了,可以仔细阅读,希望可以对你有所帮助。

Standard Go Project Layout

这一部分的内容主要来自于 github 的高星项目: golang-standards/project-layout 通过这个我们可以大概的了解到在 Go 中一些约定俗成的目录含义,虽然这些不是强制性的,但是如果有去看官方的源码或者是一些知名的项目可以发现大多都是这么命名的,所以我们最好和社区保持一致,大家保持同样的语言。

/cmd

/cmd/[appname]/main.go
  • 首先 cmd 目录下一般是项目的主干目录
  • 这个目录下的文件 不应该有太多的代码,不应该包含业务逻辑
  • main.go 当中主要做的事情就是负责程序的生命周期,服务所需资源的依赖注入等,其中依赖注入一般而言我们会使用一个依赖注入框架,这个主要看复杂程度,后续会有一篇文章单独介绍这个

/internal

internal 目录下的包,不允许被其他项目中进行导入,这是在 Go 1.4 当中引入的 feature,会在编译时执行

/internal/app
/internal/pkg
t.goIa/cmd/a/main.gob/cmd/b/main.go
aI
ba

/pkg

internalinternal
/pkg/cache/pkg/confpkg.gitlab-ci.ymlmakefile.gitignore/pkg

Kit Project Layout

kit 库其实也就是一些基础库

  • 每一个公司正常来说应该 有且仅有一个基础库项目
  • kit 库一般会包含一些常用的公共的方法,例如缓存,配置等等,比较典型的例子就是 go-kit
  • kit 库必须具有的特点:
    • 统一
    • 标准库方式布局
    • 高度抽象
    • 支持插件
    • 尽量减少依赖
    • 持续维护

减少依赖和持续维护是我后面补充的,这一点其实很遗憾,我们部门刚进来的时候方向是对的也建立了一套基础库,然后大家都使用这同一套库,但是很遗憾,我们这一套库一是没人维护,二是没有一套机制来进行迭代,到现在很多团队和项目已经各搞各的了。

Service Application Project Layout

在这一小节我们会先看到毛老师在课上讲解的他们的应用程序目录的迭代变化,然后说一些我最后的采用的目录结构以及里面的取舍,关于具体怎么演进来的当中遇到了什么问题,我们会在 Go 工程化这个系列的最后一篇文章详细说明。

/api

API 定义的目录,如果我们采用的是 grpc 那这里面一般放的就是 proto 文件,除此之外也有可能是 openapi/swagger 定义文件,以及他们生成的文件。

下面给出一个我现在使用的 api 目录的定义,其实和毛老师课上讲的类似,后面还有一篇文章会专门讲 api 的设计会讲到这里就不详细讲了

/config(s)

为什么加个(s) 是课上讲的还有参考材料中很多都叫 configs 但是我们习惯使用 config 但是含义上都是一样的

这里面一般放置配置文件文件和默认模板

/test

额外的外部测试应用程序和测试数据。一般会放测试一些辅助方法和测试数据

服务类型

微服务中的 app 服务类型分为 4 类:interface、service、job、admin。

  • interface: 对外的 BFF 服务,接受来自用户的请求,比如暴露了 HTTP/gRPC 接口。
  • service: 对内的微服务,仅接受来自内部其他服务或者网关的请求,比如暴露了 gRPC 接口只对内服务。
  • admin:区别于 service,更多是面向运营测的服务,通常数据权限更高,隔离带来更好的代码级别安全。
  • job: 流式任务处理的服务,上游一般依赖 message broker。
  • task: 定时任务,类似 cronjob,部署到 task 托管平台中。
myapp
  • myapp: 这个是主服务,由于我们大多都是 http 的服务,所以这个服务一般对外暴露 http 服务接口
  • myapp-cron: 这个是定时任务
  • myapp-job: 这个用于处理来自 message 的流式任务
  • myapp-migration: 数据库迁移任务,用于初始化数据库
  • scripts/xxx: 一次性执行的脚本,有时候会有一些脚本任务

大多大同小异,主要是 BFF 层我们一般是一个独立的应用,不会放在同一个仓库里面,

项目布局 v1

项目的依赖路径为: model -> dao -> service -> api,model struct 串联各个层,直到 api 需要做 DTO 对象转换。

  • model: 放对应“存储层”的结构体,是对存储的一一隐射。
  • dao: 数据读写层,数据库和缓存全部在这层统一处理,包括 cache miss 处理。
  • service: 组合各种数据访问来构建业务逻辑。
  • server: 依赖 proto 定义的服务作为入参,提供快捷的启动服务全局方法。
  • api: 定义了 API proto 文件,和生成的 stub 代码,它生成的 interface,其实现者在 service 中。
  • service 的方法签名因为实现了 API 的 接口定义,DTO 直接在业务逻辑层直接使用了,更有 dao 直接使用,最简化代码。
  • DO(Domain Object): 领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。缺乏 DTO -> DO 的对象转换。

v1 存在的问题

  • 没有 DTO 对象,model 中的对象贯穿全局,所有层都有
    • model 层的数据不是每个接口都需要的,这个时候会有一些问题
    • 在上一篇文章中其实也反复提到了 “如果两段看似重复的代码,如果有不同的变更速率和原因,那么这两段代码就不算是真正的重复”
  • server 层的代码可以通过基础库干掉,提供统一服务暴露方式

项目布局 v2

/internal/app/internal/job

我的项目布局

internal:是为了避免有同业务下有人跨目录引用了内部的对象

  • domain: 类似之前的 model 层,这里面包含了 DO 对象,usecase interface, repo interface 的定义
  • repo: 定于数据访问,包含 cache, db 的封装
  • usecase: 这里是业务逻辑的组装层,类似上面的 biz 层,但是区别是我们这里不包含 DO 对象和 repo 对象的定义
  • service: 实现 api 的服务层,主要实现 DTO 和 DO 对象的转化,参数的校验等等

    我们这里的定义和上面 v2 最大的区别是多了一个 domain 层,这里面有一个原因是我们对于单元测试的要求比较高,如果按照上面 v2 的代码进行组织,service 层直接依赖 usecase 的实现,service 的代码不太好进行单元测试。如果依赖 interface 会导致循环依赖,所以采用类似 go-clean-arch 的组织,单独抽象一层 domain 层

应该避免的坏习惯

/src

一般而言,在 Go 项目当中不应该出现 src 目录,Go 和 Java 不同,在 Go 中每一个目录都是一个包,每一个包都是一等公民,我们不需要将项目代码放到 src 当中,不要用写其他语言的方式来写 Go

utils,common

不要在项目中出现 utils 和 common 这种包,如果出现这种包,因为我们并不能从包中知道你这个包的作用,长久之后这个包就会变成一个大杂烩,所有东西都往这里面扔。

有的同学这个时候会问说,那我们的工具函数应该放到哪里?怎么放?

gingin/pkg/ginxgingingin

总结

关于项目目录结构这种真的算是见仁见智,不同的理论有不同的方法,但是我觉得有两件事比较重要,就服务应用而言需要灵活应用,就基础库而言一定要统一,做的好不好和要不要做是两件事情,如果因为当前做的不够好而不做,那么越到后面就越做不了。

下一篇文章会讲一讲依赖注入框架 wire 的使用与最佳(?)实践