Go 工程化标准实践

Go 工程化标准实践,第1张

Go 工程化标准实践

本文参考 Go 微服务框架 go-kratos/kratos的项目结构及相关最佳实践

标准项目结构 /cmd
|-- cmd
    |-- demo
        |-- demo
        +-- main.go
    +-- demo1
        |-- demo1
        +-- main.go

项目的主干,每个应用程序目录名与可执行文件的名称匹配。该目录不应放置太多代码。

/internal
|-- internal
    +-- demo
        |-- biz
        |-- service
        +-- data

私有应用程序和库代码。该目录由 Go 编译器强制执行(更多细节请参阅 Go 1.4 release notes),在项目树的任何级别上都可以有多个 /internal 目录。

可在 /internal 包中添加额外结构,以分隔共享和非共享的内部代码。对于较小的项目而言不是必需,但最好有可视化线索显示预期的包的用途。

实际应用程序代码可放在 /internal/app 目录下(比如 /internal/app/myapp),应用程序共享代码可放在 /internal/pkg 目录下(比如 /internal/pkg/myprivlib)。

相关服务(比如账号服务内部有 rpc、job、admin 等)整合一起后需要区分 app。单一服务则可以去掉 /internal/myapp。

/pkg
|-- pkg
    |-- memcache
    +-- redis
|-- conf
    |-- dsn
    |-- env
    |-- flagvar
    +-- paladin
.
|-- docs
|-- example
|-- misc
|-- pkg
|-- third_party
|-- tool

外部应用程序可以使用的库代码。可以显式地表示该目录代码对于其他人而言是安全可用的。

/pkg 目录内可参考 Go 标准库的组织方式,按照功能分类。/internal/pkg 一般用于项目内的跨应用公共共享代码,但其作用域仅在单个项目工程内。

💡 pkg 和 internal 目录的相关描述可以参考 [I’ll take pkg over internal](https://travisjeffery.com/b/2019/11/i-ll-take-pkg-over-internal/)

当根目录包含大量非 Go 组件和目录时,这也是一种将 Go 代码分组到一个位置的方法,使得运行各种 Go 工具更容易组织。

工具包项目结构
|-- cache
    |-- memcache
    |   +-- test
    +-- redis
        +-- test
|-- conf
    |-- dsn
    |-- env
    |-- flagvar
    +-- paladin
        +-- apollo
            +-- internal
                +-- mockserver
|-- container
    |-- group
    |-- pool
    +-- queue
        +-- apm
|-- database
    |-- hbase
    |-- sql
    +-- tidb
|-- ecode
    +-- types
|-- log
    +-- internal
        |-- core
        +-- filewriter

应当为不同的微服务建立统一的 kit 工具包项目(基础库/框架)和 app 项目。

基础库 kit 为独立项目,公司级建议只有一个。由于按照功能目录来拆分会带来不少的管理工作,建议合并整合。

其具备以下特点:

统一标准库方式布局高度抽象支持插件 服务应用项目结构
.
|-- README.md
|-- api
|-- cmd
|-- configs
|-- go.mod
|-- go.sum
|-- internal
+-- test
/api

API 协议定义目录,比如 protobuf 文件和生成的 go 文件。

通常把 API 文档直接在 proto 文件中描述。

/configs

配置文件模板或默认配置。

/test

外部测试应用程序和测试数据。可随时根据需求构造 /test 目录。

对于较大的项目数据子目录是很有意义的。比如可使用 /test/data 或 /test/testdata(如果需要忽略目录中的内容)。

Go 会忽略以“.”或“_”开头的目录或文件,因此在命名测试数据目录方面有更大灵活性。

GitLab Project
|-- app
    |-- replay
    |--..
    +-- member
|-- pkg
    |-- database
    |-- ..
    +-- log
+-- ...

一个 GitLab project 中可以放置多个微服务 app(类似 monorepo),也可以按照 GitLab 的 group 里建立多个 project,每个 project 对应一个 app。

微服务结构
|-- cmd                     负责程序的:启动、关闭、配置初始化等。
    |-- myapp1-admin        面向运营侧的服务,通常数据权限更高,隔离实现更好的代码级别安全。
    |-- myapp1-interface    对外的 BFF 服务,接受来自用户的请求(HTTP、gRPC)。
    |-- myapp1-job          流式任务服务,上游一般依赖 message broker。
    |-- myapp1-service      对内的微服务,仅接受来自内部其他服务或网关的请求(gRPC)。
    +-- myapp1-task         定时任务服务,类似 cronjob,部署到 task 托管平台中。

以下这种目录结构风格:

|-- service
    |-- api             API 定义(protobuf 等)以及对应生成的 client 代码,基于 pb 生成的 swagger.json。
    |-- cmd
    |-- configs         服务配置文件,比如 database.yaml、redis.yaml、application.yaml。
    |-- internal        避免有同业务下被跨目录引用了内部的 model、dao 等内部 struct。
        |-- model       对应“存储层”的结构体,是对存储的一一映射。
        |-- dao         数据读写层,统一处理数据库和缓存(cache miss 等问题)。
        |-- service     组合各种数据访问来构建业务逻辑,包括 api 中生成的接口实现。
        |-- server      依赖 proto 定义的服务作为入参,提供快捷的启动服务全局方法。
|-- ...

app 目录下有 api、cmd、configs、internal 目录。一般还会放置 README、CHANGELOG、OWNERS。

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

另一种结构风格是将 DDD 设计思想和工程结构做了简化,映射到 api、service、biz、data 各层。

.
|-- CHANGELOG
|-- OWNERS
|-- README
|-- api
|-- cmd
    |-- myapp1-admin
    |-- myapp1-interface
    |-- myapp1-job
    |-- myapp1-service
    +-- myapp1-task
|-- configs
|-- go.mod
|-- internal        避免有同业务下被跨目录引用了内部的 model、dao 等内部 struct。
    |-- biz         业务逻辑组装层,类似 DDD domain(repo 接口再次定义,依赖倒置)。
    |-- data        业务数据访问,包含 cache、db 等封装,实现 biz 的 repo 接口。
    |-- pkg
    +-- service     实现了 api 定义的服务层,类似 DDD application
    处理 DTO 到 biz 领域实体的转换(DTO->DO),同时协同各类 biz 交互,不处理复杂逻辑。

架构与数据模型

松散分层架构(Relaxed Layered System):层间关系不太严格,每层都可能使用它下面所有层的服务(而不仅是下一层)。每层都可能是半透明的,意味着有些服务只对上一层可见,而有些服务对上面的所有层都可见。

[       api         ]
    |     |       |
    | [ service ] |
    |    |        |
  [     biz     ] |
         |        |
      [    data     ]

继承分层架构(Layering Through Inheritance):高层继承并实现低层接口。需要调整各层顺序,将基础设施层移动到最高层。这依然是单向依赖,意味着领域层、应用层、表现层将不能依赖基础设施层,而基础设施层可以依赖它们。

[       data        ]
    |    |        |
    | [ api ]     |
    |    |        |
  [   service   ] |
         |        |
      [     biz     ]

数据模型:

失血模型:仅包含数据定义和 getter/setter 方法,业务逻辑和应用逻辑都放到服务层中。在 Java 中称为 POJO,在 .NET 中称为 POCO。贫血模型:包含一些业务逻辑,但不包含依赖持久层的业务逻辑(会放在服务层中),领域对象不依赖于持久层。充血模型:包含所有业务逻辑,领域层依赖于持久层,简单表示就是:UI 层 -> 服务层 -> 领域层 <-> 持久层。胀血模型:和业务逻辑不想关的其他应用逻辑(如授权、事务等)放到领域模型中(反而是另外一种失血模型,服务层缺失、由领域层代劳) 生命周期

考虑服务应用对象初始化和生命周期管理,所有 HTTP/gRPC 依赖的前置资源初始化(包括 data、biz、service),之后再启动监听服务。

资源初始化和关闭步骤繁琐,比较容易出错。可利用依赖注入的思路,使用 google/wire 管理资源依赖注入,方便测试和实现单次初始化与复用。

svr := http.NewServer()
app := kratos.New()
app.Append(kratos.Hook{
    OnStart: func(ctx context.Context) error {
        return svr.Start()
    },
    OnStop: func(ctx context.Context) error {
        return svr.Shutdown(ctx)
    },
})
if err := app.Run(); err != nil {
    log.Printf("app failed: %v\n", err)
    return
}

另外还支持静态生成代码,便于诊断(而不是在运行时通过 reflection 实现)。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/996236.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存