mirror of
https://github.com/gin-gonic/gin.git
synced 2026-06-12 08:38:16 +08:00
Compare commits
4 Commits
376a25726e
...
233da4a5ce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
233da4a5ce | ||
|
|
e3118cc378 | ||
|
|
cad29c5e3f | ||
|
|
a7b757e338 |
@ -154,7 +154,7 @@ func runRequest(B *testing.B, r *Engine, method, path string) {
|
||||
w := newMockWriter()
|
||||
B.ReportAllocs()
|
||||
B.ResetTimer()
|
||||
for i := 0; i < B.N; i++ {
|
||||
for B.Loop() {
|
||||
r.ServeHTTP(w, req)
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ func (err SliceValidationError) Error() string {
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
for i := 0; i < len(err); i++ {
|
||||
for i := range len(err) {
|
||||
if err[i] != nil {
|
||||
if b.Len() > 0 {
|
||||
b.WriteString("\n")
|
||||
@ -58,7 +58,7 @@ func (v *defaultValidator) ValidateStruct(obj any) error {
|
||||
case reflect.Slice, reflect.Array:
|
||||
count := value.Len()
|
||||
validateRet := make(SliceValidationError, 0)
|
||||
for i := 0; i < count; i++ {
|
||||
for i := range count {
|
||||
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
|
||||
validateRet = append(validateRet, err)
|
||||
}
|
||||
|
||||
@ -119,7 +119,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
|
||||
tValue := value.Type()
|
||||
|
||||
var isSet bool
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
for i := range value.NumField() {
|
||||
sf := tValue.Field(i)
|
||||
if sf.PkgPath != "" && !sf.Anonymous { // unexported
|
||||
continue
|
||||
|
||||
100
context.go
100
context.go
@ -5,6 +5,7 @@
|
||||
package gin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -93,6 +94,10 @@ type Context struct {
|
||||
// SameSite allows a server to define a cookie attribute making it impossible for
|
||||
// the browser to send this cookie along with cross-site requests.
|
||||
sameSite http.SameSite
|
||||
|
||||
internalContextMu sync.RWMutex
|
||||
internalContext context.Context
|
||||
internalContextCancelCause context.CancelCauseFunc
|
||||
}
|
||||
|
||||
/************************************/
|
||||
@ -114,6 +119,10 @@ func (c *Context) reset() {
|
||||
c.sameSite = 0
|
||||
*c.params = (*c.params)[:0]
|
||||
*c.skippedNodes = (*c.skippedNodes)[:0]
|
||||
|
||||
if c.useInternalContext() {
|
||||
c.WithInternalContext(context.Background())
|
||||
}
|
||||
}
|
||||
|
||||
// Copy returns a copy of the current context that can be safely used outside the request's scope.
|
||||
@ -1418,6 +1427,49 @@ func (c *Context) SetAccepted(formats ...string) {
|
||||
/***** GOLANG.ORG/X/NET/CONTEXT *****/
|
||||
/************************************/
|
||||
|
||||
// WithInternalContext replaces the internal context stored with the provided one in a thread safe manner.
|
||||
// It's important that any context you pass in is not something the wraps *gin.Context,
|
||||
// if you want to wrap a context and then provide it to WithInternalContext, use InternalContext().
|
||||
// If you don't plan to provide the context back to WithInternalContext you can safely use *Context directly.
|
||||
// Otherwise you'll end up with a stack overflow.
|
||||
//
|
||||
// For example:
|
||||
// var c *Context // given a context
|
||||
// // you can safely wrap it and pass it downstream
|
||||
// myDownstreamFunction(context.WithValue(c, ...))
|
||||
//
|
||||
// // but when you want to call WithInternalContext you should do it like this
|
||||
// c.WithInternalContext(context.WithValue(c.InternalContext(), ...))
|
||||
func (c *Context) WithInternalContext(ctx context.Context) {
|
||||
if !c.useInternalContext() {
|
||||
panic("Can't use WithInternalContext when UseInternalContext is false")
|
||||
}
|
||||
|
||||
c.internalContextMu.Lock()
|
||||
defer c.internalContextMu.Unlock()
|
||||
|
||||
c.internalContext, c.internalContextCancelCause = context.WithCancelCause(ctx)
|
||||
}
|
||||
|
||||
// InternalContext provides the currently stored internal context in a thread safe manner.
|
||||
// Use this if you want to wrap a context.Context which you'll end up providing to WithInternalContext.
|
||||
// If you don't plan to provide the context back to WithInternalContext you can safely use *Context directly.
|
||||
func (c *Context) InternalContext() context.Context {
|
||||
if !c.useInternalContext() {
|
||||
panic("Can't use InternalContext when UseInternalContext is false")
|
||||
}
|
||||
|
||||
c.internalContextMu.RLock()
|
||||
defer c.internalContextMu.RUnlock()
|
||||
|
||||
return c.internalContext
|
||||
}
|
||||
|
||||
// hasRequestContext returns whether c.Request has Context and fallback.
|
||||
func (c *Context) useInternalContext() bool {
|
||||
return c.engine != nil && c.engine.UseInternalContext
|
||||
}
|
||||
|
||||
// hasRequestContext returns whether c.Request has Context and fallback.
|
||||
func (c *Context) hasRequestContext() bool {
|
||||
hasFallback := c.engine != nil && c.engine.ContextWithFallback
|
||||
@ -1427,26 +1479,44 @@ func (c *Context) hasRequestContext() bool {
|
||||
|
||||
// Deadline returns that there is no deadline (ok==false) when c.Request has no Context.
|
||||
func (c *Context) Deadline() (deadline time.Time, ok bool) {
|
||||
if !c.hasRequestContext() {
|
||||
return
|
||||
if c.useInternalContext() {
|
||||
c.internalContextMu.RLock()
|
||||
defer c.internalContextMu.RUnlock()
|
||||
|
||||
return c.internalContext.Deadline()
|
||||
} else if c.hasRequestContext() {
|
||||
return c.Request.Context().Deadline()
|
||||
}
|
||||
return c.Request.Context().Deadline()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Done returns nil (chan which will wait forever) when c.Request has no Context.
|
||||
func (c *Context) Done() <-chan struct{} {
|
||||
if !c.hasRequestContext() {
|
||||
return nil
|
||||
if c.useInternalContext() {
|
||||
c.internalContextMu.RLock()
|
||||
defer c.internalContextMu.RUnlock()
|
||||
|
||||
return c.internalContext.Done()
|
||||
} else if c.hasRequestContext() {
|
||||
return c.Request.Context().Done()
|
||||
}
|
||||
return c.Request.Context().Done()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Err returns nil when c.Request has no Context.
|
||||
func (c *Context) Err() error {
|
||||
if !c.hasRequestContext() {
|
||||
return nil
|
||||
if c.useInternalContext() {
|
||||
c.internalContextMu.RLock()
|
||||
defer c.internalContextMu.RUnlock()
|
||||
|
||||
return c.internalContext.Err()
|
||||
} else if c.hasRequestContext() {
|
||||
return c.Request.Context().Err()
|
||||
}
|
||||
return c.Request.Context().Err()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value returns the value associated with this context for key, or nil
|
||||
@ -1464,8 +1534,14 @@ func (c *Context) Value(key any) any {
|
||||
return val
|
||||
}
|
||||
}
|
||||
if !c.hasRequestContext() {
|
||||
return nil
|
||||
if c.useInternalContext() {
|
||||
c.internalContextMu.RLock()
|
||||
defer c.internalContextMu.RUnlock()
|
||||
|
||||
return c.internalContext.Value(key)
|
||||
} else if c.hasRequestContext() {
|
||||
return c.Request.Context().Value(key)
|
||||
}
|
||||
return c.Request.Context().Value(key)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
146
context_test.go
146
context_test.go
@ -3221,6 +3221,142 @@ func TestContextWithFallbackValueFromRequestContext(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextUseInternalContextDeadline(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
|
||||
// enable UseInternalContext feature flag
|
||||
c.engine.UseInternalContext = true
|
||||
})
|
||||
|
||||
deadline, ok := c.Deadline()
|
||||
assert.Zero(t, deadline)
|
||||
assert.False(t, ok)
|
||||
|
||||
c2, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
|
||||
// enable UseInternalContext feature flag
|
||||
c.engine.UseInternalContext = true
|
||||
})
|
||||
|
||||
d := time.Now().Add(time.Second)
|
||||
ctx, cancel := context.WithDeadline(context.Background(), d)
|
||||
defer cancel()
|
||||
c2.WithInternalContext(ctx)
|
||||
deadline, ok = c2.Deadline()
|
||||
assert.Equal(t, d, deadline)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestContextUseInternalContextDone(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
|
||||
// enable UseInternalContext feature flag
|
||||
c.engine.UseInternalContext = true
|
||||
})
|
||||
|
||||
assert.Nil(t, c.Done())
|
||||
|
||||
c2, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
|
||||
// enable UseInternalContext feature flag
|
||||
c.engine.UseInternalContext = true
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
c2.WithInternalContext(ctx)
|
||||
cancel()
|
||||
assert.NotNil(t, <-c2.Done())
|
||||
}
|
||||
|
||||
func TestContextUseInternalContextErr(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
|
||||
// enable UseInternalContext feature flag
|
||||
c.engine.UseInternalContext = true
|
||||
})
|
||||
|
||||
require.NoError(t, c.Err())
|
||||
|
||||
c2, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
|
||||
// enable UseInternalContext feature flag
|
||||
c.engine.UseInternalContext = true
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
c2.WithInternalContext(ctx)
|
||||
cancel()
|
||||
|
||||
assert.EqualError(t, c2.Err(), context.Canceled.Error())
|
||||
}
|
||||
|
||||
func TestContextUseInternalContextValue(t *testing.T) {
|
||||
type contextKey string
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
getContextAndKey func() (*Context, any)
|
||||
value any
|
||||
}{
|
||||
{
|
||||
name: "c with struct context key",
|
||||
getContextAndKey: func() (*Context, any) {
|
||||
type KeyStruct struct{} // https://staticcheck.dev/docs/checks/#SA1029
|
||||
var key KeyStruct
|
||||
c, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
|
||||
// enable UseInternalContext feature flag
|
||||
c.engine.UseInternalContext = true
|
||||
})
|
||||
c.WithInternalContext(context.WithValue(context.TODO(), key, "value"))
|
||||
return c, key
|
||||
},
|
||||
value: "value",
|
||||
},
|
||||
{
|
||||
name: "c with struct context key and request context with different value",
|
||||
getContextAndKey: func() (*Context, any) {
|
||||
type KeyStruct struct{} // https://staticcheck.dev/docs/checks/#SA1029
|
||||
var key KeyStruct
|
||||
c, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
|
||||
// enable UseInternalContext feature flag
|
||||
c.engine.UseInternalContext = true
|
||||
// enable ContextWithFallback feature flag
|
||||
c.engine.ContextWithFallback = true
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/", nil)
|
||||
})
|
||||
c.WithInternalContext(context.WithValue(context.TODO(), key, "value"))
|
||||
c.Request = c.Request.WithContext(context.WithValue(context.TODO(), key, "other value"))
|
||||
return c, key
|
||||
},
|
||||
value: "value",
|
||||
},
|
||||
{
|
||||
name: "c with string context key",
|
||||
getContextAndKey: func() (*Context, any) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
|
||||
// enable UseInternalContext feature flag
|
||||
c.engine.UseInternalContext = true
|
||||
})
|
||||
c.WithInternalContext(context.WithValue(context.TODO(), contextKey("key"), "value"))
|
||||
return c, contextKey("key")
|
||||
},
|
||||
value: "value",
|
||||
},
|
||||
{
|
||||
name: "c with background internal context",
|
||||
getContextAndKey: func() (*Context, any) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder(), func(c *Context) {
|
||||
// enable UseInternalContext feature flag
|
||||
c.engine.UseInternalContext = true
|
||||
})
|
||||
c.WithInternalContext(context.Background())
|
||||
return c, "key"
|
||||
},
|
||||
value: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c, key := tt.getContextAndKey()
|
||||
assert.Equal(t, tt.value, c.Value(key))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextCopyShouldNotCancel(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
@ -3677,22 +3813,22 @@ func BenchmarkGetMapFromFormData(b *testing.B) {
|
||||
|
||||
// Test case 3: Large dataset with many bracket keys
|
||||
largeData := make(map[string][]string)
|
||||
for i := 0; i < 100; i++ {
|
||||
for i := range 100 {
|
||||
key := fmt.Sprintf("ids[%d]", i)
|
||||
largeData[key] = []string{fmt.Sprintf("value%d", i)}
|
||||
}
|
||||
for i := 0; i < 50; i++ {
|
||||
for i := range 50 {
|
||||
key := fmt.Sprintf("names[%d]", i)
|
||||
largeData[key] = []string{fmt.Sprintf("name%d", i)}
|
||||
}
|
||||
for i := 0; i < 25; i++ {
|
||||
for i := range 25 {
|
||||
key := fmt.Sprintf("other[key%d]", i)
|
||||
largeData[key] = []string{fmt.Sprintf("other%d", i)}
|
||||
}
|
||||
|
||||
// Test case 4: Dataset with many non-matching keys (worst case)
|
||||
worstCaseData := make(map[string][]string)
|
||||
for i := 0; i < 100; i++ {
|
||||
for i := range 100 {
|
||||
key := fmt.Sprintf("nonmatching%d", i)
|
||||
worstCaseData[key] = []string{fmt.Sprintf("value%d", i)}
|
||||
}
|
||||
@ -3728,7 +3864,7 @@ func BenchmarkGetMapFromFormData(b *testing.B) {
|
||||
for _, bm := range benchmarks {
|
||||
b.Run(bm.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
_, _ = getMapFromFormData(bm.data, bm.key)
|
||||
}
|
||||
})
|
||||
|
||||
25
gin.go
25
gin.go
@ -169,9 +169,14 @@ type Engine struct {
|
||||
// UseH2C enable h2c support.
|
||||
UseH2C bool
|
||||
|
||||
// ContextWithFallback enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value() when Context.Request.Context() is not nil.
|
||||
// ContextWithFallback enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value()
|
||||
// through Context.Request when Context.Request.Context() is not nil.
|
||||
ContextWithFallback bool
|
||||
|
||||
// UseInternalContext enable fallback Context.Deadline(), Context.Done(), Context.Err()
|
||||
// through InternalContext and supersedes ContextWithFallback
|
||||
UseInternalContext bool
|
||||
|
||||
delims render.Delims
|
||||
secureJSONPrefix string
|
||||
HTMLRender render.HTMLRender
|
||||
@ -669,6 +674,24 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
c.Request = req
|
||||
c.reset()
|
||||
|
||||
// If we're using internalContext then we need to pass on errors from the request context
|
||||
if c.useInternalContext() {
|
||||
reqCtx := req.Context()
|
||||
|
||||
// we need to get the cancelCause function now, so that we have the function for this request
|
||||
// because the c is a pointer to a Context that will possibly go back into the pool and be reused
|
||||
c.internalContextMu.RLock()
|
||||
cancelCause := c.internalContextCancelCause
|
||||
c.internalContextMu.RUnlock()
|
||||
|
||||
go func() {
|
||||
<-reqCtx.Done()
|
||||
if err := reqCtx.Err(); err != nil {
|
||||
cancelCause(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
engine.handleHTTPRequest(c)
|
||||
|
||||
engine.pool.Put(c)
|
||||
|
||||
@ -400,7 +400,7 @@ func TestConcurrentHandleContext(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
iterations := 200
|
||||
wg.Add(iterations)
|
||||
for i := 0; i < iterations; i++ {
|
||||
for range iterations {
|
||||
go func() {
|
||||
req, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
@ -30,7 +30,7 @@ func rawStrToBytes(s string) []byte {
|
||||
|
||||
func TestBytesToString(t *testing.T) {
|
||||
data := make([]byte, 1024)
|
||||
for i := 0; i < 100; i++ {
|
||||
for range 100 {
|
||||
_, err := cRand.Read(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -79,7 +79,7 @@ func RandStringBytesMaskImprSrcSB(n int) string {
|
||||
}
|
||||
|
||||
func TestStringToBytes(t *testing.T) {
|
||||
for i := 0; i < 100; i++ {
|
||||
for range 100 {
|
||||
s := RandStringBytesMaskImprSrcSB(64)
|
||||
if !bytes.Equal(rawStrToBytes(s), StringToBytes(s)) {
|
||||
t.Fatal("don't match")
|
||||
|
||||
@ -14,9 +14,12 @@ import (
|
||||
// This is useful for tests that need to set up a new Gin engine instance
|
||||
// along with a context, for example, to test middleware that doesn't depend on
|
||||
// specific routes. The ResponseWriter `w` is used to initialize the context's writer.
|
||||
func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) {
|
||||
func CreateTestContext(w http.ResponseWriter, opts ...func(c *Context)) (c *Context, r *Engine) {
|
||||
r = New()
|
||||
c = r.allocateContext(0)
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
c.reset()
|
||||
c.writermem.reset(w)
|
||||
return
|
||||
|
||||
7
tree.go
7
tree.go
@ -671,12 +671,7 @@ walk: // Outer loop for walking the tree
|
||||
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) {
|
||||
const stackBufSize = 128
|
||||
|
||||
// Use a static sized buffer on the stack in the common case.
|
||||
// If the path is too long, allocate a buffer on the heap instead.
|
||||
buf := make([]byte, 0, stackBufSize)
|
||||
if length := len(path) + 1; length > stackBufSize {
|
||||
buf = make([]byte, 0, length)
|
||||
}
|
||||
buf := make([]byte, 0, max(stackBufSize, len(path)+1))
|
||||
|
||||
ciPath := n.findCaseInsensitivePathRec(
|
||||
path,
|
||||
|
||||
2
utils.go
2
utils.go
@ -162,7 +162,7 @@ func resolveAddress(addr []string) string {
|
||||
|
||||
// https://stackoverflow.com/questions/53069040/checking-a-string-contains-only-ascii-characters
|
||||
func isASCII(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
for i := range len(s) {
|
||||
if s[i] > unicode.MaxASCII {
|
||||
return false
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user