博客 / 詳情

返回

Gin 框架中的規範響應格式設計與實現

在現代Web應用開發中,統一和規範化的API響應格式對於前後端協作至關重要。今天,我們來探討如何在Gin框架中設計一套既實用又易於維護的響應格式規範。

為什麼需要統一的響應格式?

首先,讓我們思考一個問題:為什麼要統一API響應格式?

  1. 前後端協作效率:一致的響應格式讓前端開發者能以統一的方式處理服務端響應
  2. 錯誤處理簡化:標準化的錯誤碼和消息便於統一處理各種異常情況
  3. 接口文檔維護:規範化響應減少文檔編寫工作量
  4. 客户端適配:移動端或其他客户端可以複用相同的響應解析邏輯

設計統一的響應結構

讓我們從最基礎的響應結構開始。在本文的示例項目中,我採用瞭如下的響應結構:

type baseResponse struct {
    Code int    `json:"code"`
    Msg  string `json:"msg"`
    Data any    `json:"data"`
}

這個結構包含了三個基本元素:

  • Code: HTTP狀態碼或業務狀態碼,表示請求的執行結果
  • Msg: 人類可讀的消息,描述請求的執行狀態。(吐槽一下,見過各種項目,有的用"message", 有的用""messages", 調用方稍不留神就寫錯了,所以乾脆用縮寫)
  • Data: 實際的業務數據,根據不同接口返回不同內容

有的業務還需要返回timestamp或者trace_id之類的內容,可以根據實際需求來修改。

實現響應處理工具類

有了基礎結構後,我們可以構建一個響應處理工具類。在我的項目中,pkg/response/response.go 文件實現了多種常用的響應方法:

// Success 返回200狀態碼, 默認返回成功
func Success(c *gin.Context, data any, opts *ResponseOption) {
    if opts == nil {
        opts = &ResponseOption{
            Msg: "success",
        }
    }
    c.JSON(http.StatusOK, baseResponse{
        Code: http.StatusOK,
        Msg:  opts.Msg,
        Data: data,
    })
}

通過這種方式,我們可以針對不同的HTTP狀態碼提供專門的響應方法:

  • Success: 正常業務響應
  • BadRequest: 參數校驗失敗
  • Unauthorized: 權限校驗失敗
  • NotFound: 資源不存在
  • InternalServerError: 服務器內部錯誤

處理異常情況

僅僅處理正常的業務響應是不夠的,我們還需要統一攔截異常進行處理,否則異常和未註冊路由都不會返回我們需要的格式。這裏我用一個自定義的異常恢復中間件做異常捕獲:

func CustomRecovery() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                var brokenPipe bool
                // 檢測是否是連接中斷
                if ne, ok := err.(*net.OpError); ok {
                    if se, ok := ne.Err.(*os.SyscallError); ok {
                        if strings.Contains(strings.ToLower(se.Error()), "broken pipe") ||
                            strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
                            brokenPipe = true
                        }
                    }
                }

                // 獲取堆棧信息
                stack := string(debug.Stack())

                if brokenPipe {
                    c.Abort()
                    response.InternalServerError(c, nil, &response.ResponseOption{Msg: "network abort"})
                    return
                }

                slog.Error("exception catched", "error", err, "stack", stack)
                c.Abort()
                response.InternalServerError(c, nil, &response.ResponseOption{Msg: "server internal error"})
            }
        }()
        c.Next()
    }
}

這個中間件有幾個亮點:

  1. 連接中斷處理:特別處理了 "broken pipe" 和 "connection reset by peer" 錯誤,避免客户端提前斷開連接時產生冗餘錯誤日誌
  2. 錯誤信息記錄:記錄錯誤詳情和堆棧信息,便於問題排查
  3. 統一錯誤響應:所有異常都以統一格式返回給客户端

路由未找到處理

除了異常處理,我們還需要處理請求路由不存在的情況:

r.NoRoute(func(c *gin.Context) {
    response.NotFound(c, nil, &response.ResponseOption{
        Msg: "接口不存在",
    })
})

這樣,當用户請求不存在的接口時,也會收到格式統一的響應。

使用示例

在實際使用中,我們的控制器代碼變得簡潔明瞭:

r.GET("/a1", func(c *gin.Context) {
    response.Success(c, nil, nil)
})

r.GET("/a2", func(c *gin.Context) {
    var respData = struct {
        Name string
    }{
        Name: "hello",
    }
    response.Success(c, respData, &response.ResponseOption{
        Msg: "how a successful response",
    })
})

無論是在成功響應還是錯誤響應中,客户端收到的都是相同格式的JSON數據,極大地提升了開發體驗。

補充-完整代碼示例

項目結構:

├── go.mod
├── go.sum
├── main.go
└── pkg
    └── response
        └── response.go

響應類

  • pkg/response/response.go
package response

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

type baseResponse struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
	Data any    `json:"data"`
}

type ResponseOption struct {
	Msg string `json:"msg"`
}

// Success 返回200狀態碼, 默認返回成功
func Success(c *gin.Context, data any, opts *ResponseOption) {
	if opts == nil {
		opts = &ResponseOption{
			Msg: "success",
		}
	}
	c.JSON(http.StatusOK, baseResponse{
		Code: http.StatusOK,
		Msg:  opts.Msg,
		Data: data,
	})
}

// SuccessCreated 返回201狀態碼, 表示創建成功。常用於新增數據
func SuccessCreated(c *gin.Context, data any, opts *ResponseOption) {
	if opts == nil {
		opts = &ResponseOption{
			Msg: "success",
		}
	}
	c.JSON(http.StatusCreated, baseResponse{
		Code: http.StatusCreated,
		Msg:  opts.Msg,
		Data: data,
	})
}

// BadRequest 返回400錯誤, 常用於參數校驗失敗
func BadRequest(c *gin.Context, data any, opts *ResponseOption) {
	if opts == nil {
		opts = &ResponseOption{
			Msg: "bad request",
		}
	}
	c.JSON(http.StatusBadRequest, baseResponse{
		Code: http.StatusBadRequest,
		Msg:  opts.Msg,
		Data: data,
	})
}

// Unauthorized 401錯誤, 常用於權限校驗失敗
func Unauthorized(c *gin.Context, data any, opts *ResponseOption) {
	if opts == nil {
		opts = &ResponseOption{
			Msg: "unauthorized",
		}
	}
	c.JSON(http.StatusUnauthorized, baseResponse{
		Code: http.StatusUnauthorized,
		Msg:  opts.Msg,
		Data: data,
	})
}

// Forbidden 403錯誤, 常用於權限不足
func Forbidden(c *gin.Context, data any, opts *ResponseOption) {
	if opts == nil {
		opts = &ResponseOption{
			Msg: "forbidden",
		}
	}
	c.JSON(http.StatusForbidden, baseResponse{
		Code: http.StatusForbidden,
		Msg:  opts.Msg,
		Data: data,
	})
}

// NotFound 404錯誤, 常用於資源不存在
func NotFound(c *gin.Context, data any, opts *ResponseOption) {
	if opts == nil {
		opts = &ResponseOption{
			Msg: "not found",
		}
	}
	c.JSON(http.StatusNotFound, baseResponse{
		Code: http.StatusNotFound,
		Msg:  opts.Msg,
		Data: data,
	})
}

func MethodNotAllowed(c *gin.Context, data any, opts *ResponseOption) {
	if opts == nil {
		opts = &ResponseOption{
			Msg: "method not allowed",
		}
	}
	c.JSON(http.StatusMethodNotAllowed, baseResponse{
		Code: http.StatusMethodNotAllowed,
		Msg:  opts.Msg,
		Data: data,
	})
}

// UnprocessableEntity 422錯誤, 常用於客户端參數導致業務邏輯處理異常
func UnprocessableEntity(c *gin.Context, data any, opts *ResponseOption) {
	if opts == nil {
		opts = &ResponseOption{
			Msg: "unprocessable entity",
		}
	}
	c.JSON(http.StatusUnprocessableEntity, baseResponse{
		Code: http.StatusUnprocessableEntity,
		Msg:  opts.Msg,
		Data: data,
	})
}

// InternalServerError 500錯誤, 常用於服務器內部錯誤
func InternalServerError(c *gin.Context, data any, opts *ResponseOption) {
	if opts == nil {
		opts = &ResponseOption{
			Msg: "internal server error",
		}
	}
	c.JSON(http.StatusInternalServerError, baseResponse{
		Code: http.StatusInternalServerError,
		Msg:  opts.Msg,
		Data: data,
	})
}

程序入口

  • main.go
package main

import (
	"log/slog"
	"net"
	"net/http/httputil"
	"os"
	"runtime/debug"
	"strings"
	"tmpgo/pkg/response"

	"github.com/gin-gonic/gin"
)

func main() {
	gin.SetMode(gin.ReleaseMode) // 生產環境設為 ReleaseMode
	r := gin.New()  // 不要用 gin.Default()

	// 添加 Logger 和 Recovery 中間件
	r.Use(gin.Logger())
	r.Use(CustomRecovery()) // 使用自定義異常恢復中間件

	// 註冊路由
	r.GET("/a1", func(c *gin.Context) {
		response.Success(c, nil, nil)
	})
	r.GET("/a2", func(c *gin.Context) {
		var respData = struct {
			Name string
		}{
			Name: "hello",
		}
		response.Success(c, respData, &response.ResponseOption{
			Msg: "how a successful response",
		})
	})
	r.GET("/b1", func(c *gin.Context) {
		response.UnprocessableEntity(c, nil, nil)
	})
	r.GET("/b2", func(c *gin.Context) {
		panic("panic something")
	})

	// 設置自定義 404 處理
	r.NoRoute(func(c *gin.Context) {
		response.NotFound(c, nil, nil)
	})

	// 設置自定義 405 處理(方法不允許)
	r.NoMethod(func(c *gin.Context) {
		response.MethodNotAllowed(c, nil, nil)
	})

	r.Run("127.0.0.1:10000")
}

// 在正式項目中,可以統一放到中間件的模塊中
// CustomRecovery 自定義異常恢復中間件
func CustomRecovery() gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				var brokenPipe bool
				// 檢測是否是連接中斷
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") ||
							strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}

				// 獲取堆棧信息
				stack := string(debug.Stack())

				// 獲取原始請求內容(可選,方便排查是哪個參數導致的崩潰)
				httpRequest, _ := httputil.DumpRequest(c.Request, false)

				if brokenPipe {
					c.Abort()
					response.InternalServerError(c, nil, &response.ResponseOption{Msg: "network abort"})
					return
				}

				slog.Error("exception catched", "error", err, "stack", stack, "request", string(httpRequest))
				// c.AbortWithStatusJSON()
				c.Abort()
				response.InternalServerError(c, nil, &response.ResponseOption{Msg: "server internal error"})
			}
		}()
		c.Next()
	}
}

調用示例

$ curl -X GET http://127.0.0.1:10000/a1
{"code":200,"msg":"success","data":null}

$ curl http://127.0.0.1:10000/a1
{"code":200,"msg":"success","data":null}

$ curl http://127.0.0.1:10000/a2
{"code":200,"msg":"how a successful response","data":{"Name":"hello"}}

$ curl http://127.0.0.1:10000/a3
{"code":404,"msg":"not found","data":null}

$ curl http://127.0.0.1:10000/b1
{"code":422,"msg":"unprocessable entity","data":null}

$ curl http://127.0.0.1:10000/b2
{"code":500,"msg":"server internal error","data":null}
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.