原文链接:https://www.yipwinghong.com/2021/08/04/Go_engineering-standard
目录
项目结构
标准项目结构
工具包项目结构
服务应用项目结构
生命周期
API 设计
gRPC
目录结构
兼容性
命名规范
原始字段
异常处理
设计规则
配置管理
最佳实践
模块管理
GOPATH
GO Modules
Proxy
Private
GOPROXY 编译部署
测试
单元测试
最佳实践
参考
项目结构
|-- cmd
|-- demo
|-- demo
+-- main.go
+-- demo1
|-- demo1
+-- main.go
项目的主干,每个应用程序目录名与可执行文件的名称匹配。该目录不应放置太多代码。
|-- 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
|-- 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[1]。
当根目录包含大量非 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 协议定义目录,比如 protobuf 文件和生成的 go 文件。
通常把 API 文档直接在 proto 文件中描述。
配置文件模板或默认配置。
外部测试应用程序和测试数据。可随时根据需求构造 /test 目录。
对于较大的项目数据子目录是很有意义的。比如可使用 /test/data 或 /test/testdata(如果需要忽略目录中的内容)。
Go 会忽略以“.”或“_”开头的目录或文件,因此在命名测试数据目录方面有更大灵活性。
|-- 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 ]
数据模型:
考虑服务应用对象初始化和生命周期管理,所有 HTTP/gRPC 依赖的前置资源初始化(包括 data、biz、service),之后再启动监听服务。
资源初始化和关闭步骤繁琐,比较容易出错。可利用依赖注入的思路,使用 google/wire[2] 管理资源依赖注入,方便测试和实现单次初始化与复用。
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 实现)。
为了统一检索和规范 API,可在内部建立统一的仓库,整合所有对内对外 API(可参考 googleapis/googleapis[3]、envoyproxy/data-plane-api[4]、istio/api[5])。
gRPC[6] 是一种高性能的开源统一 RPC 框架:
syntax = "proto3";
package rpc_package;
service HelloWorldService {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
protoc --go_out=.--go_opt=paths=source_relative \
--go-grpc_out=.--go-grpc_opt=paths=source_relative \
helloworld/helloworld.proto
设计原则:
设计时不要过早关注性能问题,先实现标准化。
参考:
|-- bapis
|-- api
|-- echo
|-- v1
|-- echo.proto
|-- OWNERS 权限拥有者
|-- rpc
|-- status.proto 内部状态码
|-- metadata 框架元信息
|-- locale
|-- network
|-- device
|-- annotations 注解定义 options
|-- third_party 第三方引用
维护 API 需要注意总是保持向后兼容(非破坏性)的修改:
应避免破坏性的修改(一般需要修改 major 版本号):
包名为应用的标识(appid),用于生成 gRPC 请求路径或 proto 之间引用 Message。
文件中声明的包名称应该与产品和服务名称一致,带有版本的 API 的软件包名称必须以此版本结尾。
参考():
示例 | |
---|---|
产品名称 | Google Calendar API |
服务名称 | calendar.googleapis.com |
软件包名称 | google.calendar.v3 |
接口名称 | google.calendar.v3.CalendarService |
来源目录 | //google/calendar/v3 |
API 名称 | calendar |
请求 URL:/package_name.version.service_name/method
gRPC 默认使用 Protobuf v3 格式,去除了 required 和 optional 关键字(默认全部是 optional)。没有赋值的字段默认是基础类型字段的默认值,比如 0 或者 “”。
// proto2
message Account {
// 必须
required string name = 1;
// 可选,默认值改为 -1.0,有 haxXxx 方法。
optional double profit_rate = 2 [default=-1.0];
}
// proto3
message Account {
// 都是可选,默认值为 0 和 "",无 hasXxx 方法。
string name = 1
double profit_rate = 2;
}
将无法区分默认值或未赋值。因此在 Protobuf v3 中建议使用:wrappers.proto[7]。Wrapper 类型的字段即包装一个 message,使用时变为指针。
message DoubleValue {
double value = 1;
}
Protobuf 作为强 schema 约束的描述文件,也方便扩展,因此也可以用于配置文件定义。
首先由于会为服务监控带来麻烦,明确禁止在 HTTP Status Code 中统一设置为 200、在 Body 中再定义 code 字段标记具体错误类型的做法。
使用标准错误配合具体错误:比如服务端使用一个标准 google.rpc.Code.NOT_FOUND
错误代码告知客户端无法找到特定资源(大类:404,小类:具体资源)。
/google/rpc/error_details
)。错误传播:如果 API 服务依赖于其他服务,不应盲目地将服务错误传播到客户端。在翻译错误时建议:
全局错误码 是松散、契约易被破坏的,应在每个服务传播错误时做一次翻译,保证每个服务 + 错误枚举是唯一的,定义在 proto 中(可作为文档)。
有时接口复用会带来歧义,比如一些字段给 A 方法用、另一些给 B 方法用;如果为不同方法定义 struct 又会造成冗余。
service LibraryService {
rpc UpdateBook(UpdateBookRequest) returns (Book);
}
message UpdateBookRequest { Book book = 1;}
message Book {
string name = 1;
string author = 2;
string title = 3;
bool read = 4;
}
gRPC 推荐的做法是利用 FieldMask 的部分更新:客户端可执行需要更新的字段信息,空 FieldMask 默认应用到所有字段。
service LibraryService {
rpc UpdateBook(UpdateBookRequest) returns (Book);
}
message UpdateBookRequest {
Book book = 1;
google.protobuf.FieldMask mask = 2;
}
通常包括以下内容:
配置传参先参考 net/http 库:
func main() {
s := &http.Server{
Addr: ":8080",
Handler: nil,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())
}
缺点是无法获知修改公共字段是否会有副作用,字段的含义也要自行查阅文档。
改进是自行设计 config struct,建议使用 functional options:
type Server struct {
Addr string // required
Port int // required
Protocol string // not null, default TCP
Timeout time.Duration // not null, default 30
MaxConn int // not null, default 1024
TLS *tls.Config //
}
type Option func(*Server)
func Protocol(p string) Option {
return func(s *Server) {
s.Protocol = p
}
}
func Timeout(timeout time.Duration) Option {
return func(s *Server) {
s.Timeout = timeout
}
}
func MaxConn(maxConn int) Option {
return func(s *Server) {
s.MaxConn = maxConn
}
}
func TLS(tls *tls.Config) Option {
return func(s *Server) {
s.TLS = tls
}
}
func NewServerFP(addr string, port int, options ...Option) (*Server, error) {
// 有一个可变参数 options 可以传出多个上面的函数,for-loop 设置 Server 对象。
srv := Server{
Addr: addr,
Port: port,
Protocol: "tcp",
Timeout: 30 * time.Second,
MaxConn: 1000,
TLS: nil,
}
for _, option := range options {
option(&srv)
}
//...
return &srv, nil
}
func TestFunctionalOptions(t *testing.T) {
s1, _ := NewServerFP("localhost", 1024)
s2, _ := NewServerFP("localhost", 2048, Protocol("udp"))
s3, _ := NewServerFP("0.0.0.0", 8080, Timeout(300*time.Second), MaxConn(1000))
fmt.Println(s1, s2, s3)
}
在实践中应注意配置文件到配置数据之间映射的解耦:
[Config Web UI] <----+---------+
| ↓
[Config API] --------+--> [Config Data] ----> [System]
| ↑
[Config Language] <--+---------+
YAML:需要先转换成 JSON,再转成 Protobuf。Protobuf 的 Config 对象不能直接扩展方法,所以还需要加一个 Options 方法。
func ApplyYAML(s *redis.Config, yml string) error {
js, err := yaml.YAMLToJSON([]byte(yml))
if err != nil {
return err
}
return ApplyJSON(s, string(js))
}
// Options apply config to options.
func Options(c *redis.Config) []redis.Options {
return []redis.Options{
redis.DialDatabase(c.Database),
redis.DialPassword(c.Password),
redis.DialReadTimeout(c.ReadTimeout),
}
}
Protobuf:使用 wrap struct 区分是否有值。
syntax = "proto3";
import "google/protobuf/duration.proto";
package config.redis.v1;
// redis config.
message redis {
string network = 1;
string address = 2;
int32 database = 3;
string password = 4;
google.protobuf.Duration read_timeout = 5;
}
最终实现配置注入:
func main() {
// load config file from yaml.
c := new(redis.Config)
_ = ApplyYAML(c, loadConfig())
r, _ := redis.Dial(c.Network, c.Address, Options(c)...)
}
实现代码变更系统功能是冗长且复杂的过程,往往还涉及 CR、测试等流程。而更改单个配置选项也可能对功能产生重大影响,且通常情况下修改配置还容易被忽略、未经测试就上线。
配置管理的目标:
Go 依赖管理是通过 Git 仓库模式实现,并随着版本的更迭逐渐完善。
早期是 GOPATH 模式:GOPATH 目录是所有工程的公共依赖包目录,所有需要编译的 go 工程的依赖包都放在 GOPATH 目录下。
后续引入多版本支持的 Vendor 特性:go 1.6 之后开启了 vendor 目录,以支持各个工程对于不同版本的依赖包使用的需求(每个工程拷贝一份代码)。
Go Module 管理:Go1.11 实现了依赖包的升级更新,在 Go1.13 版本后默认打开。
GOPATH 为 Go 开发环境时所设置的一个环境变量。
历史版本的 go 语言开发时,需要将代码放在 GOPATH 目录的 src 文件夹下。go get 命令获取依赖,也会自动下载到 GOPATH 的 src 下。以下命令会将代码下载到 $GOPATH/src/github.com/foo/bar
。
go get github.com/foo/bar
GOPATH 具体结构如下,必须包含三个文件夹:
GOPATH
|-- bin 二进制文件
|-- pkg 预编译文件(加快后续编译速度)
|-- src 源代码
|-- github.com
从 Go 1.11 开始初步支持,解决了依赖版本的信息管理,并且保证安全性 。
由 go.mod 和 go.sum 组成,包括依赖模块路径定义,通过 checksum 保证包的安全性,并且可以在 GOPATH 外创建和编译项目。
使用 go mod init
命令初始化项目,生成 go.mod 文件:
go mod init example.com.hello
cat go.mod
module example.com/hello
go 1.16
使用 go get github.com/sirupsen/logrus
可下载或更新依赖包:
module example.com/hello
go 1.16
require github.com/sirupsen/logrus v1.8.1
各关键字含义:
为解决 Go Modules 的包被篡改的安全隐患,引入 go.sum 文件以记录每个依赖包的哈希值,在构建时如果本地的依赖包 hash 值与 go.sum 文件中记录的不一致,则会拒绝构建。
go get
命令获取,将包下载到本地缓存目录 $GOPATH/pkg/mod/cache/download
,该包后缀为 .zip,并把哈希运算同步到 go.sum 文件中。Go 1.13 的 GOPROXY 默认为 https://proxy.golang.org,在国内需要配置代理才能使用。GOPROXY[9] 也可以解决公司内部的使用问题:
export GOPROXY=https://goproxy.io,direct
# 不走 proxy 的私有仓库或组,以逗号分隔。
export GOPRIVATE=git.mycompany.com,github.com/my/private
用于控制 go 命令把某些仓库视作私有仓库,可以跳过 proxy server 和 checksum 检查,GOPRIVATE 的值同时作为 GONOPROXY 和 GONOSUMDB 默认值:
# 以逗号分隔。
export GOPRIVATE=*.corp.example.com,github.com/org_name
推荐同时配置 GOPROXY 和 GOPRIVATE 使用,GOPRIVATE 也可以识别 Git SSH KEY 进行权限效验。
goproxy.io 是 Go Modules 开源代理,也可作为公司内部代理。
# 下载编译:
git clone https://github.com/goproxyio/goproxy.git
cd goproxy
go build
# 运行代理:
# ./goproxy -listen=0.0.0.0:8081 -cacheDir=/tmp/cache -proxy https://goproxy.io -exclude "github.com/private"
#
# -cacheDir 指定 Go 模块的缓存目录
# -exclude proxy 模式下指定不经过上游服务器的 path
# -listen 服务监听端口,默认 8081
# -proxy 指定上游 proxy server,推荐 goproxy.io
访问内网 Git 仓库:
GONOSUMDB=github.com/private
[url "git@github.com:"]
insteadOf = https://github.com/
[url "git@github.com:"]
insteadOf = https://gitlab.com/
小型测试带来优秀的代码质量、良好的异常处理、优雅的错误报告;大中型测试会带来整体产品质量和数据验证。
不同类型的项目对测试的需求不同,总体上有 70/20/10 经验法则:70% 小型测试,20% 中型测试,10% 大型测试。
如果一个项目是面向用户的,拥有较高的集成度或用户接口比较复杂,就应该有更多的中型和大型测试;如果是基础平台或者面向数据的项目(例如索引或网络爬虫),则最好有大量的小型测试。
单元测试的基本要求:
基于 docker-compose 实现跨平台跨语言环境的容器依赖管理方案,以解决运行 unittest 场景下的容器依赖问题:
包含测试的项目目录结构:
|-- service
|-- api
|-- cmd
|-- configs
|-- internal
|-- test
|-- docker-compose.yaml
|-- database.sql
要满足以下原则:
func TestMain(m *testing.M) {
flag.Set("f", "./test/docker-compose.yaml")
flag.Parse()
if err := lich.Setup(); err != nil {
panic(err)
}
defer lich.Teardown()
if ret := m.Run(); ret != 0 {
panic(ret)
}
}
利用 go 官方提供的 Subtests + Gomock 完成整个单元测试。对于每层代码:
一般的开发测试流程:
I’ll take pkg over internal: https://travisjeffery.com/b/2019/11/i-ll-take-pkg-over-internal/
[2]google/wire: https://github.com/google/wire
[3]googleapis/googleapis: https://github.com/googleapis/googleapis
[4]envoyproxy/data-plane-api: https://github.com/envoyproxy/data-plane-api
[5]istio/api: https://github.com/istio/api
[6]gRPC: https://grpc.io/
[7]wrappers.proto: https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/wrappers.proto
[8]expvar: https://pkg.go.dev/expvar
[9]https://proxy.golang.org,在国内需要配置代理才能使用。GOPROXY: https://proxy.golang.xn--org%2C-ux8is5bfzj85qrnap81iff2ccld904crhfz75bsl3a8vw.goproxy/
[10]Package Oriented Design (ardanlabs.com): https://www.ardanlabs.com/blog/2017/02/package-oriented-design.html
[11]Design Philosophy On Packaging (ardanlabs.com): https://www.ardanlabs.com/blog/2017/02/design-philosophy-on-packaging.html
[12]golang-standards/project-layout: Standard Go Project Layout (github.com): https://github.com/golang-standards/project-layout
[13]浅析VO、DTO、DO、PO的概念、区别和用处 - 随风而逝,只是飘零 - 博客园 (cnblogs.com): https://www.cnblogs.com/zxf330301/p/6534643.html
[14]阿里技术专家详解 DDD 系列- Domain Primitive_chikuai9995的博客-CSDN博客: https://blog.csdn.net/chikuai9995/article/details/100723540?biz_id=102&utm_term=阿里技术专家详解DDD系列&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-0-100723540&spm=1018.2118.3001.4449
[15]阿里技术专家详解DDD系列 第三讲 - Repository模式_淘系技术-CSDN博客: https://blog.csdn.net/taobaojishu/article/details/106152641
[16]Errors | Cloud APIs | Google Cloud: https://cloud.google.com/apis/design/errors
[17]贫血,充血模型的解释以及一些经验_知识库_博客园 (cnblogs.com): https://kb.cnblogs.com/page/520743/
[18]领域驱动设计 实践手册(1.Get Started) - 知乎 (zhihu.com): https://zhuanlan.zhihu.com/p/105466656
[19]DDD 实践手册(2. 实现分层架构) - 知乎 (zhihu.com): https://zhuanlan.zhihu.com/p/105648986
[20]DDD 实践手册(3. Entity, Value Object) - 知乎 (zhihu.com): https://zhuanlan.zhihu.com/p/106634373
[21]DDD 实践手册(4. Aggregate — 聚合) - 知乎 (zhihu.com): https://zhuanlan.zhihu.com/p/107347593
[22]DDD 实践手册(5. Factory 与 Repository) - 知乎 (zhihu.com): https://zhuanlan.zhihu.com/p/109048532
[23]DDD 实践手册(6. Bounded Context - 限界上下文) - 知乎 (zhihu.com): https://zhuanlan.zhihu.com/p/110252394
[24]01、DDD和微服务的关系 - 简书 (jianshu.com): https://www.jianshu.com/p/dfa427762975
[25]Domain Driven Design in Go – Citerus: https://www.citerus.se/go-ddd/
[26]Domain Driven Design in Go: Part 2 – Citerus: https://www.citerus.se/part-2-domain-driven-design-in-go/
[27]Domain Driven Design in Go: Part 3 – Citerus: https://www.citerus.se/part-3-domain-driven-design-in-go/
[28]文章正在审核中… - 简书 (jianshu.com): https://www.jianshu.com/p/5732b69bd1a1
[29]领域驱动设计系列文章(1)——通过现实例子显示领域驱动设计的威力 - Cat Qi - 博客园 (cnblogs.com): https://www.cnblogs.com/qixuejia/p/10789612.html
[30]领域驱动设计系列文章(2)——浅析VO、DTO、DO、PO的概念、区别和用处 - Cat Qi - 博客园 (cnblogs.com): https://www.cnblogs.com/qixuejia/p/4390086.html
[31]领域驱动设计系列文章(3)——有选择性的使用领域驱动设计 - Cat Qi - 博客园 (cnblogs.com): https://www.cnblogs.com/qixuejia/p/10789621.html
[32]区分 Protobuf 中缺失值和默认值 - 知乎 (zhihu.com): https://zhuanlan.zhihu.com/p/46603988
[33]protobuf/wrappers.proto at master · protocolbuffers/protobuf (github.com): https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/wrappers.proto
[34]Functional options for friendly APIs – The acme of foolishness (cheney.net): https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
[35]command center: Self-referential functions and the design of options: https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html
[36]Creating Good API Errors in REST, GraphQL and gRPC | APIs You Won’t Hate - A community that cares about API design and development. (apisyouwonthate.com): https://apisyouwonthate.com/blog/creating-good-api-errors-in-rest-graphql-and-grpc/
[37]Clean Coder Blog: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
[38]GopherCon 2018: Kat Zien - How Do You Structure Your Go Apps - YouTube: https://www.youtube.com/watch?v=oL6JBUk6tj0
[39]zitryss/go-sample: Go Project Sample Layout (github.com): https://github.com/zitryss/go-sample
[40]paper-code/packageorienteddesign.md at master · danceyoung/paper-code (github.com): https://github.com/danceyoung/paper-code/blob/master/package-oriented-design/packageorienteddesign.md
[41]Clean Architecture using Golang. Update | by Elton Minetto | Medium: https://eminetto.medium.com/clean-architecture-using-golang-b63587aa5e3f
[42]Standard Package Layout. Addressing one of the biggest technical… | by Ben Johnson | Medium: https://medium.com/@benbjohnson/standard-package-layout-7cdbc8391fc1
[43]410 Deleted by author — Medium: https://medium.com/wtf-dial/wtf-dial-domain-model-9655cd523182
[44]Trying Clean Architecture on Golang | Hacker Noon: https://hackernoon.com/golang-clean-archithecture-efd6d7c43047
[45]Trying Clean Architecture on Golang — 2 | Hacker Noon: https://hackernoon.com/trying-clean-architecture-on-golang-2-44d615bf8fdf
[46]Applying The Clean Architecture to Go applications • Manuel Kießling (kiessling.net): https://manuel.kiessling.net/2012/09/28/applying-the-clean-architecture-to-go-applications/
[47]katzien/go-structure-examples: Examples for my talk on structuring go apps (github.com): https://github.com/katzien/go-structure-examples
[48]Ashley McNamara + Brian Ketelsen. Go best practices. - YouTube: https://www.youtube.com/watch?v=MzTcsI6tn-0
[49]DTO to Entity and Entity to DTO Conversion - Apps Developer Blog: https://www.appsdeveloperblog.com/dto-to-entity-and-entity-to-dto-conversion/
[50]I’ll take pkg over internal (travisjeffery.com): https://travisjeffery.com/b/2019/11/i-ll-take-pkg-over-internal/
[51]wire/best-practices.md at main · google/wire (github.com): https://github.com/google/wire/blob/main/docs/best-practices.md
[52]wire/guide.md at main · google/wire (github.com): https://github.com/google/wire/blob/main/docs/guide.md
[53]Compile-time Dependency Injection With Go Cloud’s Wire - The Go Blog (golang.org): https://blog.golang.org/wire
[54]google/wire: Compile-time Dependency Injection for Go (github.com): https://github.com/google/wire
[55]Integration Testing in Go: Part I - Executing Tests with Docker (ardanlabs.com): https://www.ardanlabs.com/blog/2019/03/integration-testing-in-go-executing-tests-with-docker.html
[56]Integration Testing in Go: Part II - Set-up and Writing Tests (ardanlabs.com): https://www.ardanlabs.com/blog/2019/10/integration-testing-in-go-set-up-and-writing-tests.html
[57]Testable Examples in Go - The Go Blog (golang.org): https://blog.golang.org/examples
[58]Using Subtests and Sub-benchmarks - The Go Blog (golang.org): https://blog.golang.org/subtests
[59]The cover story - The Go Blog (golang.org): https://blog.golang.org/cover
[60]Keeping Your Modules Compatible - The Go Blog (golang.org): https://blog.golang.org/module-compatibility
[61]Go Modules: v2 and Beyond - The Go Blog (golang.org): https://blog.golang.org/v2-go-modules
[62]Publishing Go Modules - The Go Blog (golang.org): https://blog.golang.org/publishing-go-modules
[63]Module Mirror and Checksum Database Launched - The Go Blog (golang.org): https://blog.golang.org/module-mirror-launch
[64]Migrating to Go Modules - The Go Blog (golang.org): https://blog.golang.org/migrating-to-go-modules
[65]Using Go Modules - The Go Blog (golang.org): https://blog.golang.org/using-go-modules
[66]Go Modules in 2019 - The Go Blog (golang.org): https://blog.golang.org/modules2019
[67]Testing with GoMock: A Tutorial - codecentric AG Blog: https://blog.codecentric.de/en/2017/08/gomock-tutorial/
[68]gomock · pkg.go.dev: https://pkg.go.dev/github.com/golang/mock/gomock
[69]A GoMock Quick Start Guide. An opinionated tutorial for unit… | by Che Dan | Better Programming: https://betterprogramming.pub/a-gomock-quick-start-guide-71bee4b3a6f1?gi=e44758036c10
欢迎关注Go生态。生态君会不定期分享 Go 语言生态相关内容。