The Gin framework uses ShouldBindXXX to bind parameters/data to the structure when handling front-end requests, which is a common way to fetch data, but can cause problems in some cases

For example, there is now a /user/update interface for updating a user’s age and nickname, which accepts two fields: Age (int), nick_name(string), and these two fields do not need to be passed at the same time, you can pass both or only one of the two parameters, the back end from the request to parse these two parameters, which field is updated

type User struct {
	Age      int    `json:"age"`
	NickName string `json:"nick_name"`
}
Copy the code

Passing both fields is fine, but if you pass only one of them and the back end uses ShouldBindXXX to bind data to the structure, you might get a problem

func HandlerUpdate(c *gin.Context) {
	var user User
	iferr := c.ShouldBind(&user); err ! =nil {
		// ...}}Copy the code

If nick_name is passed and age is not passed, then user.Age is 0, ShouldBindXXX does not know whether this 0 is zero or if it is actually 0

This problem is easy to solve, two ways

One is to change a field in a structure body to a pointer type

type User struct {
	Age      *int    `json:"age"`
	NickName *string `json:"nick_name"`
}
Copy the code

If the field value is nil, ShouldBindXXX is not passed

But defining all of a structure’s fields as pointer types is a bit unwieldy-and pointer manipulators are less convenient and error-prone (such as null pointer problems)

The second way is through map

If ShouldBindXXX is wrong, then I don’t need to use it, just map parameters (GET)/ data (POST) to map

If you read the map directly, you need to implement your own validation logic. If there are too many columns, you should write a bunch of if… Else or simply implement a generic validation method, which is tedious

So the idea was to use ShouldBindXXX to do the check, and map to distinguish between zero values, that is, the data passed on the request is read twice

Take the GET request as an example:

func HandlerUpdate(c *gin.Context) {
	var user User
	// use ShouldBind to check
	iferr := c.ShouldBind(&user); err ! =nil {
		fmt.Printf("genGetMap ShouldBind error: %v\n", err)
		return
	}
	// The actual parameters passed by the request are mapped to the map
	allMap := map[string]interface{}{}
	urlvalues := c.Request.URL.Query()
	for k, urls := range urlvalues {
		fmt.Printf("\n genGetMap k, urls, %v, %v\n", k, urls)
		// Repeat the last value
		allMap[k] = urls[len(urls)- 1]}}Copy the code

So far, only the verification and acquisition of the requested data, the next step is to update the database operation, here takes GORM as an example

Because user is only used to verify the validity of the requested data and cannot determine the value of zero, you cannot directly operate on the database based on user

// An unexpected result may occur due to a zero value problem
db.Save(&user)
Copy the code

AllMap can determine which parameters/data the request contains, but there may be some additional data that is not needed. For example, if you want to update the age and nick_name attributes of a user, you can use db_user to update the age and nick_name attributes of the user. There is also an is_del column that identifies whether the user logged out, so updating as follows is also problematic:

db.Model(&user).Updates(allMap)
Copy the code

If the is_del attribute is present in allMap, then the is_del field in the table will also be updated, which is not the expected result, so you need to remove the unnecessary attributes in allMap and copy a map containing only the required updated attributes. You can also delete the additional attributes on allMap and keep only the required attributes, as shown in this example

allMap := make(map[string]interface{})
realMap := make(map[string]interface{})
if v, ok := allMap["age"]; ok {
	realMap["age"] = v
}
if v, ok := allMap["nick_name"]; ok {
	realMap["nick_name"] = v
}
db.Model(&user).Updates(realMap)
Copy the code

There is only the age, nick_name two fields so well, but if needed to update the most fields in more than five will write at most five conditional statement, rather tedious, can use reflect processing, no matter how many fields need to be updated, there are code amount is the same

realMap := make(map[string]interface{})
typ := reflect.TypeOf(user).Elem()
for i := 0; i < typ.NumField(); i++ {
	tagName := typ.Field(i).Tag.Get("json")
	if v, isOK := allMap[tagName]; isOK {
		realMap[tagName] = v
	}
}
db.Model(&user).Updates(realMap)
Copy the code

Complete code:

// Map the request parameters to m, if GET, return a map of the query parameters; In the case of POST, the data in the request body is returned
//
// instance: a pointer to an instance of a concrete structure. The function is to get the tag of each field in the structure named 'json' to map the map
func GenMapByStruct(c *gin.Context, instance interface{}, m *map[string]interface{}) error {
	ifc.ContentType() ! = gin.MIMEJSON {return errors.New("content-type must be " + gin.MIMEJSON)
	}
	ifc.Request.Method ! = http.MethodGet && c.Request.Method ! = http.MethodPost {return errors.New("method must be GET or POST")
	}
	allMap := map[string]interface{} {}if c.Request.Method == http.MethodGet {
		iferr := genGetMap(c, instance, &allMap); err ! =nil {
			return err
		}
	} else {
		iferr := genPostMap(c, instance, &allMap); err ! =nil {
			return err
		}
	}
	typ := reflect.TypeOf(instance).Elem()
	for i := 0; i < typ.NumField(); i++ {
		tagName := typ.Field(i).Tag.Get("json")
		if v, isOK := allMap[tagName]; isOK {
			(*m)[tagName] = v
		}
	}
	return nil
}

// Get query from get request and process query into map to allMap
func genGetMap(c *gin.Context, instance interface{}, allMap *map[string]interface{}) error {
	iferr := c.ShouldBind(instance); err ! =nil {
		fmt.Printf("genGetMap ShouldBind error: %v\n", err)
		return err
	}
	urlvalues := c.Request.URL.Query()
	for k, urls := range urlvalues {
		fmt.Printf("\n genGetMap k, urls, %v, %v\n", k, urls)
		// Repeat the last value
		(*allMap)[k] = urls[len(urls)- 1]}return nil
}

// Get the body from the POST request and deserialize it into allMap
func genPostMap(c *gin.Context, instance interface{}, allMap *map[string]interface{}) error {
	// shouldBind causes the body to not be read again, ShouldBindBodyWith is used for convenience
	iferr := c.ShouldBindBodyWith(instance, binding.JSON); err ! =nil {
		fmt.Printf("genPostMap ShouldBind error: %v\n", err)
		return err
	}
	body, _ := c.Get(gin.BodyBytesKey)
	var bodyByte []byte
	var ok bool
	if bodyByte, ok = body.([]byte); ! ok {return errors.New("body is invalid")}if len(bodyByte) == 0 {
		return nil
	}
	iferr := json.Unmarshal(bodyByte, allMap); err ! =nil {
		return err
	}
	return nil
}
Copy the code

Example:

type User struct {
	Age      int    `json:"age"`
	NickName string `json:"nick_name"`
}
r.Any("/update".func(c *gin.Context) {
	m := map[string]interface{} {}var user User
	iferr := 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})
})
Copy the code

As you can see, processing requests with zero values is more expensive because there is more computation involved, so this should be avoided as much as possible, and it is better for the client to carry the full set of required parameters than to do extra processing on the back end