关于go:goscaffold-一个基于-kratos-和-wire-依赖注入框架的脚手架

3次阅读

共计 11324 个字符,预计需要花费 29 分钟才能阅读完成。

  • 介绍
  • 架构图
  • 生命周期
  • 目录构造
  • 如何运行

    • go build 或 go run
    • make
    • docker-compose
    • 热重启
    • 运行子命令或脚本
  • 依赖注入
  • 配置

    • 配置模型
    • 近程配置
    • 监听配置变更
  • 日志
  • 错误处理

    • 转换为 HTTP 状态码
    • 将 GRPC 谬误转换为 Error
  • 组件

    • Casbin
    • Client

      • gRPC 客户端
    • Discovery 服务发现与注册
    • Ent
    • orm

      • 如何配置多数据库
    • Redis 客户端
    • trace
    • uid
  • transport 层

    • HTTP

      • 响应
      • swagger 文档生成
      • 如何拜访 swagger 文档
  • service 层
  • 命令行功能模块
  • cron 定时工作功能模块
  • 如何部署

    • Dockerfile
    • docker-compose
    • kubernetes

地址:https://github.com/OldSmokeGu…

欢送 Star,欢送 PR,心愿大家能够一起探讨和斧正!

介绍

go-scaffold 是一个基于 cobra 和 kratos 框架的脚手架,设计思维是基于 wire 实现模块和性能的组件化

go-scaffold 开箱即用,应用简略,能够疾速搭建起一个微服务进行业务代码的开发,反对性能:

  • 依赖注入
  • cobra 命令行
  • cron 定时工作
  • apollo 近程配置核心和配置监听
  • 日志切割
  • 服务注册和发现
  • jaeger 链路追踪
  • Swagger 文档生成
  • docker-composeKubernetes 部署

架构图

生命周期

目录构造

|-- bin # 二进制文件目录
|-- cmd # 编译入口
|   `-- app
|-- deploy # 环境和部署相干目录
|   |-- docker-compose # docker-compose 容器编排目录
|   `-- kubernetes # k8s 编排配置目录
|-- docs # 文档目录
|-- etc # 配置文件目录
|-- internal
|   `-- app
|       |-- command # 命令行功能模块
|       |   |-- handler
|       |   `-- script # 长期脚本
|       |-- component # 性能组件,如:db, redis 等
|       |-- config # 配置模型
|       |-- cron # 定时工作功能模块
|       |   `-- job
|       |-- model # 数据库模型
|       |-- pkg # 性能类库
|       |-- repository # 数据处理层
|       |-- service # 业务逻辑层
|       |-- test
|       `-- transport
|           |-- grpc
|           |   |-- api # proto 文件目录
|           |   |-- handler # 管制层
|           |   `-- middleware # 中间件
|           `-- http
|               |-- api # swagger 文档
|               |-- handler # 管制层
|               |-- middleware # 中间件
|               `-- router # 路由
|-- logs # 日志目录
|-- pkg # 性能类库
`-- proto # 第三方 proto 文件目录 

如何运行

首先将 etc/config.yaml.example 拷贝为 etc/config.yaml

go buildgo run

  1. go build 形式
$ go generate ./...
$ go build -o bin/app cmd/app/main.go cmd/app/wire_gen.go
$ ./bin/app
  1. go run 形式
$ go generate ./...
$ go run cmd/app/main.go cmd/app/wire_gen.go

make

# 下载依赖
$ make download
$ make build

# 或根据平台编译
$ make linux-build
$ make windows-build
$ make mac-build

# 运行
$ ./bin/app

docker-compose

docker-compose 的启动形式有两种,一种是基于 air 镜像,一种是基于 Dockerfile 来构建镜像

留神:

  • 基于 air 镜像的形式只实用于开发阶段,请勿用于生产环境

    • Windows 零碎环境下,热更新可能不会失效,这是因为 fsnotify 无奈收到 wsl 文件系统的变更告诉
  • 基于 Dockerfile 的形式如果用于开发阶段,批改的代码将不会更新,除非在 docker-compose 启动时指定 --build 参数,然而这将会导致每次启动时都从新构建镜像,可能须要期待很长时间
# 基于 air 
$ docker-compose -f deploy/docker-compose/docker-compose-dev.yaml up

# 基于 Dockerfile
$ docker-compose -f deploy/docker-compose/docker-compose.yaml up

热重启

热重启性能基于 air

$ air

运行子命令或脚本

命令行程序性能基于 cobra

$ ./bin/app [标记] < 子命令 > [标记] [参数]

# 帮忙信息

$ ./bin/app -h
$ ./bin/app < 子命令 > -h

依赖注入

依赖通过主动生成代码的形式在编译期实现注入

依赖构造:

配置

默认配置文件门路为:etc/app/config.yaml

能够在运行程序时通过 --config-f 选项指定其它配置文件

配置模型

配置文件的内容在程序启动时会被加载到配置模型中,相干目录:internal/app/config

  • internal/app/config/declare.go:配置的构造体定义
  • internal/app/config/config.go:申明 Provider 和监听的配置 Key

如何获取配置模型:

  • 注入配置模型类型:*config.Config
  • 注入 App 配置模型类型:*config.App

例:

package trace

import "go-scaffold/internal/app/config"

type Handler struct {
    conf    *config.Config
    appConf *config.App
}

func NewHandler(
    conf *config.Config,
    appConf *config.App,
) *Handler {
    return &Handler{
        conf:    conf,
        appConf: appConf,
    }
}

近程配置

在启动程序时,可通过以下选项配置近程配置核心

  • --config.apollo.enable: apollo 是否启用
  • --config.apollo.endpoint: 连贯地址
  • --config.apollo.appid: appID
  • --config.apollo.cluster: cluster
  • --config.apollo.namespace: 命名空间
  • --config.apollo.secret: secret

监听配置变更

internal/app/config/config.go 文件的 watchKeys 变量中注册须要监听的配置键

注册实现后,如果配置文件内容产生变更,无需重启服务,更改内容会主动同步到配置实例中

例:

var watchKeys = []string{
   "services.self",
   "jwt.key",
}

日志

日志基于 zap,日志的轮转基于 file-rotatelogs

日志内容默认输入到 logs 目录中,并且依据每天的日期进行宰割

可在程序启动时,通过以下选项扭转日志行为:

  • --log.path: 日志输入门路
  • --log.level: 日志等级(debuginfowarnerrorpanicfatal
  • --log.format: 日志输入格局(textjson
  • --log.caller-skip: 日志 caller 跳过层数

如何获取日志实例:

  • 注入类型:log.Logger

例:

package greet

import "github.com/go-kratos/kratos/v2/log"

type Service struct {logger *log.Helper}

func NewService(logger log.Logger) *Service {
    return &Service{logger: log.NewHelper(logger),
    }
}

错误处理

脚手架定义了对立的谬误格局

type Error struct {
    // Code 状态码
    Code ErrorCode

    // Message 错误信息
    Message string

    // Metadata 元数据
    Metadata map[string]string
}

快捷函数:

// ServerError 服务器谬误
func ServerError(options ...Option) *Error {return New(ServerErrorCode, ServerErrorCode.String(), options...)
}

// ClientError 客户端谬误
func ClientError(options ...Option) *Error {return New(ClientErrorCode, ClientErrorCode.String(), options...)
}

// ValidateError 参数校验谬误
func ValidateError(options ...Option) *Error {return New(ValidateErrorCode, ValidateErrorCode.String(), options...)
}

// Unauthorized 未认证
func Unauthorized(options ...Option) *Error {return New(UnauthorizedCode, UnauthorizedCode.String(), options...)
}

// PermissionDenied 权限回绝谬误
func PermissionDenied(options ...Option) *Error {return New(PermissionDeniedCode, PermissionDeniedCode.String(), options...)
}

// ResourceNotFound 资源不存在
func ResourceNotFound(options ...Option) *Error {return New(ResourceNotFoundCode, ResourceNotFoundCode.String(), options...)
}

// TooManyRequest 申请太过频繁
func TooManyRequest(options ...Option) *Error {return New(TooManyRequestCode, TooManyRequestCode.String(), options...)
}

转换为 HTTP 状态码

Code 属性实现了 HTTP 状态码的转换

例:

func (s *Service) Hello(ctx context.Context, req HelloRequest) (*HelloResponse, error) {
    // ...
    
    // 返回 Error
    return nil, errors.ServerError()
    
    // ...
}
// ...

// 调用 service 办法
ret, err := h.service.Hello(ctx.Request.Context(), *req)
if err != nil {
    // response.Error 办法会主动将 Error 转换为对应的 HTTP 状态
    response.Error(ctx, err)
    return
}

// ...

GRPC 谬误转换为 Error

Error 实现了 GRPCStatus() 接口,通过 FromGRPCError 函数可将 GRPC 谬误转换为 Error

例:

// ...

client := greet.NewGreetClient(conn)
resp, err := client.Hello(reqCtx, &greet.HelloRequest{Name: "Example"})
if err != nil {
    // 将 GRPC 谬误转换为 Error
    e := errors.FromGRPCError(err)
    response.Error(ctx, fmt.Errorf("GRPC 调用谬误:%s", e.Message))
    return
}

// ...

组件

Casbin

基于 casbin 进行封装,现反对 filegorm 两种类型的 adapter,如果同时配置,file 类型失效

如何获取 Enforcer 实例:

  • 注入类型:*casbin.Enforcer

例:

package permission

import "github.com/casbin/casbin/v2"

type Service struct {enforcer *casbin.Enforcer}

func NewService(enforcer *casbin.Enforcer) *Service {
    return &Service{enforcer: enforcer,}
}

如何进行配置:

casbin:
  model: # casbin 模型
    path: "assets/casbin/rbac_model.conf"
  adapter: # 适配器配置
    file:
      path: "assets/casbin/rbac_policy.csv"
    gorm:
      tableName: "casbin_rules" # 数据表名称 

如何自定义 casbin policy 的数据库存储模型:

internal/app/config/config.go 文件的 Loaded 函数中减少代码

func Loaded(hLogger log.Logger, cfg config.Config, conf *Config) error {
    // ...
    
    if conf.Casbin != nil {
        if conf.Casbin.Adapter != nil {
            if conf.Casbin.Adapter.Gorm != nil {conf.Casbin.Adapter.Gorm.SetMigration(func(db *gorm.DB) error {return (&model.CasbinRule{}).Migrate(db)
                })
            }
        }
    }

    // ...
}

Client

gRPC 客户端

基于 kratosgRPC 客户端进行封装,依据传入的地址主动判断是走直连还是服务发现

如何获取客户端实例:

  • 注入类型:*grpc.Client

例:

package trace

import "go-scaffold/internal/app/component/client/grpc"

type Handler struct {grpcClient *grpc.Client}

func NewHandler(grpcClient *grpc.Client,) *Handler {
    return &Handler{grpcClient: grpcClient,}
}

如何进行配置:

services:
  self: "127.0.0.1:9528"
  # self: "discovery:///go-scaffold" # 服务发现地址 

Discovery 服务发现与注册

基于 kratos 的服务注册与发现进行封装,现反对 etcdconsul,可依据配置进行切换,如果同时配置,etcd 失效

如何获取 Discovery 实例:

  • 注入类型:discovery.Discovery

例:

package transport

import "go-scaffold/internal/app/component/discovery"

type Transport struct {// ...}

func New(discovery discovery.Discovery) *Transport {// ...}

如何进行配置:

discovery:
  etcd:
    endpoints:
      - "localhost:12379"
  # consul:
  #   addr: "localhost:8500"
  #   schema: "http"

Ent

ent 组件基于 ent

如何获取 ent 客户端:

  • 注入类型:*ent.Client

例:

package user

import "go-scaffold/internal/app/component/ent/ent"

type Repository struct {ent *ent.Client}

func NewRepository(ent *ent.Client) *Repository {
    return &Repository{ent: ent,}
}

orm

orm 组件基于 gorm

如何获取 orm 实例:

  • 注入类型:*gorm.DB

例:

package user

import "gorm.io/gorm"

type Repository struct {db *gorm.DB}

func NewRepository(db *gorm.DB) *Repository {
    return &Repository{db: db,}
}

如何配置多数据库

参考:https://gorm.io/docs/dbresolv…

etc/config.yaml:

db:
  driver: "mysql"
  host: "127.0.0.1"
  port: 13306
  database: "go-scaffold"
  username: "root"
  password: "root"
  options:
    - "charset=utf8mb4"
    - "parseTime=True"
    - "loc=Local"
  maxIdleConn: 5
  maxOpenConn: 10
  connMaxIdleTime: 120
  connMaxLifeTime: 120
  logLevel: "info"
  # 多数据库配置
  resolvers:
    - type: "replica" # source 或 replica
      host: "127.0.0.1"
      port: 13307
      database: "go-scaffold"
      username: "root"
      password: "root"
      options:
        - "charset=utf8mb4"
        - "parseTime=True"
        - "loc=Local"
    - type: "replica"
      host: "127.0.0.1"
      port: 13308
      database: "go-scaffold"
      username: "root"
      password: "root"
      options:
        - "charset=utf8mb4"
        - "parseTime=True"
        - "loc=Local"

internal/app/config/config.go:

func Loaded(hLogger log.Logger, cfg config.Config, conf *Config) error {
    // ...
    
    // 配置多数据库
    if conf.DB != nil {if len(conf.DB.Resolvers) > 0 {
            var (sources  = make([]gorm.Dialector, 0, len(conf.DB.Resolvers))
                replicas = make([]gorm.Dialector, 0, len(conf.DB.Resolvers))
            )
    
            for _, resolver := range conf.DB.Resolvers {dial, err := orm.BuildDialector(conf.DB.Driver, resolver.DSN)
                if err != nil {return err}
                switch resolver.Type {
                case orm.Source:
                    sources = append(sources, dial)
                case orm.Replica:
                    replicas = append(replicas, dial)
                default:
                    return fmt.Errorf("unsupported resolver type %s", resolver.Type)
                }
            }
    
            conf.DB.Plugins = func(db *gorm.DB) ([]gorm.Plugin, error) {return []gorm.Plugin{
                    dbresolver.Register(dbresolver.Config{
                        Sources:  sources,
                        Replicas: replicas,
                        Policy:   dbresolver.RandomPolicy{},}),
                }, nil
            }
        }
    }
    
    // ...
}

Redis 客户端

Redis 客户端基于 go-redis

如何获取 Redis 客户端:

  • 注入类型:*redis.Client

例:

package user

import "github.com/go-redis/redis/v8"

type Repository struct {rdb *redis.Client}

func NewRepository(rdb *redis.Client) *Repository {
    return &Repository{rdb: rdb,}
}

trace

脚手架基于 opentelemetry-go 实现了 OpenTelemetry 标准的链路追踪

transportHTTPgRPC 均已注册链路追踪的中间件

如何获取 tracerProvidertracer

  • 注入类型:*redis.Client

例:

package trace

import "go-scaffold/internal/app/component/trace"

type Handler struct {trace *trace.Tracer}

func NewHandler(trace *trace.Tracer,) *Handler {
    return &Handler{trace: trace,}
}

uid

uid 组件是基于 snowflake 实现的 uid 生成器,可用于数据库主键

如何获取 uid 实例:

  • 注入类型:uid.Generator

例:

package user

import "go-scaffold/internal/app/component/uid"

type Repository struct {id uid.Generator}

func NewRepository(id uid.Generator) *Repository {
    return &Repository{id: id,}
}

transport

HTTP

响应

internal/app/transport/http/pkg/response 包中,对 JSON 响应进行了封装

胜利响应示例:

func (h *Handler) Hello(ctx *gin.Context) {
    // ...

    response.Success(ctx, response.WithData(ret))
    return
}

谬误响应示例:

func (h *Handler) Hello(ctx *gin.Context) {
    // ...
    
    ret, err := h.service.Hello(ctx.Request.Context(), *req)
        if err != nil {response.Error(ctx, err)
        return
    }
    
    // ...
}

swagger 文档生成

swagger 文档的生成基于 swag,对立生成到 internal/app/transport/http/api 目录下,否则无法访问

生成 swagger 文档的形式有三种

  1. swag 命令形式
$ swag fmt -d internal/app -g app.go
$ swag init -d internal/app -g app.go -o internal/app/transport/http/api
  1. make 形式
$ make doc
  1. go generate 形式
$ go generate ./...

如何拜访 swagger 文档

浏览器关上 <host>/api/docs

service

service 层解决业务逻辑 transport 层中的 HTTPgRPC,或命令行都只是其中一个入口

参数的校验基于 ozzo-validation,对立放到 service

例:

type CreateRequest struct {
    Name  string `json:"name"`
    Age   int8   `json:"age"`
    Phone string `json:"phone"`
}

func (r CreateRequest) Validate() error {
    return validation.ValidateStruct(&r,
        validation.Field(&r.Name, validation.Required.Error("名称不能为空")),
        validation.Field(&r.Phone, validation.By(validator.IsMobilePhone)),
    )
}

type CreateResponse struct {
    Id    uint64 `json:"id"`
    Name  string `json:"name"`
    Age   int8   `json:"age"`
    Phone string `json:"phone"`
}

func (s *Service) Create(ctx context.Context, req CreateRequest) (*CreateResponse, error) {
    // 参数校验
    if err := req.Validate(); err != nil {return nil, errorsx.ValidateError(errorsx.WithMessage(err.Error()))
    }

    // ...
}

命令行功能模块

命令行功能模块基于 cobra

命令行性能被形象为两局部,一部分称为“业务命令”(command),一部分称为“脚本”(script

  • “业务命令”设计用于通过命令行的形式调用业务逻辑
  • “脚本”设计用于执行开发过程中的长期脚本工作,例如:进行数据修复
  • “业务命令”被注册为应用程序的 business 子命令,“脚本”被注册为应用程序的 script 子命令

命令行目录标准:

  • “业务命令”和“脚本”的注册位于 internal/app/command/command.go 文件中
  • “业务命令”局部:

    • “业务命令”在 internal/app/command/handler 目录中进行定义
    • 应依照不同的职责对包进行纵向拆分,例如:postusercomment 三个业务模块,每一个模块都独立对外提供相应的性能
    • 每个业务模块都是一个独自的包,对应 business 命令的子命令,例如:./bin/app business post
    • 业务模块中的每个办法都抽离为一个独自的文件,对应业务模块命令的子命令,例如:./bin/app business post add
  • “脚本”局部:

    • “脚本”在 internal/app/command/script 目录中进行定义
    • 脚本文件的名称为 S+10 位工夫戳,阐明脚本的创立工夫
    • 文件中的构造体名称为脚本文件名,并且实现 Script 接口
    • 构造体的正文应该阐明此脚本的用处

留神:

不要通过零碎的定时工作来频繁调用命令行性能的“业务命令”或“脚本”,因为每次执行都会初始化数据库连贯、日志等资源,这可能会造成性能问题

如果须要频繁调用某个业务逻辑,能够思考是否应该应用 cron 功能模块

cron 定时工作功能模块

定时工作功能模块基于 cron

  • 其能够提供最小工夫单位为秒的定时工作
  • 可明确晓得我的项目中有那些定时工作

定时工作标准:

  • 工作在 internal/app/cron/cron.go 文件中进行注册
  • internal/app/cron/job 目录中进行定义
  • 工作构造体的名称为工作文件名,并且实现 cron.Job 接口
  • 构造体的正文应该阐明此工作的用处

如何部署

Dockerfile

Dockerfile 文件位于我的项目根目录

docker-compose

docker-compose 编排文件位于 deploy/docker-compose 目录中

部署前依据须要将 docker-compose.yaml.exampledocker-compose-dev.yaml.example 拷贝为 docker-compose.yaml,而后依据 docker-compose 运行

Kubernetes

Kubernetes 编排文件位于 deploy/kubernetes 目录中

Kubernetes 的形式基于 helm,部署前须要将 values.yaml.example 拷贝为 values.yaml

而后执行:

$ kubectl apply -Rf deploy/kubernetes

# 或

$ helm install go-scaffold kubernetes/
正文完
 0