# 簡介 ## 什麼是 go-zero > 解放雙手,加速開發 * **微服務架構** * go-zero 基於微服務架構設計,其生成的程式碼也有固定的架構 * **一鍵生成 code 的工具** * 自動產生的code完整涵蓋入口點到業務邏輯整段,必須要自己寫的部分只有業務邏輯和功能邏輯 * **程式碼實踐的集合** * go-zero 中包含大量的工具封裝外 (log、http、mysql、redis 等),也提供很多設定可以去按需啟用或設定一些內建功能(例如限流、監控日誌) * **大神們的結晶** * go-zero 是個開源專案,各路大神的實踐經驗囊括其中 ## 優點 * 縮短從需求到上線的距離 * 適用於高流量的大規模系統 * 分層架構設計 * 豐富的內建功能支援,ex: log、tracing、限流設定 ## 缺點 * 缺乏自由度 * 支援生成程式碼,但拔除需自行刪除相關 func * 不支援單元測試相關生成 * 不支援 redismock * 不適用複雜的 model 存取模式 ex. db join ## 架構 go-zero 可以只拿來開發 http server 或是 gRpc server / client 的 **單體服務(mono)** 但一個完整的服務通常是...我全都要! 建造一個同時提供對外的 Http 接口加上對內的 gRpc 接口的 **微服務(micro)** * 一對多([參考](https://github.com/zeromicro/zero-doc/blob/main/docs/zero/bookstore.md))移除其中一條 rpc 服務就變成一對一 ![](https://hackmd.io/_uploads/SJREFnjc2.png) :::warning 微服務間盡可能只使用自己相關的資料庫,資料邊界要劃清 ::: ### What is rpc * rpc = 服務間的傳輸協定 * gRPC = 基於 rpc 加入 Protobuf 解決溝通介面混亂與傳輸緩慢的問題 * zRPC = 基於 gRPC 加入 服務註冊、負載均衡、服務攔截器 等功能 是 go-zero 封裝的 rpc 模組 ## 開發流程 基本上只要理解 `.api` 和 `.proto` 要怎麼撰寫,以及如何使用 `goctl` 指令去幫助程式自動產生 就只需要專注在 ==依賴注入==、==業務邏輯== 和 ==功能邏輯== 的開發就好 ![](https://hackmd.io/_uploads/rkJHi2sch.png) ### 常用指令 #### `goctl` 安裝 ```shell $ go install github.com/zeromicro/go-zero/tools/goctl@latest ``` 要是套件安裝失敗要確認環境$PATH ```shell $ export GOPATH=$HOME/go $ export PATH=$PATH:$GOROOT/bin:$GOPATH/bin ``` #### template 生成 api ```shell goctl api -o xxx.api ``` protoc ```shell goctl rpc template -o xxx.proto ``` #### 程式生成 api ```shell goctl api go -api xxx.api -dir . ``` protoc ```shell goctl rpc protoc xxx.proto --go_out=. --go-grpc_out=. --zrpc_out=. ``` model `-c` 代表該 model 啟用 `redis cache` 每次查詢後會把相關結果存入 redis 中,於下次查詢時優先查找 ```shell goctl model mysql ddl -src= xxx.sql -dir ./xxx -c ``` ### goctl 程式生成功能概覽 有些是預設有的,有些是下指令時要特別指定的 他們幾乎都可以用 config 再去做一些細部設定(例如:超時控制的秒數、log config) 圖內已翻譯成常用的名詞,但要在官方簡體文件查找相關用法建議使用原文 * 鏈路追蹤:tracing,追蹤某個請求在微服務間呼叫的路徑 * 監控報警:監控服務數值,如請求數、資料庫連線數 * 數據統計:收集數據提供給(熔斷)斷路器使用 * 自動熔斷:察覺依賴之其他微服務過載時,停止呼叫,並維持自身不噴掉 → 持續等待直到回傳成錯誤 * 自動降載:微服務本身過載時,拒絕溢出的請求進入 * 緩存控制:把DB查詢結果緩存在redis裡,查詢時先找緩存,找不到再去DB;如果DB裡找到了,就把該值放入緩存,找不到就不放。 * 緩存穿透:查詢一個資料庫內一定不存在的值,但因為仍白走了緩存控制的流程,導致服務的壓力 * 緩存擊穿:緩存key被建立前拿不到資料,導致大量請求流入DB * 緩存雪崩:同一時間段內有大量緩存集中失效過期,對服務造成週期性壓力 * 緩存索引:通過索引查找緩存 ![](https://hackmd.io/_uploads/H1ipjhi92.png) # 練習 [專案 Github](https://github.com/esther-lin069/go_zero_practice/tree/master/mainMicro) ## API server(http only) 超基礎版,含 DB、Redis 的連線與簡單操作 ### quick start ``` $ goctl api new demo ``` 可以快速得到一個名叫 demo 的 http server ### 連線DB #### add config `internal/config/config.go` ```go type Config struct { rest.RestConf // 加上DB結構體 DB struct { DsnString string } } ``` `etc/demo-api.yaml` ```yaml # DB DB: DsnString: root:yourPwd@tcp(127.0.0.1:3306)/Demo?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai ``` #### 導入model 先撿到一份建表使用的`.sql`,把它放到 *model/tableName* 底下 ```sql 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'; ``` 然後下指令 ```shell $ 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 互動了 ![](https://hackmd.io/_uploads/rkxz8l3Kn.png) `model/user/usermodel.go` (go-zero幫你產生的) ```go 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), } } ``` #### add svc ```go 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), } } ``` #### create api and logic ##### 註冊API 於專案的`.api`中添加要新增的router、定義傳入及回傳格式 `demo.api` ```go 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) } ``` 執行指令 ```shell $ goctl api go --api demo.api --dir . ``` ==從 router、handler、logic 的空 func 它自動產生==,此時這個路徑已經能正常請求 接下來只要撰寫核心邏輯就好 但如果是移除已經有的api,它不會幫你把存在的檔案刪掉,但會刪除 router ##### 撰寫邏輯 `internal/logic/usersignuplogic.go` ```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 } ``` ### 連線Redis :::info go-zero [不建議(也不支援)](https://go-zero.dev/docs/tutorials/redis/db/selection)用切db的方式來區分不同的用途 ::: #### 載入設定檔 `internal/config/config.go` ```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` 於套件中已定義好的結構如下 ```go 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 檔中添加環境變數,根據上面的結構 ```yaml # Redis Redis: Host: "127.0.0.1:6379" Pass: "" ``` 之後,就會透過 main() 裡的 `conf.MustLoad(*configFile, &c)` 自動載入了 #### add svc 然後我們一樣要在 `ServiceContext` 裡註冊他 ```go 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), } } ``` ### reids 操作 於 */logic* 中透過 `svcCtx` 取用 `RedisClient` redis 連線物件,來進行任何想做的操作 ```go 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 } ``` ### middleware ### resp errorHandle ## 微服務 ### etcd 部署 先起一台 etcd 服務,後續才能繼續開發 grpc 的微服務 `docker-compose.yaml` ```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 ``` ### protoc 撰寫 於目錄 ./project/rpc/{microServiceName} 下執行指令 ``` $ goctl rpc template -o {microServiceName}.proto ``` 調整裡面的內容後,執行指令來生成微服務 ``` $ goctl rpc protoc {microServiceName}.proto --go_out=. --go-grpc_out=. --zrpc_out=. ``` 這是我們撰寫的 ![](https://hackmd.io/_uploads/BJw89ETYn.png) 這是goctl幫我們產生的 ![](https://hackmd.io/_uploads/B1dsyHTYh.png) ## 雜談&待討論 ### 小記 它會幫我們包好 mysql redis的連線以及使用 ### http error handle > https://go-zero.dev/docs/tutorials/http/server/response/ext 要統一每個專案的錯誤處理評估有兩條路 1. 每個專案有自己的 `errorCode & errorMessage(i18n) config` + `統一內容的 response 處理邏輯 pkg code`,於每加一個 API 程式碼產生後自行去 *handler* 裡修改 code 3. 開發一個單位內部的 package,把所有專案錯誤代碼的邏輯都控在裡面。每個人都去修改自己 `goctl` 的模板語法去 import 那個 package,之後所有自動產生的 *handler* 都會使用一樣的錯誤處理方式 ### 單體 api 服務與微服務的選擇 #### 適合單體 * 服務目的較單純 * 跟別的服務不會有業務重疊,不需提供對其他專案的 rpc 接口 #### 適合微服務 * logic 具有複用性,自己用多次 或是跟別的服務有重疊 * 為什麼 api 中的 logic 不應該複用? * logic 層相當於 controller 層,進去出來的參數格式為 api req 和 resp * 兩隻 api 參數不可能一樣,即使抽出當中交集的參數出來 * 若另外定義介面或結構體,它名目是什麼? * 若定義一複用 func 吃相同的 input,那變成 logic 直接相依該 func * 違反依賴注入的架構特性 * 什麼時候會使用 rpc ? * 兩個 api server 會用到相同的邏輯 * ## 會議記錄 ### 分享內容 #### 從零開始學 * 介紹go-zero * 功能 * 介紹架構 * 實際生出來 rpc + api + model(含常用指令) demo * 先準備檔案 上面寫好註解 * 直接操作 * 大致介紹內部 call func 方式 看code * 介紹已經串起來的專案 串的方式 看code * 可以生成前端 code * 其他補充內容(middleware api-error-handle ### 如何變成我們的架構 * ++ db redis 連線 * db join * config 從哪來 分環境 * gcp log、request log 紀錄哪些 logx 怎用 * gcp * elk api request * error resp * error msg 依類型區分 * testing * pub/sub 哪些模式下會用到 * 服務怎麼切微服務 repo的大小 ### 產出 三包不同程度的專案 * 初始 * .api * .protoc * 指令流程都準備好 * 基礎都建好 * 一對一微服務 * 有連線,可真的存取資料 * 開發流程,怎麼注入 * 可能的未來架構 各功能有可以參考的基底 * 有中間層 * 有log * 有測試 * 微服務間的溝通?不同repo間? ## 架構設計 * 每個 Microservice 或 MonoService 都以 Domain 做切分,只會去連線 Domain 相關的 table * 服務們共用的 package 如 實作 log、api 回傳格式化及 checkSession 的核心邏輯 * rpc 的定位為,同時提供對內及對外接口,對內給自身的 api gateway,對外供其他 Domain 的服務使用(如:bet in user 需要 call event 的賽事資訊 ``` 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 ```