手把手,带你从零封装Gin框架(六):初始化 Validat

项目源码

地址: github.com/jassue/jass…

前言

Gin 自带验证器返回的错误信息格式不太友好,本篇将进行调整,实现自定义错误信息,并规范接口返回的数据格式,分别为每种类型的错误定义错误码,前端可以根据对应的错误码实现后续不同的逻辑操作,篇末会使用自定义的 Validator 和 Response 实现第一个接口

自定义验证器错误信息

新建 app/common/request/validator.go 文件,编写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
go复制代码package request

import (
"github.com/go-playground/validator/v10"
)

type Validator interface {
GetMessages() ValidatorMessages
}

type ValidatorMessages map[string]string

// GetErrorMsg 获取错误信息
func GetErrorMsg(request interface{}, err error) string {
if _, isValidatorErrors := err.(validator.ValidationErrors); isValidatorErrors {
_, isValidator := request.(Validator)

for _, v := range err.(validator.ValidationErrors) {
// 若 request 结构体实现 Validator 接口即可实现自定义错误信息
if isValidator {
if message, exist := request.(Validator).GetMessages()[v.Field() + "." + v.Tag()]; exist {
return message
}
}
return v.Error()
}
}

return "Parameter error"
}

新建 app/common/request/user.go 文件,用来存放所有用户相关的请求结构体,并实现 Validator 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package request

type Register struct {
Name string `form:"name" json:"name" binding:"required"`
Mobile string `form:"mobile" json:"mobile" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}

// 自定义错误信息
func (register Register) GetMessages() ValidatorMessages {
return ValidatorMessages{
"Name.required": "用户名称不能为空",
"Mobile.required": "手机号码不能为空",
"Password.required": "用户密码不能为空",
}
}

routes/api.go 中编写测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
go复制代码package routes

import (
"github.com/gin-gonic/gin"
"jassue-gin/app/common/request"
"net/http"
"time"
)

// SetApiGroupRoutes 定义 api 分组路由
func SetApiGroupRoutes(router *gin.RouterGroup) {
//...
router.POST("/user/register", func(c *gin.Context) {
var form request.Register
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(http.StatusOK, gin.H{
"error": request.GetErrorMsg(form, err),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "success",
})
})
}

启动服务器,使用 Postman 测试,如下图所示,自定义错误信息成功

image-20211018192056332.png

自定义验证器

有一些验证规则在 Gin 框架中是没有的,这个时候我们就需要自定义验证器

新建 utils/validator.go 文件,定义验证规则,后续有其他的验证规则将统一存放在这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package utils

import (
"github.com/go-playground/validator/v10"
"regexp"
)

// ValidateMobile 校验手机号
func ValidateMobile(fl validator.FieldLevel) bool {
mobile := fl.Field().String()
ok, _ := regexp.MatchString(`^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$`, mobile)
if !ok {
return false
}
return true
}

新建 bootstrap/validator.go 文件,定制 Gin 框架 Validator 的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
go复制代码package bootstrap

import (
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
"jassue-gin/utils"
"reflect"
"strings"
)

func InitializeValidator() {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 注册自定义验证器
_ = v.RegisterValidation("mobile", utils.ValidateMobile)

// 注册自定义 json tag 函数
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
}
}

main.go 中调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package main

import (
"jassue-gin/bootstrap"
"jassue-gin/global"
)

func main() {
// ...

// 初始化验证器
bootstrap.InitializeValidator()

// 启动服务器
bootstrap.RunServer()
}

app/common/request/user.go 文件,增加 Resister 请求结构体中 Mobile 属性的验证 tag

注:由于在 InitializeValidator() 方法中,使用 RegisterTagNameFunc() 注册了自定义 json tag, 所以在 GetMessages() 中自定义错误信息 key 值时,需使用 json tag 名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码package request

type Register struct {
Name string `form:"name" json:"name" binding:"required"`
Mobile string `form:"mobile" json:"mobile" binding:"required,mobile"`
Password string `form:"password" json:"password" binding:"required"`
}


func (register Register) GetMessages() ValidatorMessages {
return ValidatorMessages{
"name.required": "用户名称不能为空",
"mobile.required": "手机号码不能为空",
"mobile.mobile": "手机号码格式不正确",
"password.required": "用户密码不能为空",
}
}

重启服务器,使用 PostMan 测试,如下图所示,自定义验证器成功

image-20211018194531068.png

自定义错误码

新建 global/error.go 文件,将项目中可能存在的错误都统一存放到这里,为每一种类型错误都定义一个错误码,便于在开发过程快速定位错误,前端也可以根据不同错误码实现不同逻辑的页面交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package global

type CustomError struct {
ErrorCode int
ErrorMsg string
}

type CustomErrors struct {
BusinessError CustomError
ValidateError CustomError
}

var Errors = CustomErrors{
BusinessError: CustomError{40000, "业务错误"},
ValidateError: CustomError{42200, "请求参数错误"},
}

封装 Response

新建 app/common/response/response.go 文件,编写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
go复制代码package response

import (
"github.com/gin-gonic/gin"
"jassue-gin/global"
"net/http"
)

// 响应结构体
type Response struct {
ErrorCode int `json:"error_code"` // 自定义错误码
Data interface{} `json:"data"` // 数据
Message string `json:"message"` // 信息
}

// Success 响应成功 ErrorCode 为 0 表示成功
func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, Response{
0,
data,
"ok",
})
}

// Fail 响应失败 ErrorCode 不为 0 表示失败
func Fail(c *gin.Context, errorCode int, msg string) {
c.JSON(http.StatusOK, Response{
errorCode,
nil,
msg,
})
}

// FailByError 失败响应 返回自定义错误的错误码、错误信息
func FailByError(c *gin.Context, error global.CustomError) {
Fail(c, error.ErrorCode, error.ErrorMsg)
}

// ValidateFail 请求参数验证失败
func ValidateFail(c *gin.Context, msg string) {
Fail(c, global.Errors.ValidateError.ErrorCode, msg)
}

// BusinessFail 业务逻辑失败
func BusinessFail(c *gin.Context, msg string) {
Fail(c, global.Errors.BusinessError.ErrorCode, msg)
}

实现用户注册接口

新建 utils/bcrypt.go 文件,编写密码加密及验证密码的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package utils

import (
"golang.org/x/crypto/bcrypt"
"log"
)

func BcryptMake(pwd []byte) string {
hash, err := bcrypt.GenerateFromPassword(pwd, bcrypt.MinCost)
if err != nil {
log.Println(err)
}
return string(hash)
}

func BcryptMakeCheck(pwd []byte, hashedPwd string) bool {
byteHash := []byte(hashedPwd)
err := bcrypt.CompareHashAndPassword(byteHash, pwd)
if err != nil {
return false
}
return true
}

新建 app/services/user.go 文件,编写用户注册逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
go复制代码package services

import (
"errors"
"jassue-gin/app/common/request"
"jassue-gin/app/models"
"jassue-gin/global"
"jassue-gin/utils"
)

type userService struct {
}

var UserService = new(userService)

// Register 注册
func (userService *userService) Register(params request.Register) (err error, user models.User) {
var result = global.App.DB.Where("mobile = ?", params.Mobile).Select("id").First(&models.User{})
if result.RowsAffected != 0 {
err = errors.New("手机号已存在")
return
}
user = models.User{Name: params.Name, Mobile: params.Mobile, Password: utils.BcryptMake([]byte(params.Password))}
err = global.App.DB.Create(&user).Error
return
}

新建 app/controllers/app/user.go 文件,校验入参,调用 UserService 注册逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package app

import (
"github.com/gin-gonic/gin"
"jassue-gin/app/common/request"
"jassue-gin/app/common/response"
"jassue-gin/app/services"
)

// Register 用户注册
func Register(c *gin.Context) {
var form request.Register
if err := c.ShouldBindJSON(&form); err != nil {
response.ValidateFail(c, request.GetErrorMsg(form, err))
return
}

if err, user := services.UserService.Register(form); err != nil {
response.BusinessFail(c, err.Error())
} else {
response.Success(c, user)
}
}

routes/api.go 中,添加路由

1
2
3
4
5
6
7
8
9
10
11
go复制代码package routes

import (
"github.com/gin-gonic/gin"
"jassue-gin/app/controllers/app"
)

// SetApiGroupRoutes 定义 api 分组路由
func SetApiGroupRoutes(router *gin.RouterGroup) {
router.POST("/auth/register", app.Register)
}

使用 Postman 调用接口 http://localhost:8888/api/auth/register ,如下图所示,接口返回成功

image-20211022184652754.png

查看数据库 users 表,数据已成功写入

image-20211022184849385.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%