今天学习:
- 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 }
|
如果报错,会直接返回。
不过出于灵活性,工作中常用的是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 DTO的binding 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 {
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
| 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" }
|