- 介紹
- 架構圖
- 生命週期
- 目錄結構
-
如何運行
- 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/