解放雙手,加速開發
go-zero 可以只拿來開發 http server 或是 gRpc server / client 的 單體服務(mono)
但一個完整的服務通常是…我全都要!
建造一個同時提供對外的 Http 接口加上對內的 gRpc 接口的 微服務(micro)
微服務間盡可能只使用自己相關的資料庫,資料邊界要劃清
基本上只要理解 .api
和 .proto
要怎麼撰寫,以及如何使用 goctl
指令去幫助程式自動產生
就只需要專注在 依賴注入、業務邏輯 和 功能邏輯 的開發就好
goctl
安裝$ go install github.com/zeromicro/go-zero/tools/goctl@latest
要是套件安裝失敗要確認環境$PATH
$ export GOPATH=$HOME/go
$ export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
api
goctl api -o xxx.api
protoc
goctl rpc template -o xxx.proto
api
goctl api go -api xxx.api -dir .
protoc
goctl rpc protoc xxx.proto --go_out=. --go-grpc_out=. --zrpc_out=.
model
-c
代表該 model 啟用 redis cache
每次查詢後會把相關結果存入 redis 中,於下次查詢時優先查找
goctl model mysql ddl -src= xxx.sql -dir ./xxx -c
有些是預設有的,有些是下指令時要特別指定的
他們幾乎都可以用 config 再去做一些細部設定(例如:超時控制的秒數、log config)
圖內已翻譯成常用的名詞,但要在官方簡體文件查找相關用法建議使用原文
超基礎版,含 DB、Redis 的連線與簡單操作
$ goctl api new demo
可以快速得到一個名叫 demo 的 http server
internal/config/config.go
type Config struct {
rest.RestConf
// 加上DB結構體
DB struct {
DsnString string
}
}
etc/demo-api.yaml
# DB
DB:
DsnString: root:yourPwd@tcp(127.0.0.1:3306)/Demo?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
先撿到一份建表使用的.sql
,把它放到 model/tableName 底下
CREATE TABLE user (
id bigint AUTO_INCREMENT,
name varchar(255) NULL COMMENT 'The username',
password varchar(255) NOT NULL DEFAULT '' COMMENT 'The user password',
mobile varchar(255) NOT NULL DEFAULT '' COMMENT 'The mobile phone number',
gender char(10) NOT NULL DEFAULT 'male' COMMENT 'gender,male|female|unknown',
nickname varchar(255) NULL DEFAULT '' COMMENT 'The nickname',
type tinyint(1) NULL DEFAULT 0 COMMENT 'The user type, 0:normal,1:vip, for test golang keyword',
create_at timestamp NULL,
update_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE mobile_index (mobile),
UNIQUE name_index (name),
PRIMARY KEY (id)
) ENGINE = InnoDB COLLATE utf8mb4_general_ci COMMENT 'user table';
然後下指令
$ goctl model mysql ddl --src user.sql --dir ./model/tableName
它會自動產生這些檔案,usermodel_gen.go
裡面就已經包含了基礎的CRUD,有 unique 的 key 也會產生好 byKey 去查詢的 func,需要更多再自行增加即可
以這個 case 來說,只要把 usermodel.go
中的 UserModel
new 出來後,就可以透過 usermodel_gen.go
裡的 func 與 DB 互動了
model/user/usermodel.go
(go-zero幫你產生的)
package mysql
import "github.com/zeromicro/go-zero/core/stores/sqlx"
var _ UserModel = (*customUserModel)(nil)
type (
// UserModel is an interface to be customized, add more methods here,
// and implement the added methods in customUserModel.
UserModel interface {
userModel
}
customUserModel struct {
*defaultUserModel
}
)
// NewUserModel returns a model for the database table.
func NewUserModel(conn sqlx.SqlConn) UserModel {
return &customUserModel{
defaultUserModel: newUserModel(conn),
}
}
import usermodel "go_zero/demo/model/user"
type ServiceContext struct {
Config config.Config
// 定義 UserModel 結構體
UserModel usermodel.UserModel
}
func NewServiceContext(c config.Config) *ServiceContext {
// 資料庫連線
sqlConn := sqlx.NewMysql(c.DB.DsnString)
return &ServiceContext{
Config: c,
// 把 UserModel物件 new 出來
UserModel: usermodel.NewUserModel(sqlConn),
}
}
於專案的.api
中添加要新增的router、定義傳入及回傳格式
demo.api
type SignUpRequest {
Name string `json:"name"`
Password string `json:"password"`
Mobile string `json:"mobile,optional"`
Gender string `json:"gender,options=M|F"`
Nickname string `json:"nickname"`
}
type SignUpResponse {
Message string `json:"message"`
}
service demo-api {
@handler UserSignUpHandler
post /user/sign-up(SignUpRequest) returns (SignUpResponse)
}
執行指令
$ goctl api go --api demo.api --dir .
從 router、handler、logic 的空 func 它自動產生,此時這個路徑已經能正常請求
接下來只要撰寫核心邏輯就好
但如果是移除已經有的api,它不會幫你把存在的檔案刪掉,但會刪除 router
internal/logic/usersignuplogic.go
// ...
// 上面的code是go-zero自動產生,不用改
func (l *UserSignUpLogic) UserSignUp(req *types.SignUpRequest) (resp *types.SignUpResponse, err error) {
// 呼叫前面在 svc 中新定義的 UserModel 下的功能
// 將請求內容寫入資料庫
_, err = l.svcCtx.UserModel.Insert(l.ctx, &usermodel.User{
Name: sql.NullString{
String: req.Name,
Valid: true,
},
Password: req.Password,
Mobile: req.Mobile,
Nickname: req.Nickname,
Gender: req.Gender,
})
// 設定回傳
resp = &types.SignUpResponse{
Message: fmt.Sprintf("Name: %s Added", req.Name),
}
return
}
go-zero 不建議(也不支援)用切db的方式來區分不同的用途
internal/config/config.go
package config
import (
"github.com/zeromicro/go-zero/core/stores/redis" // go-zero包好的redis庫
"github.com/zeromicro/go-zero/rest"
)
type Config struct {
rest.RestConf
DB struct {
DsnString string
}
// 新增一行 redis 設定
Redis redis.RedisConf
}
RedisConf
於套件中已定義好的結構如下
type RedisConf struct {
Host string
Type string `json:",default=node,options=node|cluster"`
Pass string `json:",optional"`
Tls bool `json:",optional"`
NonBlock bool `json:",default=true"`
// PingTimeout is the timeout for ping redis.
PingTimeout time.Duration `json:",default=1s"`
}
於 yaml 檔中添加環境變數,根據上面的結構
# Redis
Redis:
Host: "127.0.0.1:6379"
Pass: ""
之後,就會透過 main() 裡的 conf.MustLoad(*configFile, &c)
自動載入了
然後我們一樣要在 ServiceContext
裡註冊他
package svc
import (
"go_zero/demo/demo/internal/config"
usermodel "go_zero/demo/demo/model/user"
"github.com/zeromicro/go-zero/core/stores/redis"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type ServiceContext struct {
Config config.Config
// 定義 redis 連線物件
RedisClient *redis.Redis
// 定義 UserModel 結構體
UserModel usermodel.UserModel
}
func NewServiceContext(c config.Config) *ServiceContext {
// 資料庫連線
sqlConn := sqlx.NewMysql(c.DB.DsnString)
// redis 連線
redisConn := redis.MustNewRedis(c.Redis)
return &ServiceContext{
Config: c,
// redis 連線物件
RedisClient: redisConn,
// UserModel db 連線物件
UserModel: usermodel.NewUserModel(sqlConn),
}
}
於 /logic 中透過 svcCtx
取用 RedisClient
redis 連線物件,來進行任何想做的操作
package logic
import (
"context"
"database/sql"
"fmt"
"go_zero/demo/demo/internal/svc"
"go_zero/demo/demo/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type UserLoginLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewUserLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserLoginLogic {
return &UserLoginLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
//-----以上code由go-zero產生-----
// 以下 UserLogin 內的功能要自己寫
func (l *UserLoginLogic) UserLogin(req *types.LoginRequest) (resp *types.LoginResponse, err error) {
result, err := l.svcCtx.UserModel.FindOneByName(l.ctx, sql.NullString{
String: req.Name,
Valid: true,
})
if result.Password != req.Password {
return
}
// 存入redis
err = l.svcCtx.RedisClient.Set(fmt.Sprintf("%d", result.Id), result.Name.String)
resp = &types.LoginResponse{
Token: fmt.Sprintf("%s@%d", result.Name.String, result.Id),
}
return
}
先起一台 etcd 服務,後續才能繼續開發 grpc 的微服務
docker-compose.yaml
version: '3'
networks:
web-network:
services:
docker-etcd:
hostname: etcd
image: bitnami/etcd:3.5.5
volumes:
- "./etcd/data:/bitnami/etcd/data"
environment:
- ALLOW_NONE_AUTHENTICATION=yes
- ETCD_ADVERTISE_CLIENT_URLS=http://0.0.0.0:2379
ports:
- "2379:2379"
- "2380:2380"
networks:
- web-network
docker-etcdkeeper:
hostname: etcdkeeper
image: evildecay/etcdkeeper:v0.7.6
ports:
- "8099:8080"
networks:
- web-network
於目錄 ./project/rpc/{microServiceName} 下執行指令
$ goctl rpc template -o {microServiceName}.proto
調整裡面的內容後,執行指令來生成微服務
$ goctl rpc protoc {microServiceName}.proto --go_out=. --go-grpc_out=. --zrpc_out=.
這是我們撰寫的
這是goctl幫我們產生的
它會幫我們包好 mysql redis的連線以及使用
要統一每個專案的錯誤處理評估有兩條路
每個專案有自己的 errorCode & errorMessage(i18n) config
+ 統一內容的 response 處理邏輯 pkg code
,於每加一個 API 程式碼產生後自行去 handler 裡修改 code
開發一個單位內部的 package,把所有專案錯誤代碼的邏輯都控在裡面。每個人都去修改自己 goctl
的模板語法去 import 那個 package,之後所有自動產生的 handler 都會使用一樣的錯誤處理方式
三包不同程度的專案
初始
基礎都建好
可能的未來架構 各功能有可以參考的基底
project-structure-design
├── event.go
├── go.mod
├─> pkg (共用套件)
│ ├─> checkSession
│ ├─> logger
│ └─> responseHandler
├─> service-micro (event 微服務)
│ ├─> api
│ │ ├─> desc
│ │ │ ├── event.api
│ │ │ └─> types
│ │ │ └── event.api
│ │ ├─> etc
│ │ │ └── event.yaml
│ │ ├── event.go
│ │ └─> internal
│ │ ├─> config
│ │ │ ├── config.go
│ │ │ └─> errorCode
│ │ ├─> handler
│ │ │ ├─> event
│ │ │ │ └── geteventhandler.go
│ │ │ └── routes.go
│ │ ├─> logic
│ │ │ └─> event
│ │ │ └── geteventlogic.go
│ │ ├─> middleware (自定義中間件)
│ │ │ └── middleware.go
│ │ ├─> svc
│ │ │ └── servicecontext.go
│ │ └─> types
│ │ └── types.go
│ ├─> model
│ │ └─> sql
│ └─> rpc
│ ├─> etc
│ │ └── event.yaml
│ ├── event.go
│ ├─> eventctl
│ │ └── eventctl.go
│ ├─> internal
│ │ ├─> config
│ │ │ └── config.go
│ │ ├─> logic
│ │ │ ├── loginlogic.go
│ │ │ └── registerlogic.go
│ │ ├─> server
│ │ │ └── eventctlserver.go
│ │ └─> svc
│ │ └── servicecontext.go
│ ├─> pb
│ │ ├── event.pb.go
│ │ └── event_grpc.pb.go
│ └─> proto
│ └── event.proto
└─> service-mono (user 單體服務)
├─> api
│ ├─> desc
│ │ └── user.api
│ ├─> etc
│ │ └── user.yaml
│ ├─> internal
│ │ ├─> config
│ │ │ ├── config.go
│ │ │ └─> errorCode
│ │ ├─> handler
│ │ │ ├─> bet
│ │ │ │ └── bethandler.go
│ │ │ └── routes.go
│ │ ├─> logic
│ │ │ └─> bet
│ │ │ └── betlogic.go
│ │ ├─> middleware
│ │ ├─> svc
│ │ │ └── servicecontext.go
│ │ └─> types
│ │ └── types.go
│ └── user.go
└─> model
└─> sql