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

    • 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 traceimport "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 greetimport "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 permissionimport "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 traceimport "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 transportimport "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 userimport "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 userimport "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 userimport "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 traceimport "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 userimport "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/