gin 原始碼閱讀(5) - 靈活的返回值處理

haohongfan發表於2021-10-20

gin 原始碼閱讀系列文章列表:

hi,大家好,我是 haohongfan。

上一篇文章是關於如何快速解析客戶端傳遞過來的引數的,引數解析出來後就開始了我們的業務的開發流程了。

業務處理的過程 gin 並沒有給出對應的設計,這給業務開發帶來了很多不方便的地方,很多公司會基於 gin 做二次開發,定製契合公司基礎技術建設的框架升級,關於 gin 定製框架的內容這裡不再詳細展開,請關注後續文章。

經過業務邏輯框架的處理,已經有了對應的處理結果了,需要結果返回給客戶端了,本篇文章主要介紹 gin 是如何處理響應結果的。

仍然以原生的 net/http 簡單的例子開始我們的原始碼分析。

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello World"))
    })

    if err := http.ListenAndServe(":8000", nil); err != nil {
        fmt.Println("start http server fail:", err)
    }
}

output:

▶ curl -i -XGET 127.0.0.1:8000
HTTP/1.1 200 OK
Date: Sun, 10 Oct 2021 10:28:15 GMT
Content-Length: 11
Content-Type: text/plain; charset=utf-8

Hello World

可以看到呼叫 http.ResponseWriter.Write 即可將響應結果返回給客戶端。不過也可以看出一些問題:

  • 這個函式返回的值是預設的 text/plain 型別。如果想返回 application/json 就需要呼叫額外的設定 header 相關函式。
  • 這個函式只能接受 []byte 型別變數。一般情況下,我們經過業務邏輯處理過的資料都是結構體型別的,要使用 Write,需要把結構體轉換 []byte,這個就太不方便。

類似 gin 提供的引數處理,gin 同樣提供了很多格式的返回值,能讓我們簡化返回資料的處理。

下面是 gin 提供的 echo server,無需任何處理,就能返回一個 json 型別的返回值。

package main

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

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run()
}

output:

▶ curl -i -XGET 127.0.0.1:8080/ping   
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 10 Oct 2021 05:40:21 GMT
Content-Length: 18

{"message":"pong"}

當然 gin 還提其他型別格式的返回值,如 xml, yaml, protobuf 等。

var (
	_ Render     = JSON{}
	_ Render     = IndentedJSON{}
	_ Render     = SecureJSON{}
	_ Render     = JsonpJSON{}
	_ Render     = XML{}
	_ Render     = String{}
	_ Render     = Redirect{}
	_ Render     = Data{}
	_ Render     = HTML{}
	_ HTMLRender = HTMLDebug{}
	_ HTMLRender = HTMLProduction{}
	_ Render     = YAML{}
	_ Render     = Reader{}
	_ Render     = AsciiJSON{}
	_ Render     = ProtoBuf{}
)

本文僅以比較常見的 json 型別格式的返回值闡述 gin 對 ResponseWriter 的實現原理。

原始碼分析

1. 設定 json 的返回格式

// gin/context.go:L956
func (c *Context) JSON(code int, obj interface{}) {
	  c.Render(code, render.JSON{Data: obj})
}

初始化 render.JSON 型別變數

2. 通過 interface 動態轉發呼叫真正的 json 處理函式

// gin/context.go:L904
func (c *Context) Render(code int, r render.Render) {
    c.Status(code)

    if !bodyAllowedForStatus(code) {
        r.WriteContentType(c.Writer)
        c.Writer.WriteHeaderNow()
        return
    }

    if err := r.Render(c.Writer); err != nil {
        panic(err)
    }
}
  • 設定 Http status
  • 處理 Http status 為 100 - 199、204、304 的情況
  • 呼叫真正的 json 處理函式

3. 組裝 response 資料

// gin/render/json.go:L67
func WriteJSON(w http.ResponseWriter, obj interface{}) error {
    writeContentType(w, jsonContentType)
    jsonBytes, err := json.Marshal(obj)
    if err != nil {
        return err
    }
    _, err = w.Write(jsonBytes)
    return err
}
  • 設定 response Header 的 Content-Type 為 application/json; charset=utf-8
  • 將要返回資料編碼成 json 字串
  • 寫入 gin.responseWriter

4. 寫入真正的 http.ResponseWriter

// gin/response_writer.go:L76
func (w *responseWriter) Write(data []byte) (n int, err error) {
    w.WriteHeaderNow()
    n, err = w.ResponseWriter.Write(data)
    w.size += n
    return
}

這裡 gin 實現了 ResponseWriter interface,對原生的 response 做了一定的擴充套件,不過最終依然是呼叫 net/http 的 response.Write 完成對請求資料的最終寫入。

總結

本篇文章主要介紹了 gin 是如何完成對資料的組裝然後返回給客戶端的。寫到這裡基本上 gin 的整個流程就梳理完成了。gin 提供的功能就這麼多,第一篇原始碼分析文章我提到 gin 是個 httprouter 基本就是這個原因。

寫文章不易請大家幫忙點選 在看,點贊,分享。

相關文章