今天学习:

  • ShouldBindJSON
  • binding tag
  • 参数校验
  • 统一错误响应
  • 业务错误和参数错误区分

ShouldBindJSON

ShouldBindJSON会把请求body里的JSON绑定到struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}

func createUser(c *gin.Context) {
var req CreateUserRequest

if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"id": 1,
"name": req.Name,
"email": req.Email,
"age": req.Age,
})

}

注意:c.ShouldBindJSON(&req)这里必须传递指针,因为要修改req的值。

除了ShouldBindJSON,还有一个BindJSON

1
2
3
4
err := c.BindJSON(&req)
if err != nil {
return
}

如果报错,会直接返回。

1
400 Bad Request

不过出于灵活性,工作中常用的是ShouldBindJSON

validator:binding tag

对于熟悉Spring的Java程序员来说,它就相当于@Validated@Valid注解,以及各种字段注解:@NotNull@Size@Min等。

例如:

1
2
3
4
5
6
type CreateUserRequest struct {
Name string `json:"name" binding:"required,min=2,max=5"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
Age int `json:"age" binding:"required,gte=18,lte=120"`
}

在常规json struct中,加入binding,常见规则有:

1
2
3
4
5
6
7
required : 必填
email : 必须是邮箱格式
min : 最小长度或者最小值
max : 最大长度或者最大值
gte : 大于等于
lte : 小于等于
oneof : 枚举值

关于oneof,以角色状态为:

1
Status   string  `json:"status" binding:"required,oneof=active disabled"`

当然,这些知识基础校验,通常也是放在Request DTObinding tag里,对于业务校验,还应该放在service层。例如用户已存在、邮箱后缀不符等。

统一错误响应

不要每个handler随便返回不同格式,对于前端是个灾难,对于Java来说,通常都会定一个一Response类,返回固定格式:

1
2
3
4
5
{
"msg":"xxxx",
"code":200,
“data”:object
}

对于Go Web当然也都是一样的操作(俗称java味?)。

创建一个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
package response

import (
"net/http"

"github.com/gin-gonic/gin"
)

type ErrorResponse struct {
Code string `json:"code"`
Message string `json:"message"`
}

func Error(c *gin.Context, status int, code, message string) {
c.JSON(status, ErrorResponse{
Code: code,
Message: message,
})
}

func BadRequest(c *gin.Context, message string) {
Error(c, http.StatusBadRequest, "BAD_REQUEST", message)
}

func Unauthorized(c *gin.Context, message string) {
Error(c, http.StatusUnauthorized, "UNAUTHORIZED", message)
}

func OK(c *gin.Context, data any) {
c.JSON(http.StatusOK, gin.H{
"data": data,
})
}

使用办法,直接在原来的例子中修改为如下:

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
import (
"fmt"
"net/http"
// 导入
"dev.net.cn/goweb/response"
"github.com/gin-gonic/gin"
)

func createUser(c *gin.Context) {
var req CreateUserRequest

if err := c.ShouldBindJSON(&req); err != nil {
/*
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
*/
// 修改为如下
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{
"id": 1,
"name": req.Name,
"email": req.Email,
"age": req.Age,
})

}

测试:

1
2
3
4
5
6
7
POST http://localhost:8080/user
{
"name":"张三",
"email":"zhangsan@dev.net",
"password":"123456",
"age":28
}

响应如下:

1
2
3
4
{
"code": "BAD_REQUEST",
"message": "Key: 'CreateUserRequest.Password' Error:Field validation for 'Password' failed on the 'min' tag"
}

password的值修改为8位,响应如下:

1
2
3
4
5
6
7
8
{
"data": {
"age": 28,
"email": "zhangsan@dev.net",
"id": 1,
"name": "张三"
}
}

业务错误类型

除了常规错误意外,系统中还应处理业务错误,例如:邮箱已存在、用户名已存在等。

创建errs包,并创建error.go文件,内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package errs

type AppError struct {
Code string
Message string
HTTPStatus int
}

func (e *AppError) Error() string {
return e.Code + " : " + e.Message
}

var ErrUserNotFound = &AppError{
Code: "USER_NOT_FOUND",
Message: "User not found",
HTTPStatus: 404,
}

var ErrEmailExists = &AppError{
Code: "EMAIL_EXISTS",
Message: "Email exists",
HTTPStatus: 409,
}

response包里面的resopnse.go文件中,新增如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 别忘了 import "dev.net.cn/goweb/errs"
func HandleError(c *gin.Context, err error) {
if appErr, ok := errors.AsType[*errs.AppError](err); ok {
c.JSON(appErr.HTTPStatus, gin.H{
"code": appErr.Code,
"message": appErr.Message,
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"code": "INTERNAL_ERROR",
"message": "internal server error",
})
}

修改main.go,新增如下代码:

1
2
3
func validateEmail(email string) error {
return errs.ErrEmailExists
}

修改原来的createUser代码为如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func createUser(c *gin.Context) {
var req CreateUserRequest

if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}

if err := validateEmail(req.Email); err != nil {
response.HandleError(c, err)
return
}

response.OK(c, gin.H{
"id": 1,
"name": req.Name,
"email": req.Email,
"age": req.Age,
})

}

调用这个接口:

1
2
3
4
{
"code": "EMAIL_EXISTS",
"message": "Email exists"
}