Repository: gin-gonic/gin
Pull request #3963 (2024-05-10 13:18:10)

refactor(context): refactor `Keys` type to `map[any]any`
- ⚠️ Destructive adjustments require evaluation.
- Enable `Keys` to support more types.

```go
package main

import "github.com/gin-gonic/gin"

func main() {
	var ctx *gin.Context
	type key struct{}
	
	
	ctx.Set(111, "111")
	ctx.Get(111)
	ctx.Get(key{})
	ctx.Get("111")
	// ……
}
```

The first step in solving https://github.com/gin-gonic/gin/issues/1123#issuecomment-876642837

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/gin-gonic/gin/pull/3963?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.19%. Comparing base [(`3dc1cd6`)](https://app.codecov.io/gh/gin-gonic/gin/commit/3dc1cd6572b4e3a0cd170a15debe546c2c72294f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) to head [(`42a712b`)](https://app.codecov.io/gh/gin-gonic/gin/pull/3963?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic).
> Report is 52 commits behind head on master.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #3963      +/-   ##
==========================================
- Coverage   99.21%   99.19%   -0.03%     
==========================================
  Files          42       43       +1     
  Lines        3182     2722     -460     
==========================================
- Hits         3157     2700     -457     
+ Misses         17       12       -5     
- Partials        8       10       +2     
```

| [Flag](https://app.codecov.io/gh/gin-gonic/gin/pull/3963/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | Coverage Δ | |
|---|---|---|
| [](https://app.codecov.io/gh/gin-gonic/gin/pull/3963/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `?` | |
| [-tags "sonic avx"](https://app.codecov.io/gh/gin-gonic/gin/pull/3963/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `99.18% <100.00%> (?)` | |
| [-tags nomsgpack](https://app.codecov.io/gh/gin-gonic/gin/pull/3963/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `99.17% <100.00%> (?)` | |
| [go-1.18](https://app.codecov.io/gh/gin-gonic/gin/pull/3963/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `?` | |
| [go-1.19](https://app.codecov.io/gh/gin-gonic/gin/pull/3963/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `?` | |
| [go-1.20](https://app.codecov.io/gh/gin-gonic/gin/pull/3963/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `?` | |
| [go-1.21](https://app.codecov.io/gh/gin-gonic/gin/pull/3963/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `99.19% <100.00%> (-0.03%)` | :arrow_down: |
| [go-1.22](https://app.codecov.io/gh/gin-gonic/gin/pull/3963/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `99.17% <100.00%> (?)` | |
| [macos-latest](https://app.codecov.io/gh/gin-gonic/gin/pull/3963/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `99.18% <100.00%> (-0.03%)` | :arrow_down: |
| [ubuntu-latest](https://app.codecov.io/gh/gin-gonic/gin/pull/3963/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `99.17% <100.00%> (-0.04%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic#carryforward-flags-in-the-pull-request-comment) to find out more.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/gin-gonic/gin/pull/3963?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic).

jarrodhroberson: Why not just make it `comparable`, or just `any`?
flc1125: > Why not just make it `comparable`, or just `any`?

I think it's just like the `key` in `context.WithValue(ctx, key, value)`. 

It can be supported, so why not?

Moreover, when the `key` is set to `struct{},` it saves more memory, ex:

```go
type customKey struct{}

ctx.Set(customKey{}, "xxx")
```

jarrodhroberson: In Golang, maps can only have keys of types that are considered *comparable*.
This means the type needs to support the == and != operators for comparison.

which means that structs have to have nothing but  *comparable*  types as
their fields.

it introduces confusion when someone tries to use a key that is not
*comparable.*

*comparable *is broken as far as I am concerned, not being able to provide
a function that makes any struct *comparable* is a serious oversight and
flaw with the language.

I have ended up having to write a library that generates a unique hash of
any struct so that I can pass an array of structs I want to use as keys and
array of values and generate a map with consistent hashing of the identity
(fingerprint) of a struct as a key and the struct as the value. This allows
me to generate keys from structs and look them up in the map without having
to worry about if something I want to store in a map key is a valid key or
not.

On Tue, May 14, 2024 at 11:24 AM Flc゛ ***@***.***> wrote:

> Why not just make it comparable, or just any?
>
> I think it's just like the key in context.WithValue(ctx, key, value).
>
> It can be supported, so why not?
>
> Moreover, when the key is set to struct{}, it saves more memory, ex:
>
> type customKey struct{}
> ctx.Set(customKey{}, "xxx")
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/gin-gonic/gin/pull/3963#issuecomment-2110524488>, or
> unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AABF7754OZ67JDDN5LWVVBDZCIUERAVCNFSM6AAAAABHQT7FSWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCMJQGUZDINBYHA>
> .
> You are receiving this because you commented.Message ID:
> ***@***.***>
>


-- 
Jarrod Roberson
678.551.2852

flc1125: > In Golang, maps can only have keys of types that are considered *comparable*.
> 
> This means the type needs to support the == and != operators for comparison.
> 
> 
> 
> which means that structs have to have nothing but  *comparable*  types as
> 
> their fields.
> 
> 
> 
> it introduces confusion when someone tries to use a key that is not
> 
> *comparable.*
> 
> 
> 
> *comparable *is broken as far as I am concerned, not being able to provide
> 
> a function that makes any struct *comparable* is a serious oversight and
> 
> flaw with the language.
> 
> 
> 
> I have ended up having to write a library that generates a unique hash of
> 
> any struct so that I can pass an array of structs I want to use as keys and
> 
> array of values and generate a map with consistent hashing of the identity
> 
> (fingerprint) of a struct as a key and the struct as the value. This allows
> 
> me to generate keys from structs and look them up in the map without having
> 
> to worry about if something I want to store in a map key is a valid key or
> 
> not.
> 
> 
> 
> On Tue, May 14, 2024 at 11:24 AM Flc゛ ***@***.***> wrote:
> 
> 
> 
> > Why not just make it comparable, or just any?
> 
> >
> 
> > I think it's just like the key in context.WithValue(ctx, key, value).
> 
> >
> 
> > It can be supported, so why not?
> 
> >
> 
> > Moreover, when the key is set to struct{}, it saves more memory, ex:
> 
> >
> 
> > type customKey struct{}
> 
> > ctx.Set(customKey{}, "xxx")
> 
> >
> 
> > —
> 
> > Reply to this email directly, view it on GitHub
> 
> > <https://github.com/gin-gonic/gin/pull/3963#issuecomment-2110524488>, or
> 
> > unsubscribe
> 
> > <https://github.com/notifications/unsubscribe-auth/AABF7754OZ67JDDN5LWVVBDZCIUERAVCNFSM6AAAAABHQT7FSWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCMJQGUZDINBYHA>
> 
> > .
> 
> > You are receiving this because you commented.Message ID:
> 
> > ***@***.***>
> 
> >
> 
> 
> 
> 
> 
> -- 
> 
> Jarrod Roberson
> 
> 678.551.2852
> 
> 

Great reminder, I will add some single test trials.

flc1125: @jarrodhroberson It is indeed recommended to change to `map[comparable]any` for better practice, but unfortunately, Go does not support this format. After checking the official documentation, no better solution has been found. Therefore, I have maintained the `map[any]any` format and added some unit test scripts.

https://github.com/gin-gonic/gin/pull/3963/files#diff-e6ce689a25eaef174c2dd51fe869fabbe04a6c6afbd416b23eda138c82e761baR220-R247

Some references: https://github.com/golang/go/issues/51384
bound2: > @jarrodhroberson It is indeed recommended to change to `map[comparable]any` for better practice, but unfortunately, Go does not support this format. After checking the official documentation, no better solution has been found. Therefore, I have maintained the `map[any]any` format and added some unit test scripts.
> 
> https://github.com/gin-gonic/gin/pull/3963/files#diff-e6ce689a25eaef174c2dd51fe869fabbe04a6c6afbd416b23eda138c82e761baR220-R247
> 
> Some references: [golang/go#51384](https://github.com/golang/go/issues/51384)

Yep went over source and there's no exposed `func` that could be turned into interface type with the given signature. Closest thing was `cmp` package but builtin types don't conform to its `Equal`.
jarrodhroberson: > > @jarrodhroberson It is indeed recommended to change to `map[comparable]any` for better practice, but unfortunately, Go does not support this format. After checking the official documentation, no better solution has been found. Therefore, I have maintained the `map[any]any` format and added some unit test scripts.
> > https://github.com/gin-gonic/gin/pull/3963/files#diff-e6ce689a25eaef174c2dd51fe869fabbe04a6c6afbd416b23eda138c82e761baR220-R247
> > Some references: [golang/go#51384](https://github.com/golang/go/issues/51384)
> 
> Yep went over source and there's no exposed `func` that could be turned into interface type with the given signature. Closest thing was `cmp` package but builtin types don't conform to its `Equal`.


That is one of the inherit flaws of Go in reaction to all the insanely stupid things that other languages (JS cough cough) allow you to do. The inability to define an `comparable` function for your own types is one of them. Thanks for all the work on trying to so some kind of qualify life enhancements. If the Gin team would just make this an Interface it would make all this irrelevant.



xiaotushaoxia: break backwards. looks not good for me. 
dmitry-novozhilov: > if somebody write code like this, this pr make it build fail

But gin.Context is an implementation of context.Context.

And it is a broken implementation, because keys should be any, like in the original context.Context.

And so this change is more important, because it fixes this bug.
VILJkid: Yes, the official context source code: https://cs.opensource.google/go/go/+/refs/tags/go1.24.0:src/context/context.go;drc=f966695ccea356e4e4e8cc0328276e2d00c9fc1e;l=162
appleboy: @flc1125 Please help to resolve conflicts.
flc1125: > @flc1125 Please help to resolve conflicts.

OK
flc1125: @appleboy Ready for review.

PS: The current build failed, but it seems unrelated to the current issue. Please retry.
appleboy: wait https://github.com/gin-gonic/gin/pull/4245 PR merge.

Patch:
diff --git a/.golangci.yml b/.golangci.yml
index d9a60ce..d888706 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -72,6 +72,11 @@ formatters:
     - gofmt
     - gofumpt
     - goimports
+  settings:
+    gofmt:
+      rewrite-rules:
+        - pattern: 'interface{}'
+          replacement: 'any'
   exclusions:
     generated: lax
     paths:
diff --git a/binding/plain.go b/binding/plain.go
index 3b250bb..5d466bd 100644
--- a/binding/plain.go
+++ b/binding/plain.go
@@ -15,7 +15,7 @@ func (plainBinding) Name() string {
 	return "plain"
 }
 
-func (plainBinding) Bind(req *http.Request, obj interface{}) error {
+func (plainBinding) Bind(req *http.Request, obj any) error {
 	all, err := io.ReadAll(req.Body)
 	if err != nil {
 		return err
diff --git a/context.go b/context.go
index d3e4c6d..bf12830 100644
--- a/context.go
+++ b/context.go
@@ -6,6 +6,7 @@ package gin
 
 import (
 	"errors"
+	"fmt"
 	"io"
 	"io/fs"
 	"log"
@@ -72,7 +73,7 @@ type Context struct {
 	mu sync.RWMutex
 
 	// Keys is a key/value pair exclusively for the context of each request.
-	Keys map[string]any
+	Keys map[any]any
 
 	// Errors is a list of errors attached to all the handlers/middlewares who used this context.
 	Errors errorMsgs
@@ -129,7 +130,7 @@ func (c *Context) Copy() *Context {
 	cp.fullPath = c.fullPath
 
 	cKeys := c.Keys
-	cp.Keys = make(map[string]any, len(cKeys))
+	cp.Keys = make(map[any]any, len(cKeys))
 	c.mu.RLock()
 	for k, v := range cKeys {
 		cp.Keys[k] = v
@@ -264,11 +265,11 @@ func (c *Context) Error(err error) *Error {
 
 // 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.
-func (c *Context) Set(key string, value any) {
+func (c *Context) Set(key any, value any) {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 	if c.Keys == nil {
-		c.Keys = make(map[string]any)
+		c.Keys = make(map[any]any)
 	}
 
 	c.Keys[key] = value
@@ -276,7 +277,7 @@ func (c *Context) Set(key string, value any) {
 
 // Get returns the value for the given key, ie: (value, true).
 // If the value does not exist it returns (nil, false)
-func (c *Context) Get(key string) (value any, exists bool) {
+func (c *Context) Get(key any) (value any, exists bool) {
 	c.mu.RLock()
 	defer c.mu.RUnlock()
 	value, exists = c.Keys[key]
@@ -284,14 +285,14 @@ func (c *Context) Get(key string) (value any, exists bool) {
 }
 
 // MustGet returns the value for the given key if it exists, otherwise it panics.
-func (c *Context) MustGet(key string) any {
+func (c *Context) MustGet(key any) any {
 	if value, exists := c.Get(key); exists {
 		return value
 	}
-	panic("Key \"" + key + "\" does not exist")
+	panic(fmt.Sprintf("key %v does not exist", key))
 }
 
-func getTyped[T any](c *Context, key string) (res T) {
+func getTyped[T any](c *Context, key any) (res T) {
 	if val, ok := c.Get(key); ok && val != nil {
 		res, _ = val.(T)
 	}
@@ -299,162 +300,162 @@ func getTyped[T any](c *Context, key string) (res T) {
 }
 
 // GetString returns the value associated with the key as a string.
-func (c *Context) GetString(key string) (s string) {
+func (c *Context) GetString(key any) (s string) {
 	return getTyped[string](c, key)
 }
 
 // GetBool returns the value associated with the key as a boolean.
-func (c *Context) GetBool(key string) (b bool) {
+func (c *Context) GetBool(key any) (b bool) {
 	return getTyped[bool](c, key)
 }
 
 // GetInt returns the value associated with the key as an integer.
-func (c *Context) GetInt(key string) (i int) {
+func (c *Context) GetInt(key any) (i int) {
 	return getTyped[int](c, key)
 }
 
 // GetInt8 returns the value associated with the key as an integer 8.
-func (c *Context) GetInt8(key string) (i8 int8) {
+func (c *Context) GetInt8(key any) (i8 int8) {
 	return getTyped[int8](c, key)
 }
 
 // GetInt16 returns the value associated with the key as an integer 16.
-func (c *Context) GetInt16(key string) (i16 int16) {
+func (c *Context) GetInt16(key any) (i16 int16) {
 	return getTyped[int16](c, key)
 }
 
 // GetInt32 returns the value associated with the key as an integer 32.
-func (c *Context) GetInt32(key string) (i32 int32) {
+func (c *Context) GetInt32(key any) (i32 int32) {
 	return getTyped[int32](c, key)
 }
 
 // GetInt64 returns the value associated with the key as an integer 64.
-func (c *Context) GetInt64(key string) (i64 int64) {
+func (c *Context) GetInt64(key any) (i64 int64) {
 	return getTyped[int64](c, key)
 }
 
 // GetUint returns the value associated with the key as an unsigned integer.
-func (c *Context) GetUint(key string) (ui uint) {
+func (c *Context) GetUint(key any) (ui uint) {
 	return getTyped[uint](c, key)
 }
 
 // GetUint8 returns the value associated with the key as an unsigned integer 8.
-func (c *Context) GetUint8(key string) (ui8 uint8) {
+func (c *Context) GetUint8(key any) (ui8 uint8) {
 	return getTyped[uint8](c, key)
 }
 
 // GetUint16 returns the value associated with the key as an unsigned integer 16.
-func (c *Context) GetUint16(key string) (ui16 uint16) {
+func (c *Context) GetUint16(key any) (ui16 uint16) {
 	return getTyped[uint16](c, key)
 }
 
 // GetUint32 returns the value associated with the key as an unsigned integer 32.
-func (c *Context) GetUint32(key string) (ui32 uint32) {
+func (c *Context) GetUint32(key any) (ui32 uint32) {
 	return getTyped[uint32](c, key)
 }
 
 // GetUint64 returns the value associated with the key as an unsigned integer 64.
-func (c *Context) GetUint64(key string) (ui64 uint64) {
+func (c *Context) GetUint64(key any) (ui64 uint64) {
 	return getTyped[uint64](c, key)
 }
 
 // GetFloat32 returns the value associated with the key as a float32.
-func (c *Context) GetFloat32(key string) (f32 float32) {
+func (c *Context) GetFloat32(key any) (f32 float32) {
 	return getTyped[float32](c, key)
 }
 
 // GetFloat64 returns the value associated with the key as a float64.
-func (c *Context) GetFloat64(key string) (f64 float64) {
+func (c *Context) GetFloat64(key any) (f64 float64) {
 	return getTyped[float64](c, key)
 }
 
 // GetTime returns the value associated with the key as time.
-func (c *Context) GetTime(key string) (t time.Time) {
+func (c *Context) GetTime(key any) (t time.Time) {
 	return getTyped[time.Time](c, key)
 }
 
 // GetDuration returns the value associated with the key as a duration.
-func (c *Context) GetDuration(key string) (d time.Duration) {
+func (c *Context) GetDuration(key any) (d time.Duration) {
 	return getTyped[time.Duration](c, key)
 }
 
 // GetIntSlice returns the value associated with the key as a slice of integers.
-func (c *Context) GetIntSlice(key string) (is []int) {
+func (c *Context) GetIntSlice(key any) (is []int) {
 	return getTyped[[]int](c, key)
 }
 
 // GetInt8Slice returns the value associated with the key as a slice of int8 integers.
-func (c *Context) GetInt8Slice(key string) (i8s []int8) {
+func (c *Context) GetInt8Slice(key any) (i8s []int8) {
 	return getTyped[[]int8](c, key)
 }
 
 // GetInt16Slice returns the value associated with the key as a slice of int16 integers.
-func (c *Context) GetInt16Slice(key string) (i16s []int16) {
+func (c *Context) GetInt16Slice(key any) (i16s []int16) {
 	return getTyped[[]int16](c, key)
 }
 
 // GetInt32Slice returns the value associated with the key as a slice of int32 integers.
-func (c *Context) GetInt32Slice(key string) (i32s []int32) {
+func (c *Context) GetInt32Slice(key any) (i32s []int32) {
 	return getTyped[[]int32](c, key)
 }
 
 // GetInt64Slice returns the value associated with the key as a slice of int64 integers.
-func (c *Context) GetInt64Slice(key string) (i64s []int64) {
+func (c *Context) GetInt64Slice(key any) (i64s []int64) {
 	return getTyped[[]int64](c, key)
 }
 
 // GetUintSlice returns the value associated with the key as a slice of unsigned integers.
-func (c *Context) GetUintSlice(key string) (uis []uint) {
+func (c *Context) GetUintSlice(key any) (uis []uint) {
 	return getTyped[[]uint](c, key)
 }
 
 // GetUint8Slice returns the value associated with the key as a slice of uint8 integers.
-func (c *Context) GetUint8Slice(key string) (ui8s []uint8) {
+func (c *Context) GetUint8Slice(key any) (ui8s []uint8) {
 	return getTyped[[]uint8](c, key)
 }
 
 // GetUint16Slice returns the value associated with the key as a slice of uint16 integers.
-func (c *Context) GetUint16Slice(key string) (ui16s []uint16) {
+func (c *Context) GetUint16Slice(key any) (ui16s []uint16) {
 	return getTyped[[]uint16](c, key)
 }
 
 // GetUint32Slice returns the value associated with the key as a slice of uint32 integers.
-func (c *Context) GetUint32Slice(key string) (ui32s []uint32) {
+func (c *Context) GetUint32Slice(key any) (ui32s []uint32) {
 	return getTyped[[]uint32](c, key)
 }
 
 // GetUint64Slice returns the value associated with the key as a slice of uint64 integers.
-func (c *Context) GetUint64Slice(key string) (ui64s []uint64) {
+func (c *Context) GetUint64Slice(key any) (ui64s []uint64) {
 	return getTyped[[]uint64](c, key)
 }
 
 // GetFloat32Slice returns the value associated with the key as a slice of float32 numbers.
-func (c *Context) GetFloat32Slice(key string) (f32s []float32) {
+func (c *Context) GetFloat32Slice(key any) (f32s []float32) {
 	return getTyped[[]float32](c, key)
 }
 
 // GetFloat64Slice returns the value associated with the key as a slice of float64 numbers.
-func (c *Context) GetFloat64Slice(key string) (f64s []float64) {
+func (c *Context) GetFloat64Slice(key any) (f64s []float64) {
 	return getTyped[[]float64](c, key)
 }
 
 // GetStringSlice returns the value associated with the key as a slice of strings.
-func (c *Context) GetStringSlice(key string) (ss []string) {
+func (c *Context) GetStringSlice(key any) (ss []string) {
 	return getTyped[[]string](c, key)
 }
 
 // GetStringMap returns the value associated with the key as a map of interfaces.
-func (c *Context) GetStringMap(key string) (sm map[string]any) {
+func (c *Context) GetStringMap(key any) (sm map[string]any) {
 	return getTyped[map[string]any](c, key)
 }
 
 // GetStringMapString returns the value associated with the key as a map of strings.
-func (c *Context) GetStringMapString(key string) (sms map[string]string) {
+func (c *Context) GetStringMapString(key any) (sms map[string]string) {
 	return getTyped[map[string]string](c, key)
 }
 
 // GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.
-func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string) {
+func (c *Context) GetStringMapStringSlice(key any) (smss map[string][]string) {
 	return getTyped[map[string][]string](c, key)
 }
 
@@ -769,8 +770,19 @@ func (c *Context) BindUri(obj any) error {
 // It will abort the request with HTTP 400 if any error occurs.
 // See the binding package.
 func (c *Context) MustBindWith(obj any, b binding.Binding) error {
-	if err := c.ShouldBindWith(obj, b); err != nil {
-		c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck
+	err := c.ShouldBindWith(obj, b)
+	if err != nil {
+		var maxBytesErr *http.MaxBytesError
+
+		// Note: When using sonic or go-json as JSON encoder, they do not propagate the http.MaxBytesError error
+		// https://github.com/goccy/go-json/issues/485
+		// https://github.com/bytedance/sonic/issues/800
+		switch {
+		case errors.As(err, &maxBytesErr):
+			c.AbortWithError(http.StatusRequestEntityTooLarge, err).SetType(ErrorTypeBind) //nolint: errcheck
+		default:
+			c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck
+		}
 		return err
 	}
 	return nil
diff --git a/debug.go b/debug.go
index f201616..0ab14e4 100644
--- a/debug.go
+++ b/debug.go
@@ -25,7 +25,7 @@ func IsDebugging() bool {
 var DebugPrintRouteFunc func(httpMethod, absolutePath, handlerName string, nuHandlers int)
 
 // DebugPrintFunc indicates debug log output format.
-var DebugPrintFunc func(format string, values ...interface{})
+var DebugPrintFunc func(format string, values ...any)
 
 func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
 	if IsDebugging() {
diff --git a/internal/json/go_json.go b/internal/json/go_json.go
index 47c3559..dee09de 100644
--- a/internal/json/go_json.go
+++ b/internal/json/go_json.go
@@ -20,3 +20,6 @@ var (
 	// NewEncoder is exported by gin/json package.
 	NewEncoder = json.NewEncoder
 )
+
+// Package indicates what library is being used for JSON encoding.
+const Package = "github.com/goccy/go-json"
diff --git a/internal/json/json.go b/internal/json/json.go
index 2681778..539daa7 100644
--- a/internal/json/json.go
+++ b/internal/json/json.go
@@ -20,3 +20,6 @@ var (
 	// NewEncoder is exported by gin/json package.
 	NewEncoder = json.NewEncoder
 )
+
+// Package indicates what library is being used for JSON encoding.
+const Package = "encoding/json"
diff --git a/internal/json/jsoniter.go b/internal/json/jsoniter.go
index 45ed16b..287ebf7 100644
--- a/internal/json/jsoniter.go
+++ b/internal/json/jsoniter.go
@@ -21,3 +21,6 @@ var (
 	// NewEncoder is exported by gin/json package.
 	NewEncoder = json.NewEncoder
 )
+
+// Package indicates what library is being used for JSON encoding.
+const Package = "github.com/json-iterator/go"
diff --git a/internal/json/sonic.go b/internal/json/sonic.go
index 8cd8804..b3f7242 100644
--- a/internal/json/sonic.go
+++ b/internal/json/sonic.go
@@ -21,3 +21,6 @@ var (
 	// NewEncoder is exported by gin/json package.
 	NewEncoder = json.NewEncoder
 )
+
+// Package indicates what library is being used for JSON encoding.
+const Package = "github.com/bytedance/sonic"
diff --git a/logger.go b/logger.go
index db2c683..f4a250a 100644
--- a/logger.go
+++ b/logger.go
@@ -82,7 +82,7 @@ type LogFormatterParams struct {
 	// BodySize is the size of the Response Body
 	BodySize int
 	// Keys are the keys set on the request's context.
-	Keys map[string]any
+	Keys map[any]any
 }
 
 // StatusCodeColor is the ANSI color for appropriately logging http status code to a terminal.


Test patch:
diff --git a/context_test.go b/context_test.go
index 084bae5..ff43cd0 100644
--- a/context_test.go
+++ b/context_test.go
@@ -28,6 +28,7 @@ import (
 
 	"github.com/gin-contrib/sse"
 	"github.com/gin-gonic/gin/binding"
+	"github.com/gin-gonic/gin/internal/json"
 	testdata "github.com/gin-gonic/gin/testdata/protoexample"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -173,6 +174,9 @@ func TestSaveUploadedFileWithPermission(t *testing.T) {
 	assert.Equal(t, "permission_test", f.Filename)
 	var mode fs.FileMode = 0o755
 	require.NoError(t, c.SaveUploadedFile(f, "permission_test", mode))
+	t.Cleanup(func() {
+		assert.NoError(t, os.Remove("permission_test"))
+	})
 	info, err := os.Stat(filepath.Dir("permission_test"))
 	require.NoError(t, err)
 	assert.Equal(t, info.Mode().Perm(), mode)
@@ -253,7 +257,46 @@ func TestContextSetGet(t *testing.T) {
 	assert.False(t, err)
 
 	assert.Equal(t, "bar", c.MustGet("foo"))
-	assert.Panics(t, func() { c.MustGet("no_exist") })
+	assert.Panicsf(t, func() {
+		c.MustGet("no_exist")
+	}, "key no_exist does not exist")
+}
+
+func TestContextSetGetAnyKey(t *testing.T) {
+	c, _ := CreateTestContext(httptest.NewRecorder())
+
+	type key struct{}
+
+	tests := []struct {
+		key any
+	}{
+		{1},
+		{int32(1)},
+		{int64(1)},
+		{uint(1)},
+		{float32(1)},
+		{key{}},
+		{&key{}},
+	}
+
+	for _, tt := range tests {
+		t.Run(reflect.TypeOf(tt.key).String(), func(t *testing.T) {
+			c.Set(tt.key, 1)
+			value, ok := c.Get(tt.key)
+			assert.True(t, ok)
+			assert.Equal(t, 1, value)
+		})
+	}
+}
+
+func TestContextSetGetPanicsWhenKeyNotComparable(t *testing.T) {
+	c, _ := CreateTestContext(httptest.NewRecorder())
+
+	assert.Panics(t, func() {
+		c.Set([]int{1}, 1)
+		c.Set(func() {}, 1)
+		c.Set(make(chan int), 1)
+	})
 }
 
 func TestContextSetGetValues(t *testing.T) {
@@ -671,7 +714,7 @@ func TestContextDefaultQueryOnEmptyRequest(t *testing.T) {
 
 func TestContextQueryAndPostForm(t *testing.T) {
 	c, _ := CreateTestContext(httptest.NewRecorder())
-	body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second")
+	body := strings.NewReader("foo=bar&page=11&both=&foo=second")
 	c.Request, _ = http.NewRequest(http.MethodPost,
 		"/?both=GET&id=main&id=omit&array[]=first&array[]=second&ids[a]=hi&ids[b]=3.14", body)
 	c.Request.Header.Add("Content-Type", MIMEPOSTForm)
@@ -946,7 +989,7 @@ func TestContextRenderJSONPWithoutCallback(t *testing.T) {
 	c.JSONP(http.StatusCreated, H{"foo": "bar"})
 
 	assert.Equal(t, http.StatusCreated, w.Code)
-	assert.JSONEq(t, "{\"foo\":\"bar\"}", w.Body.String())
+	assert.JSONEq(t, `{"foo":"bar"}`, w.Body.String())
 	assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
 }
 
@@ -972,7 +1015,7 @@ func TestContextRenderAPIJSON(t *testing.T) {
 	c.JSON(http.StatusCreated, H{"foo": "bar"})
 
 	assert.Equal(t, http.StatusCreated, w.Code)
-	assert.JSONEq(t, "{\"foo\":\"bar\"}", w.Body.String())
+	assert.JSONEq(t, `{"foo":"bar"}`, w.Body.String())
 	assert.Equal(t, "application/vnd.api+json", w.Header().Get("Content-Type"))
 }
 
@@ -1432,7 +1475,7 @@ func TestContextNegotiationWithJSON(t *testing.T) {
 	})
 
 	assert.Equal(t, http.StatusOK, w.Code)
-	assert.JSONEq(t, "{\"foo\":\"bar\"}", w.Body.String())
+	assert.JSONEq(t, `{"foo":"bar"}`, w.Body.String())
 	assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
 }
 
@@ -1848,9 +1891,41 @@ func TestContextContentType(t *testing.T) {
 	assert.Equal(t, "application/json", c.ContentType())
 }
 
+func TestContextBindRequestTooLarge(t *testing.T) {
+	// When using sonic or go-json as JSON encoder, they do not propagate the http.MaxBytesError error
+	// The response will fail with a generic 400 instead of 413
+	// https://github.com/goccy/go-json/issues/485
+	// https://github.com/bytedance/sonic/issues/800
+	var expectedCode int
+	switch json.Package {
+	case "github.com/goccy/go-json", "github.com/bytedance/sonic":
+		expectedCode = http.StatusBadRequest
+	default:
+		expectedCode = http.StatusRequestEntityTooLarge
+	}
+
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
+	c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`))
+	c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 10)
+
+	var obj struct {
+		Foo string `json:"foo"`
+		Bar string `json:"bar"`
+	}
+	require.Error(t, c.BindJSON(&obj))
+	c.Writer.WriteHeaderNow()
+
+	assert.Empty(t, obj.Bar)
+	assert.Empty(t, obj.Foo)
+	assert.Equal(t, expectedCode, w.Code)
+	assert.True(t, c.IsAborted())
+}
+
 func TestContextAutoBindJSON(t *testing.T) {
 	c, _ := CreateTestContext(httptest.NewRecorder())
-	c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
+	c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`))
 	c.Request.Header.Add("Content-Type", MIMEJSON)
 
 	var obj struct {
@@ -1867,7 +1942,7 @@ func TestContextBindWithJSON(t *testing.T) {
 	w := httptest.NewRecorder()
 	c, _ := CreateTestContext(w)
 
-	c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
+	c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`))
 	c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
 
 	var obj struct {
@@ -1884,7 +1959,7 @@ func TestContextBindWithXML(t *testing.T) {
 	w := httptest.NewRecorder()
 	c, _ := CreateTestContext(w)
 
-	c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`<?xml version="1.0" encoding="UTF-8"?>
+	c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`<?xml version="1.0" encoding="UTF-8"?>
 		<root>
 			<foo>FOO</foo>
 		   	<bar>BAR</bar>
@@ -1951,7 +2026,7 @@ func TestContextBindWithQuery(t *testing.T) {
 	w := httptest.NewRecorder()
 	c, _ := CreateTestContext(w)
 
-	c.Request, _ = http.NewRequest(http.MethodPost, "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused"))
+	c.Request, _ = http.NewRequest(http.MethodPost, "/?foo=bar&bar=foo", strings.NewReader("foo=unused"))
 
 	var obj struct {
 		Foo string `form:"foo"`
@@ -1967,7 +2042,7 @@ func TestContextBindWithYAML(t *testing.T) {
 	w := httptest.NewRecorder()
 	c, _ := CreateTestContext(w)
 
-	c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("foo: bar\nbar: foo"))
+	c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader("foo: bar\nbar: foo"))
 	c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
 
 	var obj struct {
@@ -1984,7 +2059,7 @@ func TestContextBindWithTOML(t *testing.T) {
 	w := httptest.NewRecorder()
 	c, _ := CreateTestContext(w)
 
-	c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("foo = 'bar'\nbar = 'foo'"))
+	c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader("foo = 'bar'\nbar = 'foo'"))
 	c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
 
 	var obj struct {
@@ -2001,7 +2076,7 @@ func TestContextBadAutoBind(t *testing.T) {
 	w := httptest.NewRecorder()
 	c, _ := CreateTestContext(w)
 
-	c.Request, _ = http.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}"))
+	c.Request, _ = http.NewRequest(http.MethodPost, "http://example.com", strings.NewReader("\"foo\":\"bar\", \"bar\":\"foo\"}"))
 	c.Request.Header.Add("Content-Type", MIMEJSON)
 	var obj struct {
 		Foo string `json:"foo"`
@@ -2020,7 +2095,7 @@ func TestContextBadAutoBind(t *testing.T) {
 
 func TestContextAutoShouldBindJSON(t *testing.T) {
 	c, _ := CreateTestContext(httptest.NewRecorder())
-	c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
+	c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`))
 	c.Request.Header.Add("Content-Type", MIMEJSON)
 
 	var obj struct {
@@ -2037,7 +2112,7 @@ func TestContextShouldBindWithJSON(t *testing.T) {
 	w := httptest.NewRecorder()
 	c, _ := CreateTestContext(w)
 
-	c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
+	c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`))
 	c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
 
 	var obj struct {
@@ -2054,7 +2129,7 @@ func TestContextShouldBindWithXML(t *testing.T) {
 	w := httptest.NewRecorder()
 	c, _ := CreateTestContext(w)
 
-	c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`<?xml version="1.0" encoding="UTF-8"?>
+	c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`<?xml version="1.0" encoding="UTF-8"?>
 		<root>
 			<foo>FOO</foo>
 			<bar>BAR</bar>
@@ -2121,7 +2196,7 @@ func TestContextShouldBindWithQuery(t *testing.T) {
 	w := httptest.NewRecorder()
 	c, _ := CreateTestContext(w)
 
-	c.Request, _ = http.NewRequest(http.MethodPost, "/?foo=bar&bar=foo&Foo=bar1&Bar=foo1", bytes.NewBufferString("foo=unused"))
+	c.Request, _ = http.NewRequest(http.MethodPost, "/?foo=bar&bar=foo&Foo=bar1&Bar=foo1", strings.NewReader("foo=unused"))
 
 	var obj struct {
 		Foo  string `form:"foo"`
@@ -2141,7 +2216,7 @@ func TestContextShouldBindWithYAML(t *testing.T) {
 	w := httptest.NewRecorder()
 	c, _ := CreateTestContext(w)
 
-	c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("foo: bar\nbar: foo"))
+	c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader("foo: bar\nbar: foo"))
 	c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
 
 	var obj struct {
@@ -2158,7 +2233,7 @@ func TestContextShouldBindWithTOML(t *testing.T) {
 	w := httptest.NewRecorder()
 	c, _ := CreateTestContext(w)
 
-	c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("foo='bar'\nbar= 'foo'"))
+	c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader("foo='bar'\nbar= 'foo'"))
 	c.Request.Header.Add("Content-Type", MIMETOML) // set fake content-type
 
 	var obj struct {
@@ -2175,7 +2250,7 @@ func TestContextBadAutoShouldBind(t *testing.T) {
 	w := httptest.NewRecorder()
 	c, _ := CreateTestContext(w)
 
-	c.Request, _ = http.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}"))
+	c.Request, _ = http.NewRequest(http.MethodPost, "http://example.com", strings.NewReader(`"foo":"bar", "bar":"foo"}`))
 	c.Request.Header.Add("Content-Type", MIMEJSON)
 	var obj struct {
 		Foo string `json:"foo"`
@@ -2239,7 +2314,7 @@ func TestContextShouldBindBodyWith(t *testing.T) {
 			w := httptest.NewRecorder()
 			c, _ := CreateTestContext(w)
 			c.Request, _ = http.NewRequest(
-				http.MethodPost, "http://example.com", bytes.NewBufferString(tt.bodyA),
+				http.MethodPost, "http://example.com", strings.NewReader(tt.bodyA),
 			)
 			// When it binds to typeA and typeB, it finds the body is
 			// not typeB but typeA.
@@ -2257,7 +2332,7 @@ func TestContextShouldBindBodyWith(t *testing.T) {
 			w := httptest.NewRecorder()
 			c, _ := CreateTestContext(w)
 			c.Request, _ = http.NewRequest(
-				http.MethodPost, "http://example.com", bytes.NewBufferString(tt.bodyB),
+				http.MethodPost, "http://example.com", strings.NewReader(tt.bodyB),
 			)
 			objA := typeA{}
 			require.Error(t, c.ShouldBindBodyWith(&objA, tt.bindingA))
@@ -2603,7 +2678,7 @@ func TestContextShouldBindBodyWithPlain(t *testing.T) {
 
 func TestContextGolangContext(t *testing.T) {
 	c, _ := CreateTestContext(httptest.NewRecorder())
-	c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
+	c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`))
 	require.NoError(t, c.Err())
 	assert.Nil(t, c.Done())
 	ti, ok := c.Deadline()
@@ -2651,7 +2726,7 @@ func TestGetRequestHeaderValue(t *testing.T) {
 
 func TestContextGetRawData(t *testing.T) {
 	c, _ := CreateTestContext(httptest.NewRecorder())
-	body := bytes.NewBufferString("Fetch binary post data")
+	body := strings.NewReader("Fetch binary post data")
 	c.Request, _ = http.NewRequest(http.MethodPost, "/", body)
 	c.Request.Header.Add("Content-Type", MIMEPOSTForm)
 
diff --git a/logger_test.go b/logger_test.go
index 30b2529..8a542e9 100644
--- a/logger_test.go
+++ b/logger_test.go
@@ -181,7 +181,7 @@ func TestLoggerWithFormatter(t *testing.T) {
 
 func TestLoggerWithConfigFormatting(t *testing.T) {
 	var gotParam LogFormatterParams
-	var gotKeys map[string]any
+	var gotKeys map[any]any
 	buffer := new(strings.Builder)
 
 	router := New()
diff --git a/render/render_test.go b/render/render_test.go
index 447ea6a..4d8ebb1 100644
--- a/render/render_test.go
+++ b/render/render_test.go
@@ -608,7 +608,7 @@ func TestRenderReaderNoContentLength(t *testing.T) {
 }
 
 func TestRenderWriteError(t *testing.T) {
-	data := []interface{}{"value1", "value2"}
+	data := []any{"value1", "value2"}
 	prefix := "my-prefix:"
 	r := SecureJSON{Data: data, Prefix: prefix}
 	ew := &errorWriter{