- 介绍
- 架构图
- 生命周期
- 目录构造
如何运行
- 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-compose和Kubernetes部署
架构图
生命周期
目录构造
|-- 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 build 或 go run
go build形式
$ go generate ./...$ go build -o bin/app cmd/app/main.go cmd/app/wire_gen.go$ ./bin/appgo run形式
$ go generate ./...$ go run cmd/app/main.go cmd/app/wire_gen.gomake
# 下载依赖$ make download$ make build# 或根据平台编译$ make linux-build$ make windows-build$ make mac-build# 运行$ ./bin/appdocker-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: 日志等级(debug、info、warn、error、panic、fatal)--log.format: 日志输入格局(text、json)--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 进行封装,现反对 file 和 gorm 两种类型的 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 客户端
基于 kratos 的 gRPC 客户端进行封装,依据传入的地址主动判断是走直连还是服务发现
如何获取客户端实例:
- 注入类型:
*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 的服务注册与发现进行封装,现反对 etcd 和 consul,可依据配置进行切换,如果同时配置,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 标准的链路追踪
transport 中 HTTP 和 gRPC 均已注册链路追踪的中间件
如何获取 tracerProvider 和 tracer:
- 注入类型:
*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 文档的形式有三种
swag命令形式
$ swag fmt -d internal/app -g app.go$ swag init -d internal/app -g app.go -o internal/app/transport/http/apimake形式
$ make docgo generate形式
$ go generate ./...如何拜访 swagger 文档
浏览器关上 <host>/api/docs
service 层
service 层解决业务逻辑 transport 层中的 HTTP 和 gRPC,或命令行都只是其中一个入口
参数的校验基于 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目录中进行定义 - 应依照不同的职责对包进行纵向拆分,例如:
post、user、comment三个业务模块,每一个模块都独立对外提供相应的性能 - 每个业务模块都是一个独自的包,对应
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.example 或 docker-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/