Merge bf3ab6608cb91028f0b350f410661d040e4af65e into c3d5a28ed6d3849da820195b6774d212bcc038a9

This commit is contained in:
DesolateYH 2025-11-07 14:38:48 +07:00 committed by GitHub
commit 6e3d86dc08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 135 additions and 41 deletions

View File

@ -10,7 +10,6 @@ import (
"io" "io"
"io/fs" "io/fs"
"log" "log"
"maps"
"math" "math"
"mime/multipart" "mime/multipart"
"net" "net"
@ -55,6 +54,42 @@ const ContextRequestKey ContextKeyType = 0
// abortIndex represents a typical value used in abort functions. // abortIndex represents a typical value used in abort functions.
const abortIndex int8 = math.MaxInt8 >> 1 const abortIndex int8 = math.MaxInt8 >> 1
// ContextKeys is a thread-safe key-value store wrapper around sync.Map
// that provides compatibility with existing map[any]any API expectations
type ContextKeys struct {
m sync.Map
}
// Store stores a value in the context keys
func (ck *ContextKeys) Store(key, value any) {
ck.m.Store(key, value)
}
// Load retrieves a value from the context keys
func (ck *ContextKeys) Load(key any) (value any, exists bool) {
return ck.m.Load(key)
}
// Delete removes a value from the context keys
func (ck *ContextKeys) Delete(key any) {
ck.m.Delete(key)
}
// Range iterates over all key-value pairs in the context keys
func (ck *ContextKeys) Range(f func(key, value any) bool) {
ck.m.Range(f)
}
// IsEmpty returns true if the context keys contain no values
func (ck *ContextKeys) IsEmpty() bool {
empty := true
ck.m.Range(func(key, value any) bool {
empty = false
return false // Stop iteration on first item
})
return empty
}
// Context is the most important part of gin. It allows us to pass variables between middleware, // Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example. // manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct { type Context struct {
@ -71,11 +106,9 @@ type Context struct {
params *Params params *Params
skippedNodes *[]skippedNode skippedNodes *[]skippedNode
// This mutex protects Keys map.
mu sync.RWMutex
// Keys is a key/value pair exclusively for the context of each request. // Keys is a key/value pair exclusively for the context of each request.
Keys map[any]any // Using ContextKeys wrapper around sync.Map for better concurrent performance.
Keys *ContextKeys
// Errors is a list of errors attached to all the handlers/middlewares who used this context. // Errors is a list of errors attached to all the handlers/middlewares who used this context.
Errors errorMsgs Errors errorMsgs
@ -106,7 +139,7 @@ func (c *Context) reset() {
c.index = -1 c.index = -1
c.fullPath = "" c.fullPath = ""
c.Keys = nil c.Keys = nil // Reset to nil for backward compatibility
c.Errors = c.Errors[:0] c.Errors = c.Errors[:0]
c.Accepted = nil c.Accepted = nil
c.queryCache = nil c.queryCache = nil
@ -131,10 +164,14 @@ func (c *Context) Copy() *Context {
cp.handlers = nil cp.handlers = nil
cp.fullPath = c.fullPath cp.fullPath = c.fullPath
cKeys := c.Keys // Copy ContextKeys contents if they exist
c.mu.RLock() if c.Keys != nil {
cp.Keys = maps.Clone(cKeys) cp.Keys = &ContextKeys{}
c.mu.RUnlock() c.Keys.Range(func(key, value any) bool {
cp.Keys.Store(key, value)
return true
})
}
cParams := c.Params cParams := c.Params
cp.Params = make([]Param, len(cParams)) cp.Params = make([]Param, len(cParams))
@ -271,24 +308,22 @@ func (c *Context) Error(err error) *Error {
/************************************/ /************************************/
// Set is used to store a new key/value pair exclusively for this context. // Set is used to store a new key/value pair exclusively for this context.
// It also lazy initializes c.Keys if it was not used previously. // Uses ContextKeys wrapper around sync.Map for better concurrent performance.
func (c *Context) Set(key any, value any) { func (c *Context) Set(key any, value any) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Keys == nil { if c.Keys == nil {
c.Keys = make(map[any]any) c.Keys = &ContextKeys{}
} }
c.Keys.Store(key, value)
c.Keys[key] = value
} }
// Get returns the value for the given key, ie: (value, true). // Get returns the value for the given key, ie: (value, true).
// If the value does not exist it returns (nil, false) // If the value does not exist it returns (nil, false)
// Uses ContextKeys wrapper around sync.Map for better concurrent performance.
func (c *Context) Get(key any) (value any, exists bool) { func (c *Context) Get(key any) (value any, exists bool) {
c.mu.RLock() if c.Keys == nil {
defer c.mu.RUnlock() return nil, false
value, exists = c.Keys[key] }
return return c.Keys.Load(key)
} }
// MustGet returns the value for the given key if it exists, otherwise it panics. // MustGet returns the value for the given key if it exists, otherwise it panics.
@ -468,14 +503,27 @@ func (c *Context) GetStringMapStringSlice(key any) map[string][]string {
// Delete deletes the key from the Context's Key map, if it exists. // Delete deletes the key from the Context's Key map, if it exists.
// This operation is safe to be used by concurrent go-routines // This operation is safe to be used by concurrent go-routines
// Uses ContextKeys wrapper around sync.Map for better concurrent performance.
func (c *Context) Delete(key any) { func (c *Context) Delete(key any) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Keys != nil { if c.Keys != nil {
delete(c.Keys, key) c.Keys.Delete(key)
} }
} }
// GetKeysAsMap returns a copy of the context keys as a regular map[any]any.
// This is useful for compatibility with existing APIs that expect regular maps.
// Note: This creates a snapshot of the keys at the time of calling.
func (c *Context) GetKeysAsMap() map[any]any {
result := make(map[any]any)
if c.Keys != nil {
c.Keys.Range(func(key, value any) bool {
result[key] = value
return true
})
}
return result
}
/************************************/ /************************************/
/************ INPUT DATA ************/ /************ INPUT DATA ************/
/************************************/ /************************************/

View File

@ -667,7 +667,9 @@ func TestContextCopy(t *testing.T) {
assert.Equal(t, cp.engine, c.engine) assert.Equal(t, cp.engine, c.engine)
assert.Equal(t, cp.Params, c.Params) assert.Equal(t, cp.Params, c.Params)
cp.Set("foo", "notBar") cp.Set("foo", "notBar")
assert.NotEqual(t, cp.Keys["foo"], c.Keys["foo"]) cpFooValue, _ := cp.Get("foo")
cFooValue, _ := c.Get("foo")
assert.NotEqual(t, cpFooValue, cFooValue)
assert.Equal(t, cp.fullPath, c.fullPath) assert.Equal(t, cp.fullPath, c.fullPath)
} }

View File

@ -284,7 +284,7 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
param := LogFormatterParams{ param := LogFormatterParams{
Request: c.Request, Request: c.Request,
isTerm: isTerm, isTerm: isTerm,
Keys: c.Keys, Keys: c.GetKeysAsMap(),
} }
// Stop timer // Stop timer

View File

@ -207,7 +207,7 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
router.GET("/example", func(c *Context) { router.GET("/example", func(c *Context) {
// set dummy ClientIP // set dummy ClientIP
c.Request.Header.Set("X-Forwarded-For", "20.20.20.20") c.Request.Header.Set("X-Forwarded-For", "20.20.20.20")
gotKeys = c.Keys gotKeys = c.GetKeysAsMap()
time.Sleep(time.Millisecond) time.Sleep(time.Millisecond)
}) })
PerformRequest(router, http.MethodGet, "/example?a=100") PerformRequest(router, http.MethodGet, "/example?a=100")

74
tree.go
View File

@ -9,6 +9,7 @@ import (
"strings" "strings"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
"unsafe"
"github.com/gin-gonic/gin/internal/bytesconv" "github.com/gin-gonic/gin/internal/bytesconv"
) )
@ -59,11 +60,30 @@ func (trees methodTrees) get(method string) *node {
} }
func longestCommonPrefix(a, b string) int { func longestCommonPrefix(a, b string) int {
// Use unsafe operations for better performance in this hot path
aBytes := ([]byte)(a)
bBytes := ([]byte)(b)
minLen := min(len(aBytes), len(bBytes))
// Use word-sized comparison for better performance on 64-bit systems
// Compare 8 bytes at a time when possible
wordSize := 8
i := 0 i := 0
max_ := min(len(a), len(b))
for i < max_ && a[i] == b[i] { // Word-by-word comparison for better performance
for i+wordSize <= minLen {
if *(*uint64)(unsafe.Pointer(&aBytes[i])) != *(*uint64)(unsafe.Pointer(&bBytes[i])) {
break
}
i += wordSize
}
// Byte-by-byte comparison for the remainder
for i < minLen && aBytes[i] == bBytes[i] {
i++ i++
} }
return i return i
} }
@ -421,13 +441,18 @@ func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode
walk: // Outer loop for walking the tree walk: // Outer loop for walking the tree
for { for {
prefix := n.path prefix := n.path
if len(path) > len(prefix) { prefixLen := len(prefix)
if path[:len(prefix)] == prefix { if len(path) > prefixLen {
path = path[len(prefix):] // Use bytes comparison for better performance
pathBytes := ([]byte)(path)
if string(pathBytes[:prefixLen]) == prefix {
path = path[prefixLen:]
// Try all the non-wildcard children first by matching the indices // Try all the non-wildcard children first by matching the indices
idxc := path[0] pathBytes = ([]byte)(path) // Update pathBytes after path change
for i, c := range []byte(n.indices) { idxc := pathBytes[0]
indicesBytes := ([]byte)(n.indices)
for i, c := range indicesBytes {
if c == idxc { if c == idxc {
// strings.HasPrefix(n.children[len(n.children)-1].path, ":") == n.wildChild // strings.HasPrefix(n.children[len(n.children)-1].path, ":") == n.wildChild
if n.wildChild { if n.wildChild {
@ -460,7 +485,11 @@ walk: // Outer loop for walking the tree
for length := len(*skippedNodes); length > 0; length-- { for length := len(*skippedNodes); length > 0; length-- {
skippedNode := (*skippedNodes)[length-1] skippedNode := (*skippedNodes)[length-1]
*skippedNodes = (*skippedNodes)[:length-1] *skippedNodes = (*skippedNodes)[:length-1]
if strings.HasSuffix(skippedNode.path, path) { // Use more efficient suffix check
skippedPathBytes := ([]byte)(skippedNode.path)
pathBytes := ([]byte)(path)
if len(skippedPathBytes) >= len(pathBytes) &&
string(skippedPathBytes[len(skippedPathBytes)-len(pathBytes):]) == path {
path = skippedNode.path path = skippedNode.path
n = skippedNode.node n = skippedNode.node
if value.params != nil { if value.params != nil {
@ -489,8 +518,10 @@ walk: // Outer loop for walking the tree
// tree_test.go line: 204 // tree_test.go line: 204
// Find param end (either '/' or path end) // Find param end (either '/' or path end)
// Use bytes operations for better performance
pathBytes := ([]byte)(path)
end := 0 end := 0
for end < len(path) && path[end] != '/' { for end < len(pathBytes) && pathBytes[end] != '/' {
end++ end++
} }
@ -509,14 +540,17 @@ walk: // Outer loop for walking the tree
// Expand slice within preallocated capacity // Expand slice within preallocated capacity
i := len(*value.params) i := len(*value.params)
*value.params = (*value.params)[:i+1] *value.params = (*value.params)[:i+1]
// Use bytes slicing to avoid string allocation
val := path[:end] val := path[:end]
if unescape { if unescape && end > 0 {
// Only unescape if there are actually characters to unescape
if v, err := url.QueryUnescape(val); err == nil { if v, err := url.QueryUnescape(val); err == nil {
val = v val = v
} }
} }
(*value.params)[i] = Param{ (*value.params)[i] = Param{
Key: n.path[1:], Key: n.path[1:], // Skip the ':' character
Value: val, Value: val,
} }
} }
@ -562,14 +596,16 @@ walk: // Outer loop for walking the tree
// Expand slice within preallocated capacity // Expand slice within preallocated capacity
i := len(*value.params) i := len(*value.params)
*value.params = (*value.params)[:i+1] *value.params = (*value.params)[:i+1]
val := path val := path
if unescape { if unescape && len(path) > 0 {
// Only attempt unescape if path is not empty
if v, err := url.QueryUnescape(path); err == nil { if v, err := url.QueryUnescape(path); err == nil {
val = v val = v
} }
} }
(*value.params)[i] = Param{ (*value.params)[i] = Param{
Key: n.path[2:], Key: n.path[2:], // Skip the '*'
Value: val, Value: val,
} }
} }
@ -591,7 +627,11 @@ walk: // Outer loop for walking the tree
for length := len(*skippedNodes); length > 0; length-- { for length := len(*skippedNodes); length > 0; length-- {
skippedNode := (*skippedNodes)[length-1] skippedNode := (*skippedNodes)[length-1]
*skippedNodes = (*skippedNodes)[:length-1] *skippedNodes = (*skippedNodes)[:length-1]
if strings.HasSuffix(skippedNode.path, path) { // Use more efficient suffix check
skippedPathBytes := ([]byte)(skippedNode.path)
pathBytes := ([]byte)(path)
if len(skippedPathBytes) >= len(pathBytes) &&
string(skippedPathBytes[len(skippedPathBytes)-len(pathBytes):]) == path {
path = skippedNode.path path = skippedNode.path
n = skippedNode.node n = skippedNode.node
if value.params != nil { if value.params != nil {
@ -648,7 +688,11 @@ walk: // Outer loop for walking the tree
for length := len(*skippedNodes); length > 0; length-- { for length := len(*skippedNodes); length > 0; length-- {
skippedNode := (*skippedNodes)[length-1] skippedNode := (*skippedNodes)[length-1]
*skippedNodes = (*skippedNodes)[:length-1] *skippedNodes = (*skippedNodes)[:length-1]
if strings.HasSuffix(skippedNode.path, path) { // Use more efficient suffix check
skippedPathBytes := ([]byte)(skippedNode.path)
pathBytes := ([]byte)(path)
if len(skippedPathBytes) >= len(pathBytes) &&
string(skippedPathBytes[len(skippedPathBytes)-len(pathBytes):]) == path {
path = skippedNode.path path = skippedNode.path
n = skippedNode.node n = skippedNode.node
if value.params != nil { if value.params != nil {