deepwzh的技术笔记
  • Golang
  • Python
  • Cpp
杂项
前端开发
  • 计算机网络
  • 操作系统
  • 数据库
云原生
Devops
AIGC
关于作者
  • Golang
  • Python
  • Cpp
杂项
前端开发
  • 计算机网络
  • 操作系统
  • 数据库
云原生
Devops
AIGC
关于作者
  • Golang
  • 工具
  • 最佳实践
  • 学习资料
  • 代码生成
  • 安装与配置

    • 安装与配置
    • GVM
    • 私有仓库拉取
    • 安装与配置
  • 库

    • 库
    • Grpc-go
    • 命令行参数
    • 依赖注入库 wire
  • 编译器

    • 编译器
    • clean
    • 逃逸分析
    • 编译过程
    • 构建选项
    • 编译优化

      • 编译优化
      • PGO
  • 语言

    • 语言
    • defer
    • go:embed
  • 微服务框架

    • 微服务框架
    • Kratos

      • Kratos
      • 集成三方 web 框架
      • 项目结构体系

项目结构体系

关于 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 和调试

Prev
集成三方 web 框架