Go?Ginrest實現(xiàn)一個RESTful接口
背景
基于現(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庫就是為了消除這些模板代碼,它不是一個復(fù)雜的框架,只是一個簡單的庫,輔助處理這些重復(fù)的事情,為了實現(xiàn)這個能力使用了Go1.18的泛型。
特性
這個庫提供以下特性:
- 封裝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ù)字錯誤碼格式:[0, 4XXXX, 5XXXX]
- 默認(rèn)使用標(biāo)準(zhǔn)錯誤格式:Error{code, msg}
- 默認(rèn)統(tǒng)一狀態(tài)碼[200, 400, 500]
- 提供Recovery中間件,統(tǒng)一panic時的響應(yīng)格式
- 提供SetKey()、GetKey()方法,用于存儲請求上下文(泛型)
- 提供ReqFunc(),用于設(shè)置Req(泛型)
使用例子
示例代碼在:github.com/jiaxwu/ginr…
首先我們實現(xiàn)兩個簡單的服務(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()里面每個接口都只需要一行代碼!
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)
} // 這里拆多一步是為了顯示第一個參數(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)
}
運行上面代碼,然后嘗試訪問接口,可以看到返回結(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": {}
}
實現(xiàn)原理
Do()和DoOpt()都會轉(zhuǎn)發(fā)到do(),它其實是一個模板函數(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
}
// 進一步處理請求結(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)
}
}
功能列表
處理請求
用于把一個標(biāo)準(zhǔn)服務(wù)封裝為一個RESTfulgin.HandlerFunc,對應(yīng)Do()、DoOpt()函數(shù)。
DoOpt()相比于Do()多了一個opts參數(shù),因為很多rpc框架客戶端都有一個opts參數(shù)作為結(jié)尾。
還有一個BindJSON(),用于把請求體包裝為一個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ù)封裝為一個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()則可以使用這些方法。
處理錯誤
一般我們都需要在出錯時帶上一個業(yè)務(wù)錯誤碼,方便客戶端處理。因此我們需要提供一個合適的error類型:
// 錯誤
type Error struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
我們提供了一些函數(shù)方便使用Error,對應(yīng)NewError()、ToError()、ErrCode()、ErrMsg()、ErrEqual()函數(shù)。
比如NewError()生成一個Error類型error:
// 通過code和msg產(chǎn)生一個錯誤
func NewError(code int, msg string) error {
return &Error{
Code: code,
Msg: msg,
}
}
請求上下文操作
Gin的請求是鏈?zhǔn)教幚淼?,也就是多個handler順序的處理一個請求,比如:
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))
這個接口經(jīng)歷了Verify和ginrest.Do兩個handler,其中我們在Verify的時候通過認(rèn)證知道了用戶的身份信息(比如uid),我們希望把這個uid存起來,這樣可以在業(yè)務(wù)邏輯里使用。
因此我們提供了SetKey()、GetKey()兩個函數(shù),用于存儲請求上下文:
比如認(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,但是其實我們并不知道具體這個UID是需要設(shè)置到req里的哪個字段,因此我們提供了一個回調(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))
注
如果這個庫的設(shè)計不符合具體的業(yè)務(wù),也可以按照這種思路去封裝一個類似的庫,只要盡可能的統(tǒng)一請求、響應(yīng)的格式,就可以減少很多重復(fù)的模板代碼。
以上就是Go Ginrest實現(xiàn)一個RESTful接口的詳細(xì)內(nèi)容,更多關(guān)于Go Ginrest實現(xiàn)RESTful接口的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
gorm update傳入struct對象,零值字段不更新的解決方案
這篇文章主要介紹了gorm update傳入struct對象,零值字段不更新的解決方案,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04
golang字符串拼接實現(xiàn)方式和區(qū)別對比
本文介紹了Go語言中字符串拼接的多種方法及其優(yōu)缺點,推薦使用strings.Builder進行頻繁拼接以優(yōu)化內(nèi)存分配和性能,同時,還討論了通過sync.Pool優(yōu)化高頻創(chuàng)建的對象,以減少垃圾回收壓力,感興趣的朋友一起看看吧2025-02-02
go?singleflight緩存雪崩源碼分析與應(yīng)用
這篇文章主要為大家介紹了go?singleflight緩存雪崩源碼分析與應(yīng)用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-09-09
golang利用redis和gin實現(xiàn)保存登錄狀態(tài)校驗登錄功能
這篇文章主要介紹了golang利用redis和gin實現(xiàn)保存登錄狀態(tài)校驗登錄功能,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2024-01-01

