在現代Web應用開發中,統一和規範化的API響應格式對於前後端協作至關重要。今天,我們來探討如何在Gin框架中設計一套既實用又易於維護的響應格式規範。
為什麼需要統一的響應格式?
首先,讓我們思考一個問題:為什麼要統一API響應格式?
- 前後端協作效率:一致的響應格式讓前端開發者能以統一的方式處理服務端響應
- 錯誤處理簡化:標準化的錯誤碼和消息便於統一處理各種異常情況
- 接口文檔維護:規範化響應減少文檔編寫工作量
- 客户端適配:移動端或其他客户端可以複用相同的響應解析邏輯
設計統一的響應結構
讓我們從最基礎的響應結構開始。在本文的示例項目中,我採用瞭如下的響應結構:
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()
}
}
這個中間件有幾個亮點:
- 連接中斷處理:特別處理了 "broken pipe" 和 "connection reset by peer" 錯誤,避免客户端提前斷開連接時產生冗餘錯誤日誌
- 錯誤信息記錄:記錄錯誤詳情和堆棧信息,便於問題排查
- 統一錯誤響應:所有異常都以統一格式返回給客户端
路由未找到處理
除了異常處理,我們還需要處理請求路由不存在的情況:
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}