網(wǎng)站首頁 編程語言 正文
背景
基于現(xiàn)在微服務(wù)或者服務(wù)化的思想,我們大部分的業(yè)務(wù)邏輯處理函數(shù)都是長這樣的:
比如grpc服務(wù)端:
func (s *Service) GetUserInfo(ctx context.Context, req *pb.GetUserInfoReq) (*pb.GetUserInfoRsp, error) { // 業(yè)務(wù)邏輯 // ... }
grpc客戶端:
func (s *Service) GetUserInfo(ctx context.Context, req *pb.GetUserInfoReq, opts ...grpc.CallOption) (*pb.GetUserInfoRsp, error) { // 業(yè)務(wù)邏輯 // ... }
有些服務(wù)我們需要把它包裝為RESTful形式的接口,一般需要經(jīng)歷以下步驟:
- 指定HTTP方法、URL
- 鑒權(quán)
- 參數(shù)綁定
- 處理請求
- 處理響應(yīng)
可以發(fā)現(xiàn),參數(shù)綁定、處理響應(yīng)幾乎都是一樣模板代碼,鑒權(quán)也基本上是模板代碼(當(dāng)然有些鑒權(quán)可能比較復(fù)雜)。
而Ginrest庫就是為了消除這些模板代碼,它不是一個(gè)復(fù)雜的框架,只是一個(gè)簡單的庫,輔助處理這些重復(fù)的事情,為了實(shí)現(xiàn)這個(gè)能力使用了Go1.18的泛型。
倉庫地址:github.com/jiaxwu/ginr…
特性
這個(gè)庫提供以下特性:
- 封裝RESTful請求響應(yīng)
- 封裝RESTful請求為標(biāo)準(zhǔn)格式服務(wù)
- 封裝標(biāo)準(zhǔn)格式服務(wù)處理結(jié)果為標(biāo)準(zhǔn)RESTful響應(yīng)格式:Rsp{code, msg, data}
- 默認(rèn)使用統(tǒng)一數(shù)字錯(cuò)誤碼格式:[0, 4XXXX, 5XXXX]
- 默認(rèn)使用標(biāo)準(zhǔn)錯(cuò)誤格式:Error{code, msg}
- 默認(rèn)統(tǒng)一狀態(tài)碼[200, 400, 500]
- 提供Recovery中間件,統(tǒng)一panic時(shí)的響應(yīng)格式
- 提供SetKey()、GetKey()方法,用于存儲(chǔ)請求上下文(泛型)
- 提供ReqFunc(),用于設(shè)置Req(泛型)
使用例子
示例代碼在:github.com/jiaxwu/ginr…
首先我們實(shí)現(xiàn)兩個(gè)簡單的服務(wù):
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()里面每個(gè)接口都只需要一行代碼!
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) { // 簡單請求,不需要認(rèn)證 e.GET("/user/info/get", ginrest.Do(nil, GetUserInfo)) // 認(rèn)證,綁定UID,處理 reqFunc := func(c *gin.Context, req *UpdateUserInfoReq) { req.UID = GetUID(c) } // 這里拆多一步是為了顯示第一個(gè)參數(shù)是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) } // 認(rèn)證 func Verify(c *gin.Context) { // 認(rèn)證處理 // ... // 忽略認(rèn)證的具體邏輯 SetUID(c, 10) }
運(yùn)行上面代碼,然后嘗試訪問接口,可以看到返回結(jié)果:
請求1
GET http://127.0.0.1:8000/user/info/get
{
? ? "uid": 10
}
響應(yīng)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
}
響應(yīng)2
{
? ? "code": 40100,
? ? "msg": "user not exists"
}
請求3
POST http://127.0.0.1:8000/user/info/update
{
? ? "username": "jiaxwu",
? ? "age": 10
}
響應(yīng)3
{
? ? "code": 0,
? ? "msg": "ok",
? ? "data": {}
}
實(shí)現(xiàn)原理
Do()和DoOpt()都會(huì)轉(zhuǎn)發(fā)到do(),它其實(shí)是一個(gè)模板函數(shù),把臟活累活給處理了:
// 處理請求 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) { // 參數(shù)綁定 req, err := BindJSON[Req](c) if err != nil { return } // 進(jìn)一步處理請求結(jié)構(gòu)體 if reqFunc != nil { reqFunc(c, req) } var rsp *Rsp // 業(yè)務(wù)邏輯函數(shù)調(diào)用 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") } // 處理響應(yīng) ProcessRsp(c, rsp, err) } }
功能列表
處理請求
用于把一個(gè)標(biāo)準(zhǔn)服務(wù)封裝為一個(gè)RESTfulgin.HandlerFunc
,對應(yīng)Do()、DoOpt()函數(shù)。
DoOpt()相比于Do()多了一個(gè)opts參數(shù),因?yàn)楹芏鄏pc框架客戶端都有一個(gè)opts參數(shù)作為結(jié)尾。
還有一個(gè)BindJSON()
,用于把請求體包裝為一個(gè)Req結(jié)構(gòu)體:
// 參數(shù)綁定 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()則可以使用此方法。
處理響應(yīng)
用于把rsp、error、errcode、errmsg等數(shù)據(jù)封裝為一個(gè)JSON格式響應(yīng)體,對應(yīng)ProcessRsp()、Success()、Failure()、FailureCodeMsg()函數(shù)。
比如ProcessRsp()
需要帶上rsp和error,這樣業(yè)務(wù)里面就不需要再寫如下模板代碼了:
// 處理簡單響應(yīng) func ProcessRsp(c *gin.Context, rsp any, err error) { if err != nil { Failure(c, err) return } Success(c, rsp) }
響應(yīng)格式統(tǒng)一為:
// 響應(yīng) 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()則可以使用這些方法。
處理錯(cuò)誤
一般我們都需要在出錯(cuò)時(shí)帶上一個(gè)業(yè)務(wù)錯(cuò)誤碼,方便客戶端處理。因此我們需要提供一個(gè)合適的error類型:
// 錯(cuò)誤 type Error struct { Code int `json:"code"` Msg string `json:"msg"` }
我們提供了一些函數(shù)方便使用Error
,對應(yīng)NewError()、ToError()、ErrCode()、ErrMsg()、ErrEqual()函數(shù)。
比如NewError()
生成一個(gè)Error類型error:
// 通過code和msg產(chǎn)生一個(gè)錯(cuò)誤 func NewError(code int, msg string) error { return &Error{ Code: code, Msg: msg, } }
請求上下文操作
Gin的請求是鏈?zhǔn)教幚淼模簿褪嵌鄠€(gè)handler順序的處理一個(gè)請求,比如:
reqFunc := func(c *gin.Context, req *UpdateUserInfoReq) { req.UID = ginrest.GetKey[int](c, KeyUserID) } // 認(rèn)證,綁定UID,處理 e.POST("/user/info/update", Verify, ginrest.Do(reqFunc, UpdateUserInfo))
這個(gè)接口經(jīng)歷了Verify和ginrest.Do兩個(gè)handler,其中我們在Verify的時(shí)候通過認(rèn)證知道了用戶的身份信息(比如uid),我們希望把這個(gè)uid存起來,這樣可以在業(yè)務(wù)邏輯里使用。
因此我們提供了SetKey()、GetKey()兩個(gè)函數(shù),用于存儲(chǔ)請求上下文:
比如認(rèn)證通過后我們可以設(shè)置UID到上下文,然后在reqFunc()里讀取設(shè)置到req里面(下面介紹)。
// 認(rèn)證 func Verify(c *gin.Context) { // 認(rèn)證處理 // ... // 忽略認(rèn)證的具體邏輯 ginrest.SetKey(c, KeyUserID, uid) }
請求結(jié)構(gòu)體處理
上面我們設(shè)置了請求上下文,比如UID,但是其實(shí)我們并不知道具體這個(gè)UID是需要設(shè)置到req里的哪個(gè)字段,因此我們提供了一個(gè)回調(diào)函數(shù)ReqFunc(),用于設(shè)置Req:
// 這里↓ reqFunc := func(c *gin.Context, req *UpdateUserInfoReq) { req.UID = ginrest.GetKey[int](c, KeyUserID) } // 認(rèn)證,綁定UID,處理 e.POST("/user/info/update", Verify, ginrest.Do(reqFunc, UpdateUserInfo))
注
如果這個(gè)庫的設(shè)計(jì)不符合具體的業(yè)務(wù),也可以按照這種思路去封裝一個(gè)類似的庫,只要盡可能的統(tǒng)一請求、響應(yīng)的格式,就可以減少很多重復(fù)的模板代碼。
原文鏈接:https://juejin.cn/post/7132790934353215502
相關(guān)推薦
- 2022-01-17 EF關(guān)于報(bào)錯(cuò)Self referencing loop detected with type的原因以
- 2023-12-13 html標(biāo)簽的屬性——disabled與readonly區(qū)別
- 2022-07-06 pandas實(shí)現(xiàn)一行拆分成多行_python
- 2022-02-28 Module not found: Error: Can't resolve 'sass-loade
- 2022-09-18 ASP.NET?Core實(shí)現(xiàn)文件上傳和下載_實(shí)用技巧
- 2022-08-01 MongoDB創(chuàng)建與刪除數(shù)據(jù)庫_MongoDB
- 2022-04-01 OpenCV實(shí)現(xiàn)簡單錄屏功能_C 語言
- 2023-01-08 Android消息機(jī)制原理深入分析_Android
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支