開箱即用的微服務框架 Go-zero(進階篇)

又拍雲發表於2021-06-23

之前我們簡單介紹過 Go-zero 詳見《Go-zero:開箱即用的微服務框架》。這次我們從動手實現一個 Blog 專案的使用者模組出發,詳細講述 Go-zero 的使用。

特別說明本文涉及的所有資料都已上傳 Github 倉庫 “kougazhang/go-zero-demo”, 感興趣的同學可以自行下載。

Go-zero 實戰專案:blog

本文以 blog 的網站後臺為例,著重介紹一下如何使用 Go-zero 開發 blog 的使用者模組。

使用者模組是後臺管理系統常見的模組,它的功能大家也非常熟悉。管理使用者涉及到前端操作,使用者資訊持久化又離不開資料庫。所以使用者模組可謂是 "麻雀雖小五臟俱全"。本文將詳細介紹一下如何使用 go-zero 完成使用者模組功能,如:使用者登入、新增使用者、刪除使用者、修改使用者、查詢使用者 等(完整的 Api 文件請參考倉庫程式碼)

Blog 整體架構

blog 系統整體架構圖

最上面是 api 閘道器層。go-zero 需要 api 閘道器層來代理請求,把 request 通過 gRPC 轉發給對應的 rpc 服務去處理。這塊把具體請求轉發到對應的 rpc 服務的業務邏輯,需要手寫。

接下來是 rpc 服務層。上圖 rpc 服務中的 user 就是接下來向大家演示的模組。每個 rpc 服務可以單獨部署。服務啟動後會把相關資訊註冊到 ETCD,這樣 api 閘道器層就可以通過 ECTD 發現具體服務的地址。rpc 服務處理具體請求的業務邏輯,需要手寫。

最後是Model 層。model 層封裝的是資料庫操作的相關邏輯。如果是查詢類的相關操作,會先查詢 redis 中是否有對應的快取。非查詢類操作,則會直接操作 MySQL。goctl 能通過 sql 檔案生成普通的 CRDU 程式碼。上文也有提到,目前 goctl 這部分功能只支援 MySQL。

下面演示如何使用 go-zero 開發一個 blog 系統的使用者模組。

api 閘道器層

編寫 blog.api 檔案

  • 生成 blog.api 檔案

執行命令 goctl api -o blog.api,建立 blog.api 檔案。

  • api 檔案的作用

api 檔案的詳細語法請參閱文件[https://go-zero.dev/cn/api-grammar.html],本文按照個人理解談一談 api 檔案的作用和基礎語法。

api 檔案是用來生成 api 閘道器層的相關程式碼的。

  • api 檔案的語法

api 檔案的語法和 Golang 語言非常類似,type 關鍵字用來定義結構體,service 部分用來定義 api 服務。

type 定義的結構體,主要是用來宣告請求的入參和返回值的,即 request 和 response.

service 定義的 api 服務,則宣告瞭路由,handler,request 和 response.

具體內容請結合下面的預設的生成的 api 檔案進行理解。

// 宣告版本,可忽略
syntax = "v1"

// 宣告一些專案資訊,可忽略
info(
   title: // TODO: add title
   desc: // TODO: add description
   author: "zhao.zhang"
   email: "zhao.zhang@upai.com"
)

// 重要配置
// request 是結構體的名稱,可以使用 type 關鍵詞定義新的結構體
type request {
   // TODO: add members here and delete this comment
   // 與 golang 語言一致,這裡宣告結構體的成員
}

// 語法同上,只是業務含義不同。response 一般用來宣告返回值。
type response {
   // TODO: add members here and delete this comment
}

// 重要配置
// blog-api 是 service 的名稱.
service blog-api {
   // GetUser 是處理請求的檢視函式
   @handler GetUser // TODO: set handler name and delete this comment
   // get 宣告瞭該請求使用 GET 方法
   // /users/id/:userId 是 url,:userId 表明是一個變數
   // request 就是上面 type 定義的那個 request, 是該請求的入參
   // response 就是上面 type 定義的那個 response, 是該請求的返回值。
   get /users/id/:userId(request) returns(response)

   @handler CreateUser // TODO: set handler name and delete this comment
   post /users/create(request)
}
  • 編寫 blog.api 檔案

鑑於文章篇幅考慮完整的 blog.api 檔案請參考 gitee 上的倉庫。下面生成的程式碼是按照倉庫上的 blog.api 檔案生成的。

api 相關程式碼

  • 生成相關的程式碼

執行命令 goctl api go -api blog.api -dir . ,生成 api 相關程式碼。

  • 目錄介紹

├── blog.api # api 檔案
├── blog.go # 程式入口檔案
├── etc
│   └── blog-api.yaml # api 閘道器層配置檔案
├── go.mod
├── go.sum
└── internal
    ├── config
    │   └── config.go # 配置檔案
    ├── handler # 檢視函式層, handler 檔案與下面的 logic 檔案一一對應
    │   ├── adduserhandler.go
    │   ├── deleteuserhandler.go
    │   ├── getusershandler.go
    │   ├── loginhandler.go
    │   ├── routes.go
    │   └── updateuserhandler.go
    ├── logic # 需要手動填充程式碼的地方
    │   ├── adduserlogic.go
    │   ├── deleteuserlogic.go
    │   ├── getuserslogic.go
    │   ├── loginlogic.go
    │   └── updateuserlogic.go
    ├── svc # 封裝 rpc 物件的地方,後面會將
    │   └── servicecontext.go
    └── types # 把 blog.api 中定義的結構體對映為真正的 golang 結構體
        └── types.go
  • 檔案間的呼叫關係

因為到此時還沒涉及到 rpc 服務,所以 api 內各模組的呼叫關係就是非常簡單的單體應用間的呼叫關係。routers.go 是路由,根據 request Method 和 url 把請求分發到對應到的 handler 上,handler 內部會去呼叫對應的 logic. logic 檔案內是我們注入程式碼邏輯的地方。

小結

Api 層相關命令:

  • 執行命令 goctl api -o blog.api, 建立 blog.api 檔案。

  • 執行命令 goctl api go -api blog.api -dir . ,生成 api 相關程式碼。

  • 加引數 goctl 也可以生成其他語言的 api 層的檔案,比如 java、ts 等,嘗試之後發現很難用,所以不展開了。

rpc 服務

編寫 proto 檔案

  • 生成 user.proto 檔案

使用命令 goctl rpc template -o user.proto, 生成 user.proto 檔案

  • user.proto 檔案的作用

user.proto 的作用是用來生成 rpc 服務的相關程式碼。

protobuf 的語法已經超出了 go-zero 的範疇了,這裡就不詳細展開了。

  • 編寫 user.proto 檔案

鑑於文章篇幅考慮完整的 user.proto 檔案請參考 gitee 上的倉庫。

生成 rpc 相關程式碼

  • 生成 user rpc 服務相關程式碼

使用命令 goctl rpc proto -src user.proto -dir . 生成 user rpc 服務的程式碼。

小結

rpc 服務相關命令:

  • 使用命令 goctl rpc template -o user.proto, 生成 user.proto 檔案

  • 使用命令 goctl rpc proto -src user.proto -dir . 生成 user rpc 服務的程式碼。

api 服務呼叫 rpc 服務

A:為什麼本節要安排在 rpc 服務的後面?

Q:因為 logic 部分的內容主體就是呼叫對應的 user rpc 服務,所以我們必須要在 user rpc 的程式碼已經生成後才能開始這部分的內容。

A:api 閘道器層呼叫 rpc 服務的步驟

Q:對這部分目錄結構不清楚的,可以參考前文 “api 閘道器層-api 相關程式碼-目錄介紹”。

  • 編輯配置檔案 etc/blog-api.yaml,配置 rpc 服務的相關資訊。

Name: blog-api
Host: 0.0.0.0
Port: 8888
# 新增 user rpc 服務.
User:
  Etcd:
#  Hosts 是 user.rpc 服務在 etcd 中的 value 值  
    Hosts:
      - localhost:2379
# Key 是 user.rpc 服務在 etcd 中的 key 值
    Key: user.rpc
  • 編輯檔案 config/config.go

type Config struct {
   rest.RestConf
   // 手動新增
   // RpcClientConf 是 rpc 客戶端的配置, 用來解析在 blog-api.yaml 中的配置
   User zrpc.RpcClientConf
}
  • 編輯檔案 internal/svc/servicecontext.go
type ServiceContext struct {
   Config config.Config
   // 手動新增
   // users.Users 是 user rpc 服務對外暴露的介面
   User   users.Users
}

func NewServiceContext(c config.Config) *ServiceContext {
   return &ServiceContext{
      Config: c,
      // 手動新增
      //  zrpc.MustNewClient(c.User) 建立了一個 grpc 客戶端
      User:   users.NewUsers(zrpc.MustNewClient(c.User)),
   }
}
  • 編輯各個 logic 檔案,這裡以 internal/logic/loginlogic.go 為例
func (l *LoginLogic) Login(req types.ReqUser) (*types.RespLogin, error) {
   // 呼叫 user rpc 的 login 方法
   resp, err := l.svcCtx.User.Login(l.ctx, &users.ReqUser{Username: req.Username, Password: req.Password})
   if err != nil {
      return nil, err
   }
   return &types.RespLogin{Token: resp.Token}, nil
}

model 層

編寫 sql 檔案

編寫建立表的 SQL 檔案 user.sql, 並在資料庫中執行。

CREATE TABLE `user`
(
  `id` int NOT NULL AUTO_INCREMENT COMMENT 'id',
  `username` varchar(255) NOT NULL UNIQUE COMMENT 'username',
  `password` varchar(255) NOT NULL COMMENT 'password',
  PRIMARY KEY(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

生成 model 相關程式碼

執行命令 goctl model mysql ddl -c -src user.sql -dir ., 會生成運算元據庫的 CRDU 的程式碼。

此時的 model 目錄:

├── user.sql # 手寫
├── usermodel.go # 自動生成
└── vars.go # 自動生成

model 生成的程式碼注意點

  • model 這塊程式碼使用的是拼接 SQL 語句,可能會存在 SQL 注入的風險。

  • 生成 CRUD 的程式碼比較初級,需要我們手動編輯 usermodel.go 檔案,自己拼接業務需要的 SQL。參見 usermdel.go 中的 FindByName 方法。

rpc 呼叫 model 層的程式碼

rpc 目錄結構

rpc 服務我們只需要關注下面加註釋的檔案或目錄即可。


├── etc
│   └── user.yaml # 配置檔案,資料庫的配置寫在這
├── internal
│   ├── config
│   │   └── config.go # config.go 是 yaml 對應的結構體
│   ├── logic # 填充業務邏輯的地方
│   │   ├── createlogic.go
│   │   ├── deletelogic.go
│   │   ├── getalllogic.go
│   │   ├── getlogic.go
│   │   ├── loginlogic.go
│   │   └── updatelogic.go
│   ├── server
│   │   └── usersserver.go
│   └── svc
│       └── servicecontext.go # 封裝各種依賴
├── user
│   └── user.pb.go
├── user.go
├── user.proto
└── users
    └── users.go

rpc 呼叫 model 層程式碼的步驟

  • 編輯 etc/user.yaml 檔案
Name: user.rpc
ListenOn: 127.0.0.1:8080
Etcd:
  Hosts:
  - 127.0.0.1:2379
  Key: user.rpc
# 以下為手動新增的配置
# mysql 配置
DataSource: root:1234@tcp(localhost:3306)/gozero
# 對應的表
Table: user
# redis 作為換儲存
Cache:
  - Host: localhost:6379
  • 編輯 internal/config/config.go 檔案
type Config struct {
// zrpc.RpcServerConf 表明繼承了 rpc 服務端的配置
   zrpc.RpcServerConf
   DataSource string          // 手動程式碼
   Cache      cache.CacheConf // 手動程式碼
}
  • 編輯 internal/svc/servicecontext.go, 把 model 等依賴封裝起來。

type ServiceContext struct {
   Config config.Config
   Model  model.UserModel // 手動程式碼
}

func NewServiceContext(c config.Config) *ServiceContext {
   return &ServiceContext{
      Config: c,
      Model:  model.NewUserModel(sqlx.NewMysql(c.DataSource), c.Cache), // 手動程式碼
   }
}
  • 編輯對應的 logic 檔案,這裡以 internal/logic/loginlogic.go 為例:
func (l *LoginLogic) Login(in *user.ReqUser) (*user.RespLogin, error) {
   // todo: add your logic here and delete this line
   one, err := l.svcCtx.Model.FindByName(in.Username)
   if err != nil {
      return nil, errors.Wrapf(err, "FindUser %s", in.Username)
   }

   if one.Password != in.Password {
      return nil, fmt.Errorf("user or password is invalid")
   }

   token := GenTokenByHmac(one.Username, secretKey)
   return &user.RespLogin{Token: token}, nil
}

微服務演示執行

我們是在單機環境下執行整個微服務,需要啟動以下服務:

  • Redis

  • Mysql

  • Etcd

  • go run blog.go -f etc/blog-api.yaml

  • go run user.go -f etc/user.yaml

在上述服務中,rpc 服務要先啟動,然後閘道器層再啟動。

在倉庫中我封裝了 start.sh 和 stop.sh 指令碼來分別在單機環境下執行和停止微服務。

好了,通過上述六個步驟,blog 使用者模組的常見功能就完成了。

最後再幫大家強調下重點,除了 goctl 常用的命令需要熟練掌握,go-zero 檔案命名也是有規律可循的。配置檔案是放在 etc 目錄下的 yaml 檔案,該 yaml 檔案對應的結構體在 interval/config/config.go 中。依賴管理一般會在 interval/svc/xxcontext.go 中進行封裝。需要我們填充業務邏輯的地方是 interval/logic 目錄下的檔案。

相關文章