一行代碼實現一個 RESTful 接口
背景
基於現在微服務或者服務化的思想,我們大部分的業務邏輯處理函數都是長這樣的:
比如 grpc 服務端:
func (s *Service) GetUserInfo(ctx context.Context, req *pb.GetUserInfoReq) (*pb.GetUserInfoRsp, error) {
// 業務邏輯
// ...
}
grpc 客戶端:
func (s *Service) GetUserInfo(ctx context.Context, req *pb.GetUserInfoReq, opts ...grpc.CallOption) (*pb.GetUserInfoRsp, error) {
// 業務邏輯
// ...
}
有些服務我們需要把它包裝爲 RESTful 形式的接口,一般需要經歷以下步驟:
-
指定 HTTP 方法、URL
-
鑑權
-
參數綁定
-
處理請求
-
處理響應
可以發現,參數綁定、處理響應幾乎都是一樣模板代碼,鑑權也基本上是模板代碼(當然有些鑑權可能比較複雜)。
而 Ginrest 庫就是爲了消除這些模板代碼,它不是一個複雜的框架,只是一個簡單的庫,輔助處理這些重複的事情,爲了實現這個能力使用了 Go1.18 的泛型。
倉庫地址:https://github.com/jiaxwu/ginrest
特性
這個庫提供以下特性:
-
封裝 RESTful 請求響應
-
封裝 RESTful 請求爲標準格式服務
-
封裝標準格式服務處理結果爲標準 RESTful 響應格式:Rsp{code, msg, data}
-
默認使用統一數字錯誤碼格式:[0, 4XXXX, 5XXXX]
-
默認使用標準錯誤格式:Error{code, msg}
-
默認統一狀態碼 [200, 400, 500]
-
提供 Recovery 中間件,統一 panic 時的響應格式
-
提供 SetKey()、GetKey() 方法,用於存儲請求上下文(泛型)
-
提供 ReqFunc(),用於設置 Req(泛型)
使用例子
示例代碼在:https://github.com/jiaxwu/ginrest/blob/main/examples/main.go
首先我們實現兩個簡單的服務:
const (
ErrCodeUserNotExists = 40100 // 用戶不存在
)
type GetUserInfoReq struct {
UID int `json:"uid"`
}
type GetUserInfoRsp struct {
UID int `json:"uid"`
Username string `json:"username"`
Age int `json:"age"`
}
func GetUserInfo(ctx context.Context, req *GetUserInfoReq) (*GetUserInfoRsp, error) {
if req.UID != 10 {
return nil, ginrest.NewError(ErrCodeUserNotExists, "user not exists")
}
return &GetUserInfoRsp{
UID: req.UID,
Username: "user_10",
Age: 10,
}, nil
}
type UpdateUserInfoReq struct {
UID int `json:"uid"`
Username string `json:"username"`
Age int `json:"age"`
}
type UpdateUserInfoRsp struct{}
func UpdateUserInfo(ctx context.Context, req *UpdateUserInfoReq) (*UpdateUserInfoRsp, error) {
if req.UID != 10 {
return nil, ginrest.NewError(ErrCodeUserNotExists, "user not exists")
}
return &UpdateUserInfoRsp{}, nil
}
然後使用 Gin+Ginrest 包裝爲 RESTful 接口:
可以看到 Register() 裏面每個接口都只需要一行代碼!
func main() {
e := gin.New()
e.Use(ginrest.Recovery())
Register(e)
if err := e.Run("127.0.0.1:8000"); err != nil {
log.Println(err)
}
}
// 註冊請求
func Register(e *gin.Engine) {
// 簡單請求,不需要認證
e.GET("/user/info/get", ginrest.Do(nil, GetUserInfo))
// 認證,綁定UID,處理
reqFunc := func(c *gin.Context, req *UpdateUserInfoReq) {
req.UID = GetUID(c)
} // 這裏拆多一步是爲了顯示第一個參數是ReqFunc
e.POST("/user/info/update", Verify, ginrest.Do(reqFunc, UpdateUserInfo))
}
const (
KeyUserID = "KeyUserID"
)
// 簡單包裝方便使用
func GetUID(c *gin.Context) int {
return ginrest.GetKey[int](c, KeyUserID)
}
// 簡單包裝方便使用
func SetUID(c *gin.Context, uid int) {
ginrest.SetKey(c, KeyUserID, uid)
}
// 認證
func Verify(c *gin.Context) {
// 認證處理
// ...
// 忽略認證的具體邏輯
SetUID(c, 10)
}
運行上面代碼,然後嘗試訪問接口,可以看到返回結果:
請求1
GET http://127.0.0.1:8000/user/info/get
{
"uid": 10
}
響應1
{
"code": 0,
"msg": "ok",
"data": {
"uid": 10,
"username": "user_10",
"age": 10
}
}
請求2
GET http://127.0.0.1:8000/user/info/get
{
"uid": 1
}
響應2
{
"code": 40100,
"msg": "user not exists"
}
請求3
POST http://127.0.0.1:8000/user/info/update
{
"username": "jiaxwu",
"age": 10
}
響應3
{
"code": 0,
"msg": "ok",
"data": {}
}
實現原理
Do() 和 DoOpt() 都會轉發到 do(),它其實是一個模板函數,把髒活累活給處理了:
// 處理請求
func do[Req any, Rsp any, Opt any](reqFunc ReqFunc[Req],
serviceFunc ServiceFunc[Req, Rsp], serviceOptFunc ServiceOptFunc[Req, Rsp, Opt], opts ...Opt) gin.HandlerFunc {
return func(c *gin.Context) {
// 參數綁定
req, err := BindJSON[Req](c)
if err != nil {
return
}
// 進一步處理請求結構體
if reqFunc != nil {
reqFunc(c, req)
}
var rsp *Rsp
// 業務邏輯函數調用
if serviceFunc != nil {
rsp, err = serviceFunc(c, req)
} else if serviceOptFunc != nil {
rsp, err = serviceOptFunc(c, req, opts...)
} else {
panic("must one of ServiceFunc and ServiceFuncOpt")
}
// 處理響應
ProcessRsp(c, rsp, err)
}
}
功能列表
處理請求
用於把一個標準服務封裝爲一個 RESTfulgin.HandlerFunc
,對應 Do()、DoOpt() 函數。
DoOpt() 相比於 Do() 多了一個 opts 參數,因爲很多 rpc 框架客戶端都有一個 opts 參數作爲結尾。
還有一個BindJSON()
,用於把請求體包裝爲一個 Req 結構體:
// 參數綁定
func BindJSON[T any](c *gin.Context) (*T, error) {
var req T
if err := c.ShouldBindJSON(&req); err != nil {
FailureCodeMsg(c, ErrCodeInvalidReq, "invalid param")
return nil, err
}
return &req, nil
}
如果無法使用 Do() 和 DoOpt() 則可以使用此方法。
處理響應
用於把 rsp、error、errcode、errmsg 等數據封裝爲一個 JSON 格式響應體,對應 ProcessRsp()、Success()、Failure()、FailureCodeMsg() 函數。
比如ProcessRsp()
需要帶上 rsp 和 error,這樣業務裏面就不需要再寫如下模板代碼了:
// 處理簡單響應
func ProcessRsp(c *gin.Context, rsp any, err error) {
if err != nil {
Failure(c, err)
return
}
Success(c, rsp)
}
響應格式統一爲:
// 響應
type Rsp struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data any `json:"data,omitempty"`
}
Success()
用於處理成功情況:
// 請求成功
func Success(c *gin.Context, data any) {
ginRsp(c, http.StatusOK, &Rsp{
Code: ErrCodeOK,
Msg: "ok",
Data: data,
})
}
其餘同理。
如果無法使用 Do() 和 DoOpt() 則可以使用這些方法。
處理錯誤
一般我們都需要在出錯時帶上一個業務錯誤碼,方便客戶端處理。因此我們需要提供一個合適的 error 類型:
// 錯誤
type Error struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
我們提供了一些函數方便使用Error
,對應 NewError()、ToError()、ErrCode()、ErrMsg()、ErrEqual() 函數。
比如NewError()
生成一個 Error 類型 error:
// 通過code和msg產生一個錯誤
func NewError(code int, msg string) error {
return &Error{
Code: code,
Msg: msg,
}
}
請求上下文操作
Gin 的請求是鏈式處理的,也就是多個 handler 順序的處理一個請求,比如:
reqFunc := func(c *gin.Context, req *UpdateUserInfoReq) {
req.UID = ginrest.GetKey[int](c, KeyUserID)
}
// 認證,綁定UID,處理
e.POST("/user/info/update", Verify, ginrest.Do(reqFunc, UpdateUserInfo))
這個接口經歷了 Verify 和 ginrest.Do 兩個 handler,其中我們在 Verify 的時候通過認證知道了用戶的身份信息(比如 uid),我們希望把這個 uid 存起來,這樣可以在業務邏輯裏使用。
因此我們提供了 SetKey()、GetKey() 兩個函數,用於存儲請求上下文:
比如認證通過後我們可以設置 UID 到上下文,然後在 reqFunc() 裏讀取設置到 req 裏面(下面介紹)。
// 認證
func Verify(c *gin.Context) {
// 認證處理
// ...
// 忽略認證的具體邏輯
ginrest.SetKey(c, KeyUserID, uid)
}
請求結構體處理
上面我們設置了請求上下文,比如 UID,但是其實我們並不知道具體這個 UID 是需要設置到 req 裏的哪個字段,因此我們提供了一個回調函數 ReqFunc(),用於設置 Req:
// 這裏↓
reqFunc := func(c *gin.Context, req *UpdateUserInfoReq) {
req.UID = ginrest.GetKey[int](c, KeyUserID)
}
// 認證,綁定UID,處理
e.POST("/user/info/update", Verify, ginrest.Do(reqFunc, UpdateUserInfo))
注
如果這個庫的設計不符合具體的業務,也可以按照這種思路去封裝一個類似的庫,只要儘可能的統一請求、響應的格式,就可以減少很多重複的模板代碼。
轉自:
https://juejin.cn/post/7132790934353215502
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/RPpRru1W3CBMMKTwpK-q8A