【go-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
結構體,
目前支援JSON
、XML
、FORM
請求格式繫結(請看前面定義的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(¶ms); 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