Go 进阶训练营 – Go 工程化实践一:工程项目结构

2022-09-13 09:09:12 浏览数 (1)

工程项目结构

序言

go工程的项目结构多种多样,不同的框架对应不同的结构,即使是同一个框架,也有可能出现不同的结构。我个人感觉目前go很年轻,处于百花齐放的时代。而不像java,早已形成业界标准,主流框架spring全家桶,你想要的它都有。基本不用纠结项目结构,只需照着来就行。这对刚从java转到go的同学造成很大的困扰,不知道怎么写符合业界标注,找开源项目参考,github一搜,发现大家各不相同,难啊!

最终,我们团队内部形成共识,在B站毛剑老师推荐的项目布局的基础上进行修改,使其更适合我们当前的业务。最近正好要给团队分享B站微服务框架kratos的使用,借此机会增加go项目的共识。

Standard Go Project Layout

首先出场的是 github 高星项目:golang-standards/project-layout ,了解go项目里的目录含义,go sdk也是符合其定义的。

/cmd

/internal

  • 私有代码,不希望被依赖者引入。与之对应的是pkg目录。
  • 项目内应用之间公共部分放在/internal/pkg
  • 编译器支持,编译时会禁止此目录下的文件被外部依赖。并且不局限于顶级目录,在任何目录当中都是生效的。

/pkg

  • 显式的告诉外部应用程序可以使用的库代码。
  • pkg 目录下的包一般会按照功能进行区分,例如 /pkg/cache/pkg/conf
  • 可以放工具类,例如string的工具类,就放到/pkg/stringx/string.go,stringx作为包名是为了和sdk里的string包区分。注意:不应该将util、common作为一个包名,意义不明确,容易造成大杂烩。
  • 当根目录包含大量非 Go 组件和目录时,可以挪到pkg里(目前没遇到这类文件)。

Service Application Project Layout

服务应用布局在标准布局的基础上,增加下列目录。

/api

API 协议定义目录,xxapi.proto protobuf 文件,以及生成的 go 文件。我们使用了API大仓的方案,没有用这个目录。具体请移步Gitlab CI/CD 实践六:统一管理 protocol buffer,API 大仓设计与实现。

/configs

配置文件模板或默认配置。这里使用的复数,原因是参考其他开开源项目,但Uber-go不建议目录名使用复数。

/test

额外的测试程序和测试数据,这里使用子目录可以达到编译忽略的效果,也就是目录以"."或"_"开头。

不应该包含:/src

有些 Go 项目确实有一个 src 文件夹,但这通常发生在开发人员有 Java 背景,在那里它是一种常见的模式。一般而言,在 Go 项目当中不应该出现 src 目录,Go 和 Java 不同,在 Go 中每一个目录都是一个包,每一个包都是一等公民,我们不需要将项目代码放到 src 当中,不要用写其他语言的方式来写 Go。

kit project

  • 一个公司应该只有一个kit基础库。
  • kit 项目必须具备的特点:
代码语言:txt复制
- 统一
- 标准库方式布局
- 高度抽象
- 支持插件
- 持续维护尽量少的依赖于三方包,只保留起项目使用的gRPC之类的包。使用接口达到解耦的目的。例如A项目引入kit库,需要使用日志,而kit库里定义了日志接口,打日志的地方都是调用接口。此时需要引入另个一内部logrus基础库,该库实现了kit库定义的接口,底层调用真正的logrus三方包。这样带来的好处:1、避免kit库太臃肿;2、可自行实现日志接口,不使用内部logrus基础库,实现自定义功能,还可以更改依赖的logrus版本。
  • 类似于springboot starter和springboot data starter的关系,一个kit基础库,搭配多个内部实现库。但我们团队内部没有这样做,我们的基础库里放的是各种基础代码,例如创建日志,创建数据库连接等,基本是把业务无关的基础代码封装到kit库里。要说原因的话,我想大概是业务简单,我们经常使用到的基础库代码,大多数和三方包耦合的,可以说就是为了更方便的使用三方包。这和B站的kit库理念不同,他们高度抽象,支持插件,不依赖具体实现,封装的是复杂的,非业务的逻辑。
  • 持续维护这点是mohuishou大佬加的,结合他的经验,这点确实重要。 减少依赖和持续维护是我后面补充的,这一点其实很遗憾,我们部门刚进来的时候方向是对的也建立了一套基础库,然后大家都使用这同一套库,但是很遗憾,我们这一套库一是没人维护,二是没有一套机制来进行迭代,到现在很多团队和项目已经各搞各的了。 这样其实会导致做很多重复工作以及后续的一些改动很难推进,前车之鉴,如果有类似的情况一定要在小火苗出来的时候先摁住,从大的角度来讲统一有时候比好用重要,不好用应该参与贡献而不是另起炉灶。

服务类型

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

interface

对外的 BFF 服务,接受来自用户的请求,比如暴露了 HTTP/gRPC 接口。

我们的服务业务简单,不需要BFF来聚合,所以没有使用interface。

service

对内的微服务,仅接受来自内部其他服务或者网关的请求,比如暴露了gRPC 接口只对内服务。

admin

区别于 service,更多是面向运营测的服务,通常数据权限更高,隔离带来更好的代码级别安全。

job

流式任务处理的服务,上游一般依赖 message broker。

task

定时任务,类似 cronjob,部署到 task 托管平台中。

项目结构

团队内部应尽量保持项目结构统一,可以使用项目模板生成。

毛剑老师推荐的目录结构,这样做的编译产物名字是比较好的,一看就知道是什么程序。但是我们项目内没有这么干,为避免和项目名冗余,cmd下的目录去掉了myapp1,api定义也是这样的,项目组/项目名/版本/admin.proto。如果此项目只包含一种服务类型,那就去掉服务类型这级目录,包括internal也是。

项目命名

appid,b站采用的是三段式,业务.服务.子服务,例如:account.service.vip

数据库初始化

mohuishou大佬还使用了一个服务类型,myapp-migration: 数据库迁移任务,用于初始化数据库。这个正好是我最近遇到的多环境数据库初始化问题,目前是计划单独做一个项目,通过cicd触发,在各个环境执行数据库变更任务。想在想来,也可以做成一个基础库,项目启动时初始化数据库。但是听龙哥说grom的数据库迁移有问题,很多字段类型对不上,这块后面验证下,看能不能解决。这两种方案,我更倾向于前者,支持跨语言,我们公司内部就存在多种编程语言的服务。

B站项目布局演进

项目布局 v1

项目的依赖路径为: model -> dao -> service -> api,model struct 串联各个层。整体和java的结构类似,controller-》service-》dao。

  • 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 的字段,和前端显示的字段是有差异的,例如password。
  • server 层的代码可以通过基础库精简下,公共逻辑封装到基础库,提供统一服务暴露方式。

项目布局 v2

  • app 目录下有 api、cmd、configs、internal 目录,目录里一般还会放置 README、CHANGELOG、OWNERS。
  • internal:是为了避免有同业务下有人跨目录引用了内部的 biz、data、service 等内部 struct。
    • 如果存在一个仓库多个应用,那么可以在 internal 里面进行分层,例如 /internal/admin , /internal/job
    • biz: 业务逻辑的组装层,类似 DDD 的 domain 层,data 类似 DDD 的 repo,repo 接口在这里定义,使用依赖倒置的原则。
    • data: 业务数据访问,包含 cache、db 等封装,实现了 biz 的 repo 接口。我们可能会把 data 与 dao 混淆在一起,data 偏重业务的含义,它所要做的是将领域对象重新拿出来,去掉了 DDD 的 infra 层。
    • 实现接口需要编译校验。go里的接口与实现类是隐式的关联关系,没有依赖关系,由于关系不明确,可使用类型断言:_,interface=(impl)(nil),在编译期间确定实现关系。
    • data层相对于dao层偏业务,不是以表为单位,而是以业务对象为单位,也就是说data层可能包含业务对象组装。另外可将缓存逻辑也放在data层,对上层而言,仅仅是获取对象,不管data层是从db、缓存、http还是grpc里获取的。
    • service: 实现了 api 定义的服务层,类似 DDD 的 application 层,处理 DTO 到 biz 领域实体的转换(DTO -> DO),同时协同各类 biz 交互,但是不应处理复杂逻辑。
  • PO(Persistent Object): 持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么数据表中的每个字段(或若干个)就对应 PO 的一个(或若干个)属性。

示例可以参考 kratos v2 的 example

参考

Go工程化(二) 项目目录结构

Post Views: 5

0 人点赞