共计 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-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/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-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
: 日志等级(debug
、info
、warn
、error
、panic
、fatal
)--log.format
: 日志输入格局(text
、json
)--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 进行封装,现反对 file
和 gorm
两种类型的 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
客户端
基于 kratos
的 gRPC
客户端进行封装,依据传入的地址主动判断是走直连还是服务发现
如何获取客户端实例:
- 注入类型:
*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
的服务注册与发现进行封装,现反对 etcd
和 consul
,可依据配置进行切换,如果同时配置,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
标准的链路追踪
transport
中 HTTP
和 gRPC
均已注册链路追踪的中间件
如何获取 tracerProvider
和 tracer
:
- 注入类型:
*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
文档的形式有三种
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
文档
浏览器关上 <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/