[Bug]: 使用v10.26.0版本导致了应用程序的崩溃
What happened?
当我使用 v10.26.0 的时候,验证参数,导致了下面的崩溃:
2025/04/03 08:28:01 [Recovery] 2025/04/03 - 08:28:01 panic recovered:
POST /canteen/orderItem/multiple HTTP/1.1
Host: localhost:8586
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Authorization: *
Connection: close
Content-Length: 54
Content-Type: application/json
Referer: http://localhost:9281/h5/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1 HBuilderX
X-Forwarded-For: 127.0.0.1
X-Forwarded-Host: localhost:9281
X-Forwarded-Port: 9281
X-Forwarded-Proto: http
Bad field type param.OrderItem
E:/dependencies/go/pkg/mod/github.com/go-playground/validator/[email protected]/baked_in.go:2192 (0x14629d2)
isGte: panic(fmt.Sprintf("Bad field type %T", field.Interface()))
E:/dependencies/go/pkg/mod/github.com/go-playground/validator/[email protected]/baked_in.go:2285 (0x14635cb)
hasMinOf: return isGte(fl)
E:/dependencies/go/pkg/mod/github.com/go-playground/validator/[email protected]/baked_in.go:44 (0x1450ce3)
wrapFunc.func1: return fn(fl)
E:/dependencies/go/pkg/mod/github.com/go-playground/validator/[email protected]/validator.go:473 (0x14765e1)
(*validate).traverseField: if !ct.fn(ctx, v) {
E:/dependencies/go/pkg/mod/github.com/go-playground/validator/[email protected]/validator.go:315 (0x147882f)
(*validate).traverseField: v.traverseField(ctx, parent, current.Index(i), ns, structNs, reusableCF, ct)
E:/dependencies/go/pkg/mod/github.com/go-playground/validator/[email protected]/validator.go:78 (0x1473f0e)
(*validate).validateStruct: v.traverseField(ctx, current, current.Field(f.idx), ns, structNs, f, f.cTags)
E:/dependencies/go/pkg/mod/github.com/go-playground/validator/[email protected]/validator_instance.go:396 (0x147dcc8)
(*Validate).StructCtx: vd.validateStruct(ctx, top, val, val.Type(), vd.ns[0:0], vd.actualNs[0:0], nil)
E:/dependencies/go/pkg/mod/github.com/go-playground/validator/[email protected]/validator_instance.go:369 (0x147d7d4)
(*Validate).Struct: return v.StructCtx(context.Background(), s)
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/binding/default_validator.go:83 (0x158a74b)
(*defaultValidator).validateStruct: return v.validate.Struct(obj)
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/binding/default_validator.go:60 (0x158a42a)
(*defaultValidator).ValidateStruct: return v.validateStruct(obj)
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/binding/binding.go:121 (0x1589da9)
validate: return Validator.ValidateStruct(obj)
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/binding/json.go:55 (0x15902b7)
decodeJSON: return validate(obj)
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/binding/json.go:37 (0x15900d5)
jsonBinding.Bind: return decodeJSON(req.Body, obj)
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:752 (0x15a4b98)
(*Context).ShouldBindWith: return b.Bind(c.Request, obj)
E:/code/GoglandProjects/realMsgService/ctrl/order_item.go:173 (0x18598f3)
(*OrderItemController).CreateOrderItems: if err := c.ShouldBindWith(&in, binding.JSON); err != nil {
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
(*Context).Next: c.handlers[c.index](c)
E:/code/GoglandProjects/realMsgService/web/middleware/online.go:18 (0x18936d2)
Online: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
(*Context).Next: c.handlers[c.index](c)
E:/code/GoglandProjects/realMsgService/web/middleware/permission.go:16 (0x1895c97)
PermissionMiddleware.func1: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
(*Context).Next: c.handlers[c.index](c)
E:/code/GoglandProjects/realMsgService/web/middleware/auth.go:73 (0x1892804)
Auth: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
(*Context).Next: c.handlers[c.index](c)
E:/code/GoglandProjects/realMsgService/web/middleware/elapsed.go:14 (0x18929c5)
Elapsed: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
(*Context).Next: c.handlers[c.index](c)
E:/code/GoglandProjects/realMsgService/web/middleware/safety_trace.go:71 (0x189486b)
SafetyTracer: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
(*Context).Next: c.handlers[c.index](c)
E:/code/GoglandProjects/realMsgService/web/middleware/repeat_read.go:25 (0x1893a26)
RepeatRead: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
(*Context).Next: c.handlers[c.index](c)
E:/dependencies/go/pkg/mod/github.com/gin-contrib/[email protected]/zap.go:76 (0x189057a)
GinzapWithConfig.func1: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
(*Context).Next: c.handlers[c.index](c)
E:/code/GoglandProjects/realMsgService/web/middleware/request_id.go:19 (0x1893cf8)
RequestIDMiddleware: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
(*Context).Next: c.handlers[c.index](c)
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/recovery.go:102 (0x15b26bc)
CustomRecoveryWithWriter.func1: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:185 (0x159fdb9)
(*Context).Next: c.handlers[c.index](c)
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/gin.go:633 (0x15b00e9)
(*Engine).handleHTTPRequest: c.Next()
E:/dependencies/go/pkg/mod/github.com/gin-gonic/[email protected]/gin.go:589 (0x15afbbb)
(*Engine).ServeHTTP: engine.handleHTTPRequest(c)
C:/Program Files/Go/src/net/http/server.go:3210 (0xbb3f16)
serverHandler.ServeHTTP: handler.ServeHTTP(rw, req)
C:/Program Files/Go/src/net/http/server.go:2092 (0xb9cc74)
(*conn).serve: serverHandler{c.server}.ServeHTTP(w, w.req)
C:/Program Files/Go/src/runtime/asm_amd64.s:1700 (0x6a59c0)
goexit: BYTE $0x90 // NOP
当我退回 v10.14.0 的时候就没有问题了。
realMsgService/ctrl/order_item.go:173 是这样的:
这段代码我已经很久没有改过它,这一次崩溃,就是因为 github.com/go-playground/validator/v10 从 v10.14.0 升到了 v10.26.0 导致的。这给我造成了非常严重的客户信任危机。
代码中使用的结构体 param.OrderItemsCreateInput 是这样的:
package param
type OrderItem struct {
SkuId int64 `json:"skuId,omitempty" form:"skuId" binding:"required,gte=1"`
Subject int8 `json:"subject,omitempty" form:"subject" binding:"required,oneof=1 2"`
Amount int64 `json:"amount,omitempty" form:"amount" binding:"required,gte=1"`
}
type OrderItemsCreateInput struct {
OrderItems []OrderItem `json:"orderItems,omitempty" form:"orderItems" binding:"required,dive,min=1,max=100"`
}
Version
v10.26.0
Example Code
func (ctrl *OrderItemController) CreateOrderItems(c *gin.Context) {
var in param.OrderItemsCreateInput
if err := c.ShouldBindWith(&in, binding.JSON); err != nil {
c.JSON(http.StatusBadRequest, ctrl.ApiCheck(c, gin.H{"msg": ctrl.processError(err)}))
return
}
session, err := concurrency.NewSession(g.EtcdCli())
if err != nil {
c.JSON(http.StatusInternalServerError, ctrl.ApiFail(c, gin.H{"msg": err.Error()}))
return
}
defer func(session *concurrency.Session) {
if err := session.Close(); err != nil {
g.LogWithContext(c.Request.Context()).Error("关闭etcd的session出现错误", zap.Error(err))
}
}(session)
mutex := concurrency.NewMutex(session, fmt.Sprintf("/create-order-item-lock/%d", ctrl.MustLoginUser(c).UserId))
// 竞争锁,最多等待3秒
ctx, cancel := context.WithTimeout(c, time.Second*3)
defer cancel()
err = mutex.TryLock(ctx)
if errors.Is(err, concurrency.ErrLocked) {
c.JSON(http.StatusOK, ctrl.ApiFail(c, gin.H{"msg": "这会儿下单的人太多了"}))
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, ctrl.ApiFail(c, gin.H{"msg": err.Error()}))
return
}
defer func(mutex *concurrency.Mutex, ctx context.Context) {
if err := mutex.Unlock(ctx); err != nil {
g.Log().Error("释放etcd的锁出现错误", zap.Error(err))
}
}(mutex, ctx)
// 创建订单
if orderId, err := ctrl.createOrderItemsAction(c, in); err != nil {
c.JSON(http.StatusInternalServerError, ctrl.ApiFail(c, gin.H{"msg": err.Error()}))
return
} else {
c.JSON(http.StatusOK, ctrl.ApiOk(c, gin.H{
"code": http.StatusOK,
"data": map[string]interface{}{
"orderId": orderId,
},
}))
return
}
}
Hey @ccpwcn , sorry to hear about this issue. Nothing should have changed that would affected these validations, especially such core ones.
Is it possible to provide a simpler reproducible example only using the validator code not through gin? Mainly so I can take a look tomorrow , it’s very late here, will help me debug faster 🙏 If not I can likely piece it together from your example.
I tried really quickly to reproduce but was unable to using the below code.
I even checked the git blame, the gte logic hasn't changed since 2023 and that was a fix for float types and not int64 which I think are used in your case and not changes since 2020, so it must be upstream of that, but if I'm unable to reproduce it would be hard to know where the issue lies.
package main
import (
"fmt"
"github.com/go-playground/validator/v10"
)
type OrderItem struct {
SkuId int64 `json:"skuId,omitempty" form:"skuId" binding:"required,gte=1"`
Subject int8 `json:"subject,omitempty" form:"subject" binding:"required,oneof=1 2"`
Amount int64 `json:"amount,omitempty" form:"amount" binding:"required,gte=1"`
}
type OrderItemsCreateInput struct {
OrderItems []OrderItem `json:"orderItems,omitempty" form:"orderItems" binding:"required,dive,min=1,max=100"`
}
func main() {
validator := validator.New()
input := OrderItemsCreateInput{
OrderItems: []OrderItem{
{
SkuId: 3,
Subject: 2,
Amount: 3,
},
},
}
errs := validator.Struct(input)
fmt.Println(errs)
}
@deankarn I successfully reproduced this crash/panic
package main
import (
"fmt"
"github.com/go-playground/validator/v10"
)
type OrderItem struct {
SkuId int64 `json:"skuId,omitempty" form:"skuId" validate:"required,gte=1"`
Subject int8 `json:"subject,omitempty" form:"subject" validate:"required,oneof=1 2"`
Amount int64 `json:"amount,omitempty" form:"amount" validate:"required,gte=1"`
}
type OrderItemsCreateInput struct {
OrderItems []OrderItem `json:"orderItems,omitempty" form:"orderItems" validate:"required,dive,min=1,max=100"`
}
func main() {
validator := validator.New()
input := OrderItemsCreateInput{
OrderItems: []OrderItem{
{
SkuId: 3,
Subject: 2,
Amount: 3,
},
},
}
errs := validator.Struct(input)
fmt.Println(errs)
}
Do you mean this exact same code fails on your computer @nodivbyzero ?
What version of Go and OS are you using, I was using:
- Go v1.24.2
- Macos Sequoia 15.4
Hey @ccpwcn , sorry to hear about this issue. Nothing should have changed that would affected these validations, especially such core ones.
Is it possible to provide a simpler reproducible example only using the validator code not through gin? Mainly so I can take a look tomorrow , it’s very late here, will help me debug faster 🙏 If not I can likely piece it together from your example.
I used go version go1.23.2 windows/amd64, Windows 10, No more code for this bug, the code are customer private, but this bug is bound to occur.
Hey @ccpwcn , sorry to hear about this issue. Nothing should have changed that would affected these validations, especially such core ones.
Is it possible to provide a simpler reproducible example only using the validator code not through gin? Mainly so I can take a look tomorrow , it’s very late here, will help me debug faster 🙏 If not I can likely piece it together from your example.
v10.14.0 no problem, v10.26.0 is bound to crash/panic.
@deankarn not exactly the same. I changed binding:"required,dive,min=1,max=100" to validate:"required,dive,min=1,max=100" in the provided example.
My Go version:
$ go version
go version go1.24.1 darwin/arm64
Here is the unit-test which fails right now:
type Foo struct {
A int
}
type Bar struct {
B []Foo `validate:"dive,min=1"`
}
fooBarTest := &Bar{
B: []Foo{
{
A: 1,
},
},
}
errs = validate.Struct(fooBarTest)
Equal(t, errs, nil)
I am confused, the original bug report stack trace shows the gte validation erroring which is only on the inner struct.
I see now though why it would fail, this should have never worked before, if it did it was a bug, because min and max shouldn’t work with a struct.
You're right. To constrain the maximum and minimum values of a number, I should use lt、lte、gt、gte.
The problem is: Indeed, our usage is incorrect, however, this still cannot explain the issue of not reporting an error in v10.14.0 but reporting an error in v10.26.0. After all, my code has not been modified for a long time, and the sudden error caught me off guard.
You're right. To constrain the maximum and minimum values of a number, I should use lt、lte、gt、gte.
The problem is: Indeed, our usage is incorrect, however, this still cannot explain the issue of not reporting an error in v10.14.0 but reporting an error in v10.26.0. After all, my code has not been modified for a long time, and the sudden error caught me off guard.
It can prompt me that a certain option is incorrect, but crashing directly is still too uncomfortable.
But, I look it again, my code no problem!!! min, max options used on slice, gte used on int64, oneof used on int8.
But, I look it again, my code no problem!!! min, max options used on slice, gte used on int64, oneof used on int8.
But the current definition is not running min & max on a/the slice because they are after the dive , they are being applied to each OrderItem in the slice. If you want them to apply to the slice of OrderItem they need to come before the dive.
This not me deflecting blame for having a bug, but this is also why it’s important to have unit tests to catch unexpected things before they can affect a customer. I highly recommend everyone always them :)
But, I look it again, my code no problem!!! min, max options used on slice, gte used on int64, oneof used on int8.
But the current definition is not running min & max on a/the slice because they are after the dive , they are being applied to each OrderItem in the slice. If you want them to apply to the slice of OrderItem they need to come before the dive.
This not me deflecting blame for having a bug, but this is also why it’s important to have unit tests to catch unexpected things before they can affect a customer. I highly recommend everyone always them :)
I see, Are you saying that if I want to constrain the []OrderItems slice itself, I should place the constraint before the dive, and if I want to constrain every member of the []OrerItems slice, I should place it after the dive? What do I think is wrong with this? Because in a slice, there may not only be int types, but also other types. How can we use min and max to constrain them uniformly?