项目结构体系
关于 Kratos
Kratos 是 B 站开源的一款轻量型微服务框架。包含大量微服务相关框架及工具。
整套 Kratos 框架也是不错的学习仓库,可以了解和参考到微服务方面的技术积累和经验。
Kratos 有较为丰富的组件和抽象层次;
自己感觉的比较好的点如下:
结合了依赖注入框架 Wire, 实际体验会让代码可维护性更好一些
抽象出了 Transport 层,便于自己替换其他的 Web 服务器实现,如 Gin, hertz**, fiber 等等**
一些丰富的中间件,如
- logger,支持自动 reload, 从多个源(文件,配置中心,环境遍历)读取配置并合并
- Tracing,支持集成了 OpenTelemetry
- Monitor,暴露 prometheus 接口
支持丰富的中间件
- 服务发现: etcd, k8s, consul;
- 以及一些限流,熔断算法等等
方便的代码生成,可以通过定义 proto 的方式,自动生成 client 和 Service 层代码,并能直接导出 openapi 文件
提供了较为丰富的 example 和一个默认的 Layout 文件,里面大多参照 DDD 领域驱动设计,结构良好
架构图如下

框架包含的主要特性:
- APIs:协议通信以 HTTP/gRPC 为基础,通过 Protobuf 进行定义;
- Errors:通过 Protobuf 的 Enum 作为错误码定义,以及工具生成判定接口;
- Metadata:在协议通信 HTTP/gRPC 中,通过 Middleware 规范化服务元信息传递;
- Config:支持多数据源方式,进行配置合并铺平,通过 Atomic 方式支持动态配置;
- Logger:标准日志接口,可方便集成三方 log 库,并可通过 fluentd 收集日志;
- Metrics:统一指标接口,可以实现各种指标系统,默认集成 Prometheus;
- Tracing:遵循 OpenTelemetry 规范定义,以实现微服务链路追踪;
- Encoding:支持 Accept 和 Content-Type 进行自动选择内容编码;
- Transport:通用的 HTTP/gRPC 传输层,实现统一的 Middleware 插件支持;
- Registry:实现统一注册中心接口,可插件化对接各种注册中心;
自己做的一些实践
工具方面
简单封了相关 kratos 和 protoc 的接口,搞了个 flowgame-ctl 命令行
.PHONY: all user-private gorm-gen gen-config
game-name:
go run ./cmd/name
gen-gorm:
go run ./cmd/gorm-generator
gen-proto:
../flowgame-ctl/flowgame-ctl service gen
gen-config:
protoc --proto_path=./internal --proto_path=/data/game/common/flowgame-apis --go_out=paths=source_relative:./internal internal/conf/*.proto
gen-client:
../flowgame-ctl/flowgame-ctl service server
migrate:
go run ./cmd/gorm-migrate --dsn '$(DSN)'
项目结构方面(以 flowgame-game-name 为例)

API
- 存放本项目的接口
- 参照了 googleapis 的规范 https://cloud.google.com/apis/design/standard_methods?hl=zh-cn

- 如果这个项目被其他项目调用,会将 api 目录拷贝到公共的 flowgame-apis 仓库

代码示例如下:
syntax = "proto3";
package flowgame.game.name.v1;
import "google/api/annotations.proto";
option go_package = "gitlab.xgjoy.org/flowgame/flowgame-game-name/api/name/v1;v1";
service NameRandLibService {
rpc ListNameRandLib (ListNameRandLibRequest) returns (ListNameRandLibReply) {
option (google.api.http) = {
get: "name/randlib",
};
}
rpc GetNameRandLib (GetNameRandLibRequest) returns (GetNameRandLibReply) {
option (google.api.http) = {
get: "name/randlib/{id}",
};
}
rpc CreateNameRandLib (CreateNameRandLibRequest) returns (CreateNameRandLibReply) {
option (google.api.http) = {
post: "name/randlib",
body: "*",
};
}
rpc DeleteNameRandLib (DeleteNameRandLibRequest) returns (DeleteNameRandLibReply) {
option (google.api.http) = {
delete: "name/randlib/{id}",
};
}
rpc UpdateNameRandLib (UpdateNameRandLibRequest) returns (UpdateNameRandLibReply) {
option (google.api.http) = {
put: "name/randlib/{id}",
body: "*",
};
}
}
message ListNameRandLibItemRequest {
int32 lib_id = 1; // 随机库id
}
message ListNameRandLibItemReply {
repeated NameRandLibItem items = 1;
}
message GetNameRandLibItemRequest {
int32 lib_id = 1; // 随机库id
int32 item_id = 2; // 名字id
}
message GetNameRandLibItemReply {
NameRandLibItem item = 1;
}
message NameRandLibItem {
int32 id = 1; // 项id
int64 name_id = 2; // 名字id
}
cmd 目录
- 项目入口
- 包含 orm 生成工具,迁移工具和项目本身
Internal
- 存放具体的代码
- Biz 领域层,被 Service 调用,他会调用 data 层
- Conf 配置,里面包含配置定义
- Data 数据层,包含对数据库的操作,对其他服务的操作等等
- Server 单独的传输层,包含各种 HTTP 或者 GRPC 服务
- Service 工具自动生成的,是请求的入口,会调用 biz 层的逻辑
原本的框架示例是上边的层次,集成 gorm-gen 后,我又加了两层
- Model 单独存放模型,解决循环依赖问题
- Query gorm-gen 生成的代码
Conf
syntax = "proto3";
package conf;
option go_package = "gitlab.xgjoy.org/flowgame/flowgame-game-name/internal/conf;conf";
import "flowgame-apis-common/api/conf/v1/conf.proto";
message Bootstrap {
flowgame.common.api.conf.Server server = 1;
Data data = 2;
flowgame.common.api.conf.Trace trace = 3;
flowgame.common.api.conf.Registry registry = 4;
}
message Data {
message Database {
string driver = 1;
string source = 2;
}
message Redis {
string host = 1;
int32 port = 2;
string password = 3;
int32 db = 4;
}
Database database = 1;
Redis redis = 2;
}
如上所示,用 proto 定义配置格式,同时从公共仓库引用了公共配置项,更具有复用性
Data 层
package data
import (
"github.com/go-kratos/kratos/contrib/registry/etcd/v2"
"github.com/go-kratos/kratos/v2/registry"
"github.com/go-redis/redis"
"github.com/google/wire"
commonConf "gitlab.xgjoy.org/flowgame/flowgame-apis-common/api/conf/v1"
"gitlab.xgjoy.org/flowgame/flowgame-game-name/internal/conf"
"gitlab.xgjoy.org/flowgame/flowgame-game-name/internal/pkg/util"
"gitlab.xgjoy.org/flowgame/flowgame-game-name/internal/query"
clientv3 "go.etcd.io/etcd/client/v3"
)
type Data struct {
query *query.Query
rdb *redis.Client
}
func NewData(data *conf.Data) *Data {
return &Data{
query: query.Use(util.GetDBClient(data.Database.Source)),
rdb: util.GetRedisClient(data.Redis.Host, int(data.Redis.Port), data.Redis.Password, int(data.Redis.Db)),
}
}
func NewDiscovery(conf *commonConf.Registry) registry.Discovery {
// new etcd client
client, err := clientv3.New(clientv3.Config{
Endpoints: conf.GetEtcd().GetEndpoints(),
})
if err != nil {
panic(err)
}
// new reg with etcd client
reg := etcd.New(client)
return reg
}
func NewRegistrar(conf *commonConf.Registry) registry.Registrar {
// new etcd client
client, err := clientv3.New(clientv3.Config{
Endpoints: conf.GetEtcd().GetEndpoints(),
})
if err != nil {
panic(err)
}
reg := etcd.New(client)
return reg
}
var (
ProviderSet = wire.NewSet(
NewData,
NewDiscovery,
NewRegistrar,
NewNameGroupRepo,
NewNameItemRepo,
NewNameMapRepo,
NewNameRandLibRepo,
NewNameRandLibItemRepo,
)
)
定义了服务注册和发现,以及一个公共的数据源 Data,这个 Data 接收 conf.Data, 据此创建数据库链接
借助 wire,实现各种对象的自动注入
Pkg
存放一些公共逻辑,工具函数之类的
Query
gorm-gen 自动生成的
关于 gorm-gen
- 支持根据 sql 生成方法
type NameGroupQuerier interface {
// SELECT * FROM @@table WHERE name = @name{{if role !=""}} AND role = @role{{end}}
FilterWithNameAndRole(name, role string) ([]gen.T, error)
// UPDATE @@table SET realname = @realname, id_card = @idcard where uid = @uid
UpdateRealnameByUID(uid uint64, realname, idcard string) error
// UPDATE @@table SET phone = @phone where uid = @uid
UpdatePhoneByUID(uid uint64, phone string) error
// UPDATE @@table SET phone = NULL where uid = @uid
DeletePhoneByUID(uid uint64) error
// UPDATE @@table SET phone = NULL where phone = @phone
DeletePhoneByPhone(phone string) error
}
- 支持如下的写法
func (repo *NameMapRepo) GetName(ctx context.Context, nameID int64) (string, error) {
q := repo.Data.query.NameMap
item, err := repo.Data.query.NameMap.WithContext(ctx).Where(q.ID.Eq(int(nameID))).First()
if err != nil {
return "", err
}
return item.Name, nil
}
server 层

service 层
自动生成的入口
有关链路追踪和监控
只需要如下一些代码实现就好了,非常方便

主流的 redis-go 和 gorm 都支持 OpenTel 的集成,可以方便的监控到他们的 Trace 数据,便于查看整个请求的耗时情况
有关 wire

在 wire 中定义相关需要注入的函数,执行 wire 就可以自动注入了
wire 自动识别依赖关系,wireApp 的参数列表,将与 wire.Build 的参数一起,传递到需要该参数的对象中去
有关 openapi.yaml
直接自动或者手动导入到 apifox 或者 postman,便于共享 API 和调试