欢迎 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/