mirror of
https://github.com/gin-gonic/gin.git
synced 2025-12-13 13:12:17 +08:00
Merge bf3ab6608cb91028f0b350f410661d040e4af65e into c3d5a28ed6d3849da820195b6774d212bcc038a9
This commit is contained in:
commit
6e3d86dc08
94
context.go
94
context.go
@ -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 ************/
|
||||||
/************************************/
|
/************************************/
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
74
tree.go
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user