?在gin框架中,我們可以將多種請求數據(json, form,uri,header等)直接綁定到我們定義的結構體,底層是通過反射方式獲取我們定義在結構體上面的tag來實現請求數據到我們的結構體數據的綁定的。 在gin的底層有2大體系的數據綁定一個是Bind,是個是ShouldBind, 下面我們就從數據綁定入口開始一層層的解開gin數據綁定的神秘面紗!
gin中支持的數據綁定類型
? ? ? ? gin框架中的所有數據的綁定都是通過請求類型的 Content-Type這個 MIME類型來完成的,他所支持的類型如下:
// Content-Type MIME of the most common data formats.
const (MIMEJSON = "application/json"MIMEHTML = "text/html"MIMEXML = "application/xml"MIMEXML2 = "text/xml"MIMEPlain = "text/plain"MIMEPOSTForm = "application/x-www-form-urlencoded"MIMEMultipartPOSTForm = "multipart/form-data"MIMEPROTOBUF = "application/x-protobuf"MIMEMSGPACK = "application/x-msgpack"MIMEMSGPACK2 = "application/msgpack"MIMEYAML = "application/x-yaml"MIMEYAML2 = "application/yaml"MIMETOML = "application/toml"
)
我們在Bind和ShouldBind? 2大序列?中是使用的XXX 定義
// These implement the Binding interface and can be used to bind the data
// present in the request to struct instances.
var (JSON BindingBody = jsonBinding{}XML BindingBody = xmlBinding{}Form Binding = formBinding{}Query Binding = queryBinding{}FormPost Binding = formPostBinding{}FormMultipart Binding = formMultipartBinding{}ProtoBuf BindingBody = protobufBinding{}MsgPack BindingBody = msgpackBinding{}YAML BindingBody = yamlBinding{}Uri BindingUri = uriBinding{}Header Binding = headerBinding{}TOML BindingBody = tomlBinding{}
)
上面這些就是gin框架中支持的數據的綁定類型XXX定義, 如 BindJSON,? BindForm,? ShouldBindJSON,? ? ShouldBinxUri? 等。
gin框架中的2大類型的數據綁定方式
????????他們實現的功能是一樣的,區別在于Bind序列如果數據綁定失敗會直接拋異常并退出當前請求,而ShouldBind 則不會中斷當前的請求。?原因是 Bind序列使用的是?c.MustBindWith ,注意這里的名字前綴 Must? , 在go語言的開發中我們通常的做法就是帶這個Must的方法,就表示必須要滿足的方法, 如果不滿足就直接給你個?panic 異常(直接退出當前請求),gin框架也不另外,MustXxx?的方法也是必須要滿足的,否則panic中斷當前請求; 而ShouldBind序列是通過? c.ShouldBindWith 來實現的,他在數據綁定異常時會忽略異常,繼續后的的請求。
1. ?Bind? 序列 示例
他由Bind方法,和 BindXXX 方法主從,他們內部都是調用了c.MustBindWith 方法,這里的XXX 即gin中支持的數據綁定類型,見 gin中支持的數據綁定類型定義?
func (c *Context) Bind(obj any) error {b := binding.Default(c.Request.Method, c.ContentType())return c.MustBindWith(obj, b)
}// BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON).
func (c *Context) BindJSON(obj any) error {return c.MustBindWith(obj, binding.JSON)
}
// ....
2.? ShouldBind序列 示例
func (c *Context) ShouldBind(obj any) error {b := binding.Default(c.Request.Method, c.ContentType())return c.ShouldBindWith(obj, b)
}// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON).
func (c *Context) ShouldBindJSON(obj any) error {return c.ShouldBindWith(obj, binding.JSON)
}
// ......
gin 中數據綁定接口定義
????????不管是那個序列的數據綁定,他們都是通過實現以下接口來完成具體的數據綁定的,這個也是go語言的一個核心思想 -- 面向接口編程 !? 你沒有看錯就是面向接口編程,而你常見其他語言,如java 等好像都是說的面向對象編程,而go語言的特別就在于此, go語言中把面向接口編程做到了極致!
? ? ? ? gin框架中為數據綁定定義了3個接口來實現不同類型的數據綁定。?
// Binding describes the interface which needs to be implemented for binding the
// data present in the request such as JSON request body, query parameters or
// the form POST.
type Binding interface {Name() stringBind(*http.Request, any) error
}// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
// but it reads the body from supplied bytes instead of req.Body.
type BindingBody interface {BindingBindBody([]byte, any) error
}// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
// but it reads the Params.
type BindingUri interface {Name() stringBindUri(map[string][]string, any) error
}
gin中的數據綁定實現
? ? ? ? gin框架中已經給我們實現了多種常見的數據類型的綁定,見?gin中支持的數據綁定類型 。 當然, 如果已有實現中沒有你想要的數據類型的綁定或者你想自己動手來實現, 這個也非常簡單, 你只要實現上面定義的對應的接口即可! 不知道怎么實現的話你就參考一下gin中已有的實現,哈哈!
gin框架數據綁定實現截圖
form數據綁定實現示例
????????這里的數據實現比較多, 我們就以 我們最常用的form數據綁定實現為例,和大家一起來學習一下gin中的數據綁定是如何實現的。
1.? 數據綁定入口
????????下面的formBinding 綁定是普通form的綁定, 另外還有formPostBinding? POST類型的數據綁定,?formMultipartBinding 這個是針對媒體上傳類型的數據的綁定實現,我們就不一一列舉了,他們的實現思路都差不多。
func (formBinding) Bind(req *http.Request, obj any) error {if err := req.ParseForm(); err != nil {return err}if err := req.ParseMultipartForm(defaultMemory); err != nil && !errors.Is(err, http.ErrNotMultipart) {return err}if err := mapForm(obj, req.Form); err != nil {return err}return validate(obj)
}
2.? 請求form數據解析?req.ParseForm()?
// ParseForm populates r.Form and r.PostForm.
//
// For all requests, ParseForm parses the raw query from the URL and updates
// r.Form.
//
// For POST, PUT, and PATCH requests, it also reads the request body, parses it
// as a form and puts the results into both r.PostForm and r.Form. Request body
// parameters take precedence over URL query string values in r.Form.
//
// If the request Body's size has not already been limited by [MaxBytesReader],
// the size is capped at 10MB.
//
// For other HTTP methods, or when the Content-Type is not
// application/x-www-form-urlencoded, the request Body is not read, and
// r.PostForm is initialized to a non-nil, empty value.
//
// [Request.ParseMultipartForm] calls ParseForm automatically.
// ParseForm is idempotent.
func (r *Request) ParseForm() error {var err errorif r.PostForm == nil {if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {r.PostForm, err = parsePostForm(r)}if r.PostForm == nil {r.PostForm = make(url.Values)}}if r.Form == nil {if len(r.PostForm) > 0 {r.Form = make(url.Values)copyValues(r.Form, r.PostForm)}var newValues url.Valuesif r.URL != nil {var e errornewValues, e = url.ParseQuery(r.URL.RawQuery)if err == nil {err = e}}if newValues == nil {newValues = make(url.Values)}if r.Form == nil {r.Form = newValues} else {copyValues(r.Form, newValues)}}return err
}
3. 上傳類型數據解析?req.ParseMultipartForm
// ParseMultipartForm parses a request body as multipart/form-data.
// The whole request body is parsed and up to a total of maxMemory bytes of
// its file parts are stored in memory, with the remainder stored on
// disk in temporary files.
// ParseMultipartForm calls [Request.ParseForm] if necessary.
// If ParseForm returns an error, ParseMultipartForm returns it but also
// continues parsing the request body.
// After one call to ParseMultipartForm, subsequent calls have no effect.
func (r *Request) ParseMultipartForm(maxMemory int64) error {if r.MultipartForm == multipartByReader {return errors.New("http: multipart handled by MultipartReader")}var parseFormErr errorif r.Form == nil {// Let errors in ParseForm fall through, and just// return it at the end.parseFormErr = r.ParseForm()}if r.MultipartForm != nil {return nil}mr, err := r.multipartReader(false)if err != nil {return err}f, err := mr.ReadForm(maxMemory)if err != nil {return err}if r.PostForm == nil {r.PostForm = make(url.Values)}for k, v := range f.Value {r.Form[k] = append(r.Form[k], v...)// r.PostForm should also be populated. See Issue 9305.r.PostForm[k] = append(r.PostForm[k], v...)}r.MultipartForm = freturn parseFormErr
}
4. 數據映射 函數?mapForm ,?mapFormByTag
? ?注意這里是一個函數,上面2個ParseForm 和?ParseMultipartForm 都是在請求對象上面的方法。
從下面的代碼可見, 他這里調用的是mapFormByTag 這個函數,這個即是根據我們定義在結構體中的Tag來映射數據, 這里因為是form類型的數據綁定,所以這個地方的第三個參數就是 form
func mapForm(ptr any, form map[string][]string) error {return mapFormByTag(ptr, form, "form")
}
我們接著看看這個mapFormByTag
這里的ptr就是我們要將數據綁定到的我們自定義的結構體對象的指針, form 這個就是上面解析后的請求表單的數據map,? ?第三個參數 tag 這個就是我們要解析的數據類型的Tag定義名稱,這里就的?form 就表示他解析的數據就是我們的結構體TAG中的名稱為form的Tag數據, 如我們結構體中的字段Page的定義??Page int `json:"page" form:"page" `??, 這里的tag名稱就是form,而對于的字段名稱就是 page, 就表示他可以綁定請求參數page的值到 結構體的 Page 字段。
func mapFormByTag(ptr any, form map[string][]string, tag string) error {// Check if ptr is a mapptrVal := reflect.ValueOf(ptr)var pointed anyif ptrVal.Kind() == reflect.Ptr {ptrVal = ptrVal.Elem()pointed = ptrVal.Interface()}if ptrVal.Kind() == reflect.Map &&ptrVal.Type().Key().Kind() == reflect.String {if pointed != nil {ptr = pointed}return setFormMap(ptr, form)}return mappingByPtr(ptr, formSource(form), tag)
}
PS: 這里有一個很容易忽略但又非常重要的知識點,就是Elem這個方法的應用時機。 當我們在對一個對象應用函數 reflect.ValueOf() 獲取對于的 reflect.Value 對象后, 如果any類型的入參 ptr是一個指針,則獲取到的Value對象就必須要調用 .Elem()方法獲取指針對應的具體的數據的 reflect.Value后再進行操作,否則就獲取不到你想要的數據,因為你拜佛沒有找對廟門,哈哈!
5. mappingByPtr函數
這個就是具體的數據綁定映射函數的實現邏輯了, 他這個里面用了遞歸方式來處理數據的映射,另外還使用了一個 setter 數據設置接口來進行數據的設置。?
從下面的代碼我們可以看到,在mapping的第一行就繼續了一個tag名稱的判斷,如果名稱是 - 就直接返回 忽略了這個字段的映射處理。? 如我們結構體中某個字段的tag 名稱是是這樣定義的? Name string `form:"-" `? 這個就表示會忽略Name這個字段的form數據的綁定
func mappingByPtr(ptr any, setter setter, tag string) error {_, err := mapping(reflect.ValueOf(ptr), emptyField, setter, tag)return err
}func mapping(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {if field.Tag.Get(tag) == "-" { // just ignoring this fieldreturn false, nil}vKind := value.Kind()if vKind == reflect.Ptr {var isNew boolvPtr := valueif value.IsNil() {isNew = truevPtr = reflect.New(value.Type().Elem())}isSet, err := mapping(vPtr.Elem(), field, setter, tag)if err != nil {return false, err}if isNew && isSet {value.Set(vPtr)}return isSet, nil}if vKind != reflect.Struct || !field.Anonymous {ok, err := tryToSetValue(value, field, setter, tag)if err != nil {return false, err}if ok {return true, nil}}if vKind == reflect.Struct {tValue := value.Type()var isSet boolfor i := 0; i < value.NumField(); i++ {sf := tValue.Field(i)if sf.PkgPath != "" && !sf.Anonymous { // unexportedcontinue}ok, err := mapping(value.Field(i), sf, setter, tag)if err != nil {return false, err}isSet = isSet || ok}return isSet, nil}return false, nil
}
6 setter數據設置接口定義
這個接口就定義了一個方法, TrySet 嘗試幫我們設置數據
// setter tries to set value on a walking by fields of a struct
type setter interface {TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSet bool, err error)
}
7. 數據設置函數?tryToSetValue?
這里就是數據映射過程中的字段Tag值的獲取核心函數。 通過下面的代碼我們可以找到gin的數據綁定的Tag中的數據是如何處理的。? ? 詳見下面的代碼注釋
func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {var tagValue stringvar setOpt setOptions// 通過反射獲取結構體字段tag對應的數據,tagValue = field.Tag.Get(tag)//將獲取到的tag數據再使用逗號分隔?tagValue, opts := head(tagValue, ",")if tagValue == "" { // default value is FieldNametagValue = field.Name}if tagValue == "" { // when field is "emptyField" variablereturn false, nil}var opt stringfor len(opts) > 0 {opt, opts = head(opts, ",")// 如果獲取到的tag值中包含了 default=xx 則對這個字段設置默認值if k, v := head(opt, "="); k == "default" {setOpt.isDefaultExists = truesetOpt.defaultValue = v}}return setter.TrySet(value, field, tagValue, setOpt)
}
? ? ? ? 根據上面的代碼 舉例說明:? field.Tag.Get(tag)? 這個就是獲取我們在結構體中設置的tag對應的值, 如 假設tag為form, 我們有一個結構體中的字段定義是? Page int `json:"page" form:"page,default=1" `? ? ?這里的代碼field.Tag.Get(tag)? 獲取到的內容就是?page,default=1
tagValue, opts := head(tagValue, ",") 這個獲取到的是tagValue就是?page, opts的值就是default=1
這個定義的意思就是 將請求表單中的 page 對應的字段幫我們綁定到我們定義的這個結構體的 Page字段上面,如果請求表單中沒有相關的數據則使用這里定義的默認值1(default=1就是定義默認值), 這個地方就是如何給綁定數據設置默認值的方式, 這個知識點gin官方文檔和示例可沒有哦!! 這個就是通過這里的源碼發現的使用方法。
form的setter接口執行?TrySet
// TrySet tries to set a value by request's form source (like map[string][]string)
func (form formSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSet bool, err error) {return setByForm(value, field, form, tagValue, opt)
}
?form數據設置函數?setByForm
通過下面的代碼,可見他可以設置的數據類型有 切片, 數組,還有可序列化的數據(默認),這個可序列化的數據類型就包含所有的可以被序列化的數據。
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {vs, ok := form[tagValue]if !ok && !opt.isDefaultExists {return false, nil}switch value.Kind() {case reflect.Slice:if !ok {vs = []string{opt.defaultValue}}return true, setSlice(vs, value, field)case reflect.Array:if !ok {vs = []string{opt.defaultValue}}if len(vs) != value.Len() {return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())}return true, setArray(vs, value, field)default:var val stringif !ok {val = opt.defaultValue}if len(vs) > 0 {val = vs[0]}if ok, err := trySetCustom(val, value); ok {return ok, err}return true, setWithProperType(val, value, field)}
}
可序列化的數據 設置
// trySetCustom tries to set a custom type value
// If the value implements the BindUnmarshaler interface, it will be used to set the value, we will return `true`
// to skip the default value setting.
func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {switch v := value.Addr().Interface().(type) {case BindUnmarshaler:return true, v.UnmarshalParam(val)}return false, nil
}
?這個就是可序列化的數據的設置的具體邏輯,這里也是根據反射方式先獲取要設置的結構體的字段的類型,然后根據不同的類型來設置具體的值。? 細心的你應該能夠注意到,我們上面提到的小知識點 指針類型的數據需要先調用 .Elem()方法 的應用,見下面的?case reflect.Ptr:??
func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {switch value.Kind() {case reflect.Int:return setIntField(val, 0, value)case reflect.Int8:return setIntField(val, 8, value)case reflect.Int16:return setIntField(val, 16, value)case reflect.Int32:return setIntField(val, 32, value)case reflect.Int64:switch value.Interface().(type) {case time.Duration:return setTimeDuration(val, value)}return setIntField(val, 64, value)case reflect.Uint:return setUintField(val, 0, value)case reflect.Uint8:return setUintField(val, 8, value)case reflect.Uint16:return setUintField(val, 16, value)case reflect.Uint32:return setUintField(val, 32, value)case reflect.Uint64:return setUintField(val, 64, value)case reflect.Bool:return setBoolField(val, value)case reflect.Float32:return setFloatField(val, 32, value)case reflect.Float64:return setFloatField(val, 64, value)case reflect.String:value.SetString(val)case reflect.Struct:switch value.Interface().(type) {case time.Time:return setTimeField(val, field, value)case multipart.FileHeader:return nil}return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())case reflect.Map:return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())case reflect.Ptr:if !value.Elem().IsValid() {value.Set(reflect.New(value.Type().Elem()))}return setWithProperType(val, value.Elem(), field)default:return errUnknownType}return nil
}
ok, 至此,gin框架中的數據請求綁定源碼都扒完了...? ?后面就是如何使用了,當你了解了他的原理后使用那就是小菜一碟了, 本文就不做討論了。。。。。。
如果本文對你有幫助,歡迎點贊,收藏,評論, 你的支持就是我們繼續產出優質內容的動力哦 :)?