Gin
框架处理前端请求的时候,使用 ShouldBindXXX
绑定参数/数据到结构体上是一种比较常用的取数据手段,但在一些情况下,可能会出现问题
例如,现在有一个 /user/update
接口,用于更新用户的 年龄
和 昵称
,即接收两个字段:age(int)
、nick_name(string)
,并且这两个字段并不要求必须同时传递,可以两个都传,也可以只传其中一个,后端从请求中解析这两个参数,取到哪个字段就对哪个字段进行更新
typeUserstruct{Ageint`json:"age"`NickNamestring`json:"nick_name"`}
两个字段都传递那还好说,但如果只传其中一个字段,并且后端用 ShouldBindXXX
来绑定数据到结构体,就可能会出现问题了
funcHandlerUpdate(c*gin.Context){varuserUseriferr:=c.ShouldBind(&user);err!=nil{//...}}
如果前端只传了一个 nick_name
字段,没传 age
字段,那么user.Age
的值就是零值,即 0
,ShouldBindXXX
并不判断这个 0
到底是零值还是前端真的传了 0
这个问题解决起来倒也简单,两个方法
一是将结构体内的字段改成指针类型
typeUserstruct{Age*int`json:"age"`NickName*string`json:"nick_name"`}
指针的零值 nil
,ShouldBindXXX
之后,字段值为 nil
的自然就是没传值的
但将结构体所有的字段都定义为指针类型未免有些不符合习惯,并且操作指针也不方便,也更容易出错(例如空指针问题)
第二个是办法是借助 map
ShouldBindXXX
有问题的话那我大不了不用了,直接将参数(GET
)/数据(POST
)映射到 map
就行
但这样的话就会引出另外一个问题,ShouldBindXXX
方法一个显著的好处是可以根据结构体里定义的 tag
规则来对字段进行校验,如果你直接读到 map
中就要自己实现字段校验逻辑了,字段少点还好,要是多了得写一大串的 if...else
或者是干脆要实现一个通用校验方法了,未免繁琐
所以想到用 ShouldBindXXX
来做校验,再借助 map
用于区分零值,即对请求传递的数据读了两次
以 GET
请求为例:
funcHandlerUpdate(c*gin.Context){varuserUser//用ShouldBind作校验iferr:=c.ShouldBind(&user);err!=nil{fmt.Printf("genGetMapShouldBinderror:%v\n",err)return}//请求真正传递的参数映射到map中allMap:=map[string]interface{}{}urlvalues:=c.Request.URL.Query()fork,urls:=rangeurlvalues{fmt.Printf("\ngenGetMapk,urls,%v,%v\n",k,urls)//重复值则取最后一个allMap[k]=urls[len(urls)-1]}}
截至目前,只是校验并获取到了请求的数据,下一步还要进行更新数据库的操作,这里以 gorm
为例
因为 user
只是用于校验请求的数据是否合法,无法判断零值,所以不能直接以 user
为基础操作数据库
//可能因零值问题导致出现不符合预期的结果db.Save(&user)
allMap
可以分辨出请求到底携带了哪些参数/数据,但可能存在一些额外不需要的数据,例如当希望更新用户的 age
和 nick_name
属性的时候,操作的数据表是 db_user
,而这个数据表中除了 age
、nick_name
两列外,还存在用于标识用户是否注销了的 is_del
列,那么按照如下更新方式也是会出问题的:
db.Model(&user).Updates(allMap)
如果 allMap
中存在 is_del
属性,那么也会更新数据表中的 is_del
字段,并不是预期的结果,所以需要将 allMap
中不需要的属性去掉,可以复制出一份只包含所需更新属性的 map
,也可以直接删除掉 allMap
上额外的属性只保留所需的,这里以前一种为例
allMap:=make(map[string]interface{})realMap:=make(map[string]interface{})ifv,ok:=allMap["age"];ok{realMap["age"]=v}ifv,ok:=allMap["nick_name"];ok{realMap["nick_name"]=v}db.Model(&user).Updates(realMap)
这里只有 age
、nick_name
两个字段所以还好,但如果所需更新的字段最多在 5
个以上就要写最多 5
个条件语句了,未免繁琐,可以借助 reflect
处理,无论存在多少个需要更新的字段,代码量都是一样的
realMap:=make(map[string]interface{})typ:=reflect.TypeOf(user).Elem()fori:=0;i<typ.NumField();i++{tagName:=typ.Field(i).Tag.Get("json")ifv,isOK:=allMap[tagName];isOK{realMap[tagName]=v}}db.Model(&user).Updates(realMap)
完整代码:
//将请求的参数映射到m中,如果是GET,返回query参数组成的map;如果是POST,返回请求体里的数据////instance:指向具体结构体实例的指针,作用是获取结构体中每个字段名为`json`的tag,以映射mapfuncGenMapByStruct(c*gin.Context,instanceinterface{},m*map[string]interface{})error{ifc.ContentType()!=gin.MIMEJSON{returnerrors.New("content-typemustbe"+gin.MIMEJSON)}ifc.Request.Method!=http.MethodGet&&c.Request.Method!=http.MethodPost{returnerrors.New("methodmustbeGETorPOST")}allMap:=map[string]interface{}{}ifc.Request.Method==http.MethodGet{iferr:=genGetMap(c,instance,&allMap);err!=nil{returnerr}}else{iferr:=genPostMap(c,instance,&allMap);err!=nil{returnerr}}typ:=reflect.TypeOf(instance).Elem()fori:=0;i<typ.NumField();i++{tagName:=typ.Field(i).Tag.Get("json")ifv,isOK:=allMap[tagName];isOK{(*m)[tagName]=v}}returnnil}//从get请求中获取query,并将query处理成map映射到allMap中funcgenGetMap(c*gin.Context,instanceinterface{},allMap*map[string]interface{})error{iferr:=c.ShouldBind(instance);err!=nil{fmt.Printf("genGetMapShouldBinderror:%v\n",err)returnerr}urlvalues:=c.Request.URL.Query()fork,urls:=rangeurlvalues{fmt.Printf("\ngenGetMapk,urls,%v,%v\n",k,urls)//重复值则取最后一个(*allMap)[k]=urls[len(urls)-1]}returnnil}//从post请求中获取body,并将body反序列化到allMap中funcgenPostMap(c*gin.Context,instanceinterface{},allMap*map[string]interface{})error{//shouldBind会导致body无法再次读取,方便起见这里使用了ShouldBindBodyWithiferr:=c.ShouldBindBodyWith(instance,binding.JSON);err!=nil{fmt.Printf("genPostMapShouldBinderror:%v\n",err)returnerr}body,_:=c.Get(gin.BodyBytesKey)varbodyByte[]bytevarokboolifbodyByte,ok=body.([]byte);!ok{returnerrors.New("bodyisinvalid")}iflen(bodyByte)==0{returnnil}iferr:=json.Unmarshal(bodyByte,allMap);err!=nil{returnerr}returnnil}
使用示例:
typeUserstruct{Ageint`json:"age"`NickNamestring`json:"nick_name"`}r.Any("/update",func(c*gin.Context){m:=map[string]interface{}{}varuserUseriferr:=GenMapByStruct(c,&s,&m);err!=nil{c.JSON(http.StatusOK,gin.H{"code":-1,"message":err.Error()})return}c.JSON(http.StatusOK,gin.H{"code":0,"data":&m})})
可以看到,因为存在更多的计算过程,所以处理请求零值的情况,会带来更高的资源消耗,所以应该尽可能避免这种情况的出现,相比于在后端额外处理,让客户端携带完整的所需参数才是更优解。