欢迎 Star,欢迎 PR,希望大家可以一起讨论和指正!

介绍

go-scaffold
go-scaffold
apollojaegerSwaggerdocker-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.exampleetc/config.yaml

go build 或 go run

go build
$ go generate ./...
$ go build -o bin/app cmd/app/main.go cmd/app/wire_gen.go
$ ./bin/app
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-composeairDockerfile

注意:

airWindowsfsnotifywslDockerfiledocker-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.gointernal/app/config/config.goProviderKey

如何获取配置模型:

*config.ConfigApp*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.enableapollo--config.apollo.endpoint--config.apollo.appidappID--config.apollo.clustercluster--config.apollo.namespace--config.apollo.secretsecret

监听配置变更

internal/app/config/config.gowatchKeys

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

例:

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

日志

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

logs

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

--log.path--log.leveldebuginfowarnerrorpanicfatal--log.formattextjson--log.caller-skipcaller

如何获取日志实例:

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 状态码

CodeHTTP

例:

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

ErrorGRPCStatus()FromGRPCErrorGRPCError

例:

// ...

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

filegormadapterfile
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.goLoaded
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 服务发现与注册

kratosetcdconsuletcd
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.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
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,
    }
}

如何配置多数据库

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
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
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

uiduid
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/responseJSON

成功响应示例:

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 文档生成

swaggerinternal/app/transport/http/api
swagger
swag
$ swag fmt -d internal/app -g app.go
$ swag init -d internal/app -g app.go -o internal/app/transport/http/api
make
$ make doc
go generate
$ go generate ./...

如何访问 swagger 文档

/api/docs

service 层

servicetransportHTTPgRPC
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

commandscript
businessscript

命令行目录规范:

internal/app/command/command.gointernal/app/command/handlerpostusercommentbusiness./bin/app business post./bin/app business post addinternal/app/command/scriptS10Script
cron

cron 定时任务功能模块

定时任务功能模块基于 cron

  • 其可以提供最小时间单位为秒的定时任务
  • 可明确知道项目中有那些定时任务

定时任务规范:

internal/app/cron/cron.gointernal/app/cron/jobcron.Job

如何部署

Dockerfile

Dockerfile

docker-compose

docker-composedeploy/docker-compose
docker-compose.yaml.exampledocker-compose-dev.yaml.exampledocker-compose.yaml

Kubernetes

Kubernetesdeploy/kubernetes
Kuberneteshelmvalues.yaml.examplevalues.yaml

然后执行:

$ kubectl apply -Rf deploy/kubernetes

# 或

$ helm install go-scaffold kubernetes/