【Gin-API系列】請求和響應參數的檢查綁定(二)


參數設計

一套合格的API的服務需要規范的輸入請求和標准的輸出響應格式。
為了更規范的設計,也是為了代碼的可讀性和擴展性,我們需要對Http請求和響應做好模型設計。

  • 請求

根據【Gin-API系列】需求設計和功能規划(一)請求案例的設計,
我們在ip參數后面再增加一個參數oid來表示模型ID,只返回需要的模型model

 // 考慮到后面會有更多的 API 路由設計,本路由可以命名為 SearchIp

type ReqGetParaSearchIp struct {
	Ip  string        
	Oid configure.Oid 
}

type ReqPostParaSearchIp struct {
	Ip  string        
	Oid configure.Oid 
}

const (
	OidHost   Oid = "HOST"
	OidSwitch Oid = "SWITCH"
)

var OidArray = []Oid{OidHost, OidSwitch}
  • 響應

API的響應都需要統一格式,並維護各字段的文檔解釋

type ResponseData struct {
	Page     int64         `json:"page"`  // 分頁顯示,頁碼
	PageSize int64         `json:"page_size"`  // 分頁顯示,頁面大小
	Size     int64         `json:"size"`  // 返回的元素總數
	Total    int64         `json:"total"`  // 篩選條件計算得到的總數,但可能不會全部返回
	List     []interface{} `json:"list"`
}

type Response struct {
	Code    configure.Code `json:"code"`  // 響應碼
	Message string         `json:"message"` // 響應描述
	Data    ResponseData   `json:"data"`  // 最終返回數據
}
  • 請求IP合法性檢查

我們需要對search_ip接口的請求參數長度、格式、錯誤IP做檢查。
其中,錯誤IP一般指的是127.0.0.1這種回環IP,或者是網關、重復的局域網IP等

func CheckIp(ipArr []string, low, high int) error {
	if low > len(ipArr) || len(ipArr) > high {
		return errors.New(fmt.Sprintf("請求IP數量超過限制"))
	}
	for _, ip := range ipArr {
		if !network.MatchIpPattern(ip) {
			return errors.New(fmt.Sprintf("錯誤的IP格式:%s", ip))
		}
		if network.ErrorIpPattern(ip) {
			return errors.New(fmt.Sprintf("不支持的IP:%s", ip))
		}
	}
	return nil
}

utils.network 文件

// IP地址格式匹配  "010.99.32.88" 屬於正常IP
func MatchIpPattern(ip string) bool {
	//pattern := `^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$`
	//reg := regexp.MustCompile(pattern)
	//return reg.MatchString(ip)
	if net.ParseIP(ip) == nil {
		return false
	}
	return true
}

// 排查錯誤的IP
func ErrorIpPattern(ip string) bool {
	errorIpMapper := map[string]bool{
		"192.168.122.1": true,
		"192.168.250.1": true,
		"192.168.255.1": true,
		"192.168.99.1":  true,
		"192.168.56.1":  true,
		"10.10.10.1":    true,
	}
	errorIpPrefixPattern := []string{"127.0.0.", "169.254.", "11.1.", "10.176."}
	errorIpSuffixPattern := []string{".0.1"}
	if _, ok := errorIpMapper[ip]; ok {
		return true
	}
	for _, p := range errorIpPrefixPattern {
		if strings.HasPrefix(ip, p) {
			return true
		}
	}
	for _, p := range errorIpSuffixPattern {
		if strings.HasSuffix(ip, p) {
			return true
		}
	}
	return false
}
  • 代碼實現

為了通用性設計,我們將main函數的func(c *gin.Context)獨立定義成一個函數SearchIpHandlerWithGet
考慮到API的擴展和兼容,我們將對API的實現區分版本,在route文件夾中新建v1文件夾作為第一版本代碼實現。
同時,我們將search_ip歸類為sdk集合,存放於v1

var SearchIpHandlerWithGet = func(c *gin.Context) {
	ipStr := c.DefaultQuery("ip", "")
	response := route_response.Response{
		Code:configure.RequestSuccess,
		Data: route_response.ResponseData{List: []interface{}{}},
	}
	if ipStr == "" {
		response.Code, response.Message = configure.RequestParameterMiss, "缺少請求參數ip"
		c.JSON(http.StatusOK, response)
		return
	}
	ipArr := strings.Split(ipStr, ",")
	if err := route_request.CheckIp(ipArr, 1, 10); err != nil {
		response.Code, response.Message = configure.RequestParameterRangeError, err.Error()
		c.JSON(http.StatusOK, response)
		return
	}
	hostInfo := map[string]interface{}{
		"10.1.162.18": map[string]string{
			"model": "主機", "IP": "10.1.162.18",
		},
	}
	response.Data = route_response.ResponseData{
		Page:     1,
		PageSize: 1,
		Size:     1,
		Total:    1,
		List:     []interface{}{hostInfo, },
	}
	c.JSON(http.StatusOK, response)
	return
}
  • 結果驗證
D:\> curl http://127.0.0.1:8080?ip=''
{"code":4002,"message":"錯誤的IP格式:''","data":{"page":0,"page_size":0,"size":0,"total":0,"list":[]}}

D:\> curl http://127.0.0.1:8080?ip=
{"code":4005,"message":"缺少請求參數ip","data":{"page":0,"page_size":0,"size":0,"total":0,"list":[]}}

D:\> curl http://127.0.0.1:8080?ip="10.1.1.1"
{"code":0,"message":"","data":{"page":1,"page_size":1,"size":1,"total":1,"list":[{"10.1.162.18":{"IP":"10.1.162.18","model":"主機"}}]}}

Gin.ShouldBind參數綁定

  • 為了使請求參數的可讀性和擴展性更強,我們使用ShouldBind函數來對請求進行參數綁定和校驗

ShouldBind 支持將Http請求內容綁定到Gin Struct結構體,
目前支持JSONXMLFORM請求格式綁定(請看前面定義的ReqParaSearchIp)。

  • 使用方法
// ipStr := c.DefaultQuery("ip", "")
 
var req route_request.ReqParaSearchIp
if err := c.ShouldBindQuery(&req); err != nil {
    response.Code, response.Message = configure.RequestParameterTypeError, err.Error()
    c.JSON(http.StatusOK, response)
    return
}
ipStr := req.Ip
if ipStr == "" {
    response.Code, response.Message = configure.RequestParameterMiss, "缺少請求參數ip"
    c.JSON(http.StatusOK, response)
    return
}
  • 注意事項

GET請求的struct使用form解析
POST請求的使用JSON解析,Content-Type使用application/json

type ReqParaSearchIp struct {
    Ip string         `form:"ip"`
    Oid configure.Oid `form:"oid"`
}

type ReqPostParaSearchIp struct {
    Ip string         `json:"ip"`
    Oid configure.Oid `json:"oid"`
}
  • 自定義校驗器

使用了ShouldBind之后我們就可以使用第三方校驗器來協助校驗參數了。
還記得我們前面的參數校驗嗎,邏輯很簡單,代碼卻很繁瑣。
接下來,我們將使用validator.v10來做自定義校驗器。

先完成validator.v10的初始化綁定

// DefaultValidator 驗證器
type DefaultValidator struct {
	once     sync.Once
	validate *validator.Validate
}

var _ binding.StructValidator = &DefaultValidator{}

// ValidateStruct 如果接收到的類型是一個結構體或指向結構體的指針,則執行驗證。
func (v *DefaultValidator) ValidateStruct(obj interface{}) error {
	if kindOfData(obj) == reflect.Struct {
		v.lazyinit()
		if err := v.validate.Struct(obj); err != nil {
			return err
		}
	}
	return nil
}

// Engine 返回支持`StructValidator`實現的底層驗證引擎
func (v *DefaultValidator) Engine() interface{} {
	v.lazyinit()
	return v.validate
}

func (v *DefaultValidator) lazyinit() {
	v.once.Do(func() {
		v.validate = validator.New()
		v.validate.SetTagName("validate")

		//自定義驗證器 初始化
		for valName, valFun := range validatorMapper {
			if err := v.validate.RegisterValidation(valName, valFun); err != nil {
				fmt.Println(err)
				os.Exit(1)
			}
		}
	})
}

func kindOfData(data interface{}) reflect.Kind {
	value := reflect.ValueOf(data)
	valueType := value.Kind()

	if valueType == reflect.Ptr {
		valueType = value.Elem().Kind()
	}
	return valueType
}

func InitValidator() {
	binding.Validator = new(DefaultValidator)
}

自定義參數驗證器

// 自定義參數驗證器名稱
const (
	ValNameCheckOid string = "check_oid"
)

// 自定義參數驗證器字典
var validatorMapper = map[string]func(field validator.FieldLevel) bool{
	ValNameCheckOid: CheckOid,
}

// 自定義參數驗證器函數
func CheckOid(field validator.FieldLevel) bool {
	oid := configure.Oid(field.Field().String())
	for _, id := range configure.OidArray {
		if oid == id {
			return true
		}
	}
	return false
}

使用參數驗證器

type ReqGetParaSearchIp struct {
	Ip  string        `form:"ip" validate:"required"`
	Oid configure.Oid `form:"oid" validate:"required,check_oid"`
}

type ReqPostParaSearchIp struct {
	Ip  string        `json:"ip" validate:"required"`
	Oid configure.Oid `json:"oid" validate:"required,check_oid"`
}

ShouldBind綁定

var SearchIpHandlerWithGet = func(c *gin.Context) {
	response := route_response.Response{
		Code:configure.RequestSuccess,
		Data: route_response.ResponseData{List: []interface{}{}},
	}
	var params route_request.ReqGetParaSearchIp
	if err := c.ShouldBindQuery(&params); err != nil {
		code, msg := params.ParseError(err)
		response.Code, response.Message = code, msg
		c.JSON(http.StatusOK, response)
		return
	}
	ipArr := strings.Split(params.Ip, ",")
	if err := route_request.CheckIp(ipArr, 1, 10); err != nil {
		response.Code, response.Message = configure.RequestParameterRangeError, err.Error()
		c.JSON(http.StatusOK, response)
		return
	}
	hostInfo := map[string]interface{}{
		"10.1.162.18": map[string]string{
			"model": "主機", "IP": "10.1.162.18",
		},
	}
	response.Data = route_response.ResponseData{
		Page:     1,
		PageSize: 1,
		Size:     1,
		Total:    1,
		List:     []interface{}{hostInfo, },
	}
	c.JSON(http.StatusOK, response)
	return
}

main函數初始化

func main() {
	route := gin.Default()
	route_request.InitValidator()
	route.GET("/", v1_sdk.SearchIpHandlerWithGet)
	if err := route.Run("127.0.0.1:8080"); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}
  • 實現效果展示
D:\> curl "http://127.0.0.1:8080?ip=10.1.1.1&oid=HOST"
{"code":0,"message":"","data":{"page":1,"page_size":1,"size":1,"total":1,"list":[{"10.1.162.18":{"IP":"10.1.162.18","model":"主機"}}]}}

D:\> curl "http://127.0.0.1:8080?ip=10.1.1.1&oid=SWITCH"
{"code":0,"message":"","data":{"page":1,"page_size":1,"size":1,"total":1,"list":[{"10.1.162.18":{"IP":"10.1.162.18","model":"主機"}}]}}

D:\> curl "http://127.0.0.1:8080?ip=10.1.1.1&oid=XX"
{"code":4002,"message":"請求參數 Oid 需要傳入 object id","data":{"page":0,"page_size":0,"size":0,"total":0,"list":[]}}

D:\> curl "http://127.0.0.1:8080?ip=10.1.1&oid=HOST"
{"code":4002,"message":"錯誤的IP格式:10.1.1","data":{"page":0,"page_size":0,"size":0,"total":0,"list":[]}}

Github 代碼

請訪問 Gin-IPs 或者搜索 Gin-IPs


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM