Repository: gin-gonic/gin
Pull request #3910 (2024-03-29 09:46:49)

refactor(slice): simplify SliceValidationError Error method
Enhance code readability by replacing the original "switch statement and if conditionals" with a simple "for loop" to iterate over the error slice.

Below is my local test code:

default_validator.go
```go
type SliceValidationError []error

// Error concatenates all error elements in SliceValidationError into a single string separated by \n.
func (err SliceValidationError) Error() string {
	n := len(err)
	switch n {
	case 0:
		return ""
	default:
		var b strings.Builder
		if err[0] != nil {
			fmt.Fprintf(&b, "[%d]: %s", 0, err[0].Error())
		}
		if n > 1 {
			for i := 1; i < n; i++ {
				if err[i] != nil {
					b.WriteString("\n")
					fmt.Fprintf(&b, "[%d]: %s", i, err[i].Error())
				}
			}
		}
		return b.String()
	}
}

func (err SliceValidationError) NewError() string {
	if len(err) == 0 {
		return ""
	}

	var b strings.Builder
	for i := 0; i < len(err); i++ {
		if err[i] != nil {
			if b.Len() > 0 {
				b.WriteString("\n")
			}
			b.WriteString("[" + strconv.Itoa(i) + "]: " + err[i].Error())
		}
	}
	return b.String()
}
```

default_validator_benchmark_test.go
```go
package binding

import (
	"errors"
	"strconv"
	"testing"

	"github.com/stretchr/testify/assert"
)

func getSliceValidationError(n int) SliceValidationError {
	e := make(SliceValidationError, n)
	for i := 0; i < n; i++ {
		e[i] = errors.New(strconv.Itoa(i))
	}
	return e
}

func TestSameError(t *testing.T) {
	e0 := getSliceValidationError(0)
	assert.Equal(t, e0.Error(), e0.NewError())

	e1 := getSliceValidationError(1)
	assert.Equal(t, e1.Error(), e1.NewError())

	e10 := getSliceValidationError(10)
	assert.Equal(t, e10.Error(), e10.NewError())
}

func BenchmarkSliceValidationError0(b *testing.B) {
	e := getSliceValidationError(0)

	b.ReportAllocs()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		_ = e.Error()
	}
}

func BenchmarkSliceValidationNewError0(b *testing.B) {
	e := getSliceValidationError(0)

	b.ReportAllocs()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		_ = e.NewError()
	}
}

func BenchmarkSliceValidationError1(b *testing.B) {
	e := getSliceValidationError(1)

	b.ReportAllocs()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		_ = e.Error()
	}
}

func BenchmarkSliceValidationNewError1(b *testing.B) {
	e := getSliceValidationError(1)

	b.ReportAllocs()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		_ = e.NewError()
	}
}

func BenchmarkSliceValidationError10(b *testing.B) {
	e := getSliceValidationError(10)

	b.ReportAllocs()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		_ = e.Error()
	}
}

func BenchmarkSliceValidationNewError10(b *testing.B) {
	e := getSliceValidationError(10)

	b.ReportAllocs()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		_ = e.NewError()
	}
}

```

test output
```text
=== RUN   TestSameError
--- PASS: TestSameError (0.00s)
PASS

Process finished with the exit code 0
```

benchmark output
```text
goos: darwin
goarch: amd64
pkg: github.com/gin-gonic/gin/binding
cpu: Intel(R) Core(TM) i7-8569U CPU @ 2.80GHz
BenchmarkSliceValidationError0
BenchmarkSliceValidationError0-8       	611569416	         1.911 ns/op	       0 B/op	       0 allocs/op
BenchmarkSliceValidationNewError0
BenchmarkSliceValidationNewError0-8    	614464096	         1.909 ns/op	       0 B/op	       0 allocs/op
BenchmarkSliceValidationError1
BenchmarkSliceValidationError1-8       	 5836453	       196.6 ns/op	      56 B/op	       3 allocs/op
BenchmarkSliceValidationNewError1
BenchmarkSliceValidationNewError1-8    	21680595	        51.82 ns/op	       8 B/op	       1 allocs/op
BenchmarkSliceValidationError10
BenchmarkSliceValidationError10-8      	  705206	      1609 ns/op	     440 B/op	      16 allocs/op
BenchmarkSliceValidationNewError10
BenchmarkSliceValidationNewError10-8   	 2226651	       530.6 ns/op	     248 B/op	       5 allocs/op
PASS

Process finished with the exit code 0
```



Patch:
diff --git a/.github/workflows/gin.yml b/.github/workflows/gin.yml
index 3fe007f..947abf9 100644
--- a/.github/workflows/gin.yml
+++ b/.github/workflows/gin.yml
@@ -22,19 +22,18 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v5
         with:
-          go-version-file: "go.mod"
-          check-latest: true
+          go-version: "^1"
       - name: Setup golangci-lint
-        uses: golangci/golangci-lint-action@v4
+        uses: golangci/golangci-lint-action@v6
         with:
-          version: v1.56.2
+          version: v1.58.1
           args: --verbose
   test:
     needs: lint
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest]
-        go: ["1.18", "1.19", "1.20", "1.21", "1.22"]
+        go: ["1.21", "1.22"]
         test-tags:
           ["", "-tags nomsgpack", '-tags "sonic avx"', "-tags go_json", "-race"]
         include:
diff --git a/.golangci.yml b/.golangci.yml
index 4a72f73..5a65972 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -55,3 +55,6 @@ issues:
     - linters:
         - revive
       path: _test\.go
+    - path: gin.go
+      linters:
+        - gci
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index e435e56..99b66fe 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -1,8 +1,7 @@
 project_name: gin
 
 builds:
-  -
-    # If true, skip the build.
+  - # If true, skip the build.
     # Useful for library projects.
     # Default is false
     skip: true
@@ -10,7 +9,7 @@ builds:
 changelog:
   # Set it to true if you wish to skip the changelog generation.
   # This may result in an empty release notes on GitHub/GitLab/Gitea.
-  skip: false
+  disable: false
 
   # Changelog generation implementation to use.
   #
@@ -21,7 +20,7 @@ changelog:
   # - `github-native`: uses the GitHub release notes generation API, disables the groups feature.
   #
   # Defaults to `git`.
-  use: git
+  use: github
 
   # Sorts the changelog by the commit's messages.
   # Could either be asc, desc or empty
@@ -38,20 +37,20 @@ changelog:
     - title: Features
       regexp: "^.*feat[(\\w)]*:+.*$"
       order: 0
-    - title: 'Bug fixes'
+    - title: "Bug fixes"
       regexp: "^.*fix[(\\w)]*:+.*$"
       order: 1
-    - title: 'Enhancements'
+    - title: "Enhancements"
       regexp: "^.*chore[(\\w)]*:+.*$"
       order: 2
+    - title: "Refactor"
+      regexp: "^.*refactor[(\\w)]*:+.*$"
+      order: 3
+    - title: "Build process updates"
+      regexp: ^.*?(build|ci)(\(.+\))??!?:.+$
+      order: 4
+    - title: "Documentation updates"
+      regexp: ^.*?docs?(\(.+\))??!?:.+$
+      order: 4
     - title: Others
       order: 999
-
-  filters:
-    # Commit messages matching the regexp listed here will be removed from
-    # the changelog
-    # Default is empty
-    exclude:
-      - '^docs'
-      - 'CICD'
-      - typo
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7968520..de47c75 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,82 @@
 # Gin ChangeLog
 
+## Gin v1.10.0
+
+### Features
+
+* feat(auth): add proxy-server authentication (#3877) (@EndlessParadox1)
+* feat(bind): ShouldBindBodyWith shortcut and change doc (#3871) (@RedCrazyGhost)
+* feat(binding): Support custom BindUnmarshaler for binding. (#3933) (@dkkb)
+* feat(binding): support override default binding implement (#3514) (@ssfyn)
+* feat(engine): Added `OptionFunc` and `With` (#3572) (@flc1125)
+* feat(logger): ability to skip logs based on user-defined logic (#3593) (@palvaneh)
+
+### Bug fixes
+
+* Revert "fix(uri): query binding bug (#3236)" (#3899) (@appleboy)
+* fix(binding): binding error while not upload file (#3819) (#3820) (@clearcodecn)
+* fix(binding): dereference pointer to struct (#3199) (@echovl)
+* fix(context): make context Value method adhere to Go standards (#3897) (@FarmerChillax)
+* fix(engine): fix unit test (#3878) (@flc1125)
+* fix(header): Allow header according to RFC 7231 (HTTP 405) (#3759) (@Crocmagnon)
+* fix(route): Add fullPath in context copy (#3784) (@KarthikReddyPuli)
+* fix(router): catch-all conflicting wildcard (#3812) (@FirePing32)
+* fix(sec): upgrade golang.org/x/crypto to 0.17.0 (#3832) (@chncaption)
+* fix(tree): correctly expand the capacity of params (#3502) (@georgijd-form3)
+* fix(uri): query binding bug (#3236) (@illiafox)
+* fix: Add pointer support for url query params (#3659) (#3666) (@omkar-foss)
+* fix: protect Context.Keys map when call Copy method (#3873) (@kingcanfish)
+ 
+### Enhancements
+
+* chore(CI): update release args (#3595) (@qloog)
+* chore(IP): add TrustedPlatform constant for Fly.io. (#3839) (@ab)
+* chore(debug): add ability to override the debugPrint statement (#2337) (@josegonzalez)
+* chore(deps): update dependencies to latest versions (#3835) (@appleboy)
+* chore(header): Add support for RFC 9512: application/yaml (#3851) (@vincentbernat)
+* chore(http): use white color for HTTP 1XX (#3741) (@viralparmarme)
+* chore(optimize): the ShouldBindUri method of the Context struct (#3911) (@1911860538)
+* chore(perf): Optimize the Copy method of the Context struct (#3859) (@1911860538)
+* chore(refactor): modify interface check way (#3855) (@demoManito)
+* chore(request): check reader if it's nil before reading (#3419) (@noahyao1024)
+* chore(security): upgrade Protobuf for CVE-2024-24786 (#3893) (@Fotkurz)
+* chore: refactor CI and update dependencies (#3848) (@appleboy)
+* chore: refactor configuration files for better readability (#3951) (@appleboy)
+* chore: update GitHub Actions configuration (#3792) (@appleboy)
+* chore: update changelog categories and improve documentation (#3917) (@appleboy)
+* chore: update dependencies to latest versions (#3694) (@appleboy)
+* chore: update external dependencies to latest versions (#3950) (@appleboy)
+* chore: update various Go dependencies to latest versions (#3901) (@appleboy)
+
+### Build process updates
+
+* build(codecov): Added a codecov configuration (#3891) (@flc1125)
+* ci(Makefile): vet command add .PHONY (#3915) (@imalasong)
+* ci(lint): update tooling and workflows for consistency (#3834) (@appleboy)
+* ci(release): refactor changelog regex patterns and exclusions (#3914) (@appleboy)
+* ci(testing): add go1.22 version (#3842) (@appleboy)
+
+### Documentation updates
+
+* docs(context): Added deprecation comments to BindWith (#3880) (@flc1125)
+* docs(middleware): comments to function `BasicAuthForProxy` (#3881) (@EndlessParadox1)
+* docs: Add document  to constant `AuthProxyUserKey` and  `BasicAuthForProxy`. (#3887) (@EndlessParadox1)
+* docs: fix typo in comment (#3868) (@testwill)
+* docs: fix typo in function documentation (#3872) (@TotomiEcio)
+* docs: remove redundant comments (#3765) (@WeiTheShinobi)
+* feat: update version constant to v1.10.0 (#3952) (@appleboy)
+
+### Others
+
+* Upgrade golang.org/x/net -> v0.13.0 (#3684) (@cpcf)
+* test(git): gitignore add develop tools (#3370) (@demoManito)
+* test(http): use constant instead of numeric literal (#3863) (@testwill)
+* test(path): Optimize unit test execution results (#3883) (@flc1125)
+* test(render): increased unit tests coverage (#3691) (@araujo88)
+
 ## Gin v1.9.1
 
-### BUG FIXES 
+### BUG FIXES
 
 * fix Request.Context() checks [#3512](https://github.com/gin-gonic/gin/pull/3512)
 
diff --git a/Makefile b/Makefile
index ebde4ee..1a7de86 100644
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,7 @@ TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | gr
 TESTTAGS ?= ""
 
 .PHONY: test
+# Run tests to verify code functionality.
 test:
 	echo "mode: count" > coverage.out
 	for d in $(TESTFOLDER); do \
@@ -30,10 +31,12 @@ test:
 	done
 
 .PHONY: fmt
+# Ensure consistent code formatting.
 fmt:
 	$(GOFMT) -w $(GOFILES)
 
 .PHONY: fmt-check
+# format (check only).
 fmt-check:
 	@diff=$$($(GOFMT) -d $(GOFILES)); \
 	if [ -n "$$diff" ]; then \
@@ -42,31 +45,37 @@ fmt-check:
 		exit 1; \
 	fi;
 
+.PHONY: vet
+# Examine packages and report suspicious constructs if any.
 vet:
 	$(GO) vet $(VETPACKAGES)
 
 .PHONY: lint
+# Inspect source code for stylistic errors or potential bugs.
 lint:
 	@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
 		$(GO) get -u golang.org/x/lint/golint; \
 	fi
 	for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
 
-.PHONY: misspell-check
-misspell-check:
+.PHONY: misspell
+# Correct commonly misspelled English words in source code.
+misspell:
 	@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
 		$(GO) get -u github.com/client9/misspell/cmd/misspell; \
 	fi
-	misspell -error $(GOFILES)
+	misspell -w $(GOFILES)
 
-.PHONY: misspell
-misspell:
+.PHONY: misspell-check
+# misspell (check only).
+misspell-check:
 	@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
 		$(GO) get -u github.com/client9/misspell/cmd/misspell; \
 	fi
-	misspell -w $(GOFILES)
+	misspell -error $(GOFILES)
 
 .PHONY: tools
+# Install tools (golint and misspell).
 tools:
 	@if [ $(GO_VERSION) -gt 15 ]; then \
 		$(GO) install golang.org/x/lint/golint@latest; \
@@ -75,3 +84,23 @@ tools:
 		$(GO) install golang.org/x/lint/golint; \
 		$(GO) install github.com/client9/misspell/cmd/misspell; \
 	fi
+
+.PHONY: help
+# Help.
+help:
+	@echo ''
+	@echo 'Usage:'
+	@echo ' make [target]'
+	@echo ''
+	@echo 'Targets:'
+	@awk '/^[a-zA-Z\-\0-9]+:/ { \
+	helpMessage = match(lastLine, /^# (.*)/); \
+		if (helpMessage) { \
+			helpCommand = substr($$1, 0, index($$1, ":")-1); \
+			helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \
+			printf " - \033[36m%-20s\033[0m %s\n", helpCommand, helpMessage; \
+		} \
+	} \
+	{ lastLine = $$0 }' $(MAKEFILE_LIST)
+
+.DEFAULT_GOAL := help
diff --git a/README.md b/README.md
index e007bf2..a595656 100644
--- a/README.md
+++ b/README.md
@@ -11,46 +11,44 @@
 [![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases)
 [![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/gin-gonic/gin)](https://www.tickgit.com/browse?repo=github.com/gin-gonic/gin)
 
-Gin is a web framework written in [Go](https://go.dev/). It features a martini-like API with performance that is up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin.
+Gin is a web framework written in [Go](https://go.dev/). It features a martini-like API with performance that is up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter).
+If you need performance and good productivity, you will love Gin.
 
-**The key features of Gin are:**
+**Gin's key features are:**
 
 - Zero allocation router
-- Fast
+- Speed
 - Middleware support
 - Crash-free
 - JSON validation
-- Routes grouping
+- Route grouping
 - Error management
-- Rendering built-in
-- Extendable
-
+- Built-in rendering
+- Extensible
 
 ## Getting started
 
 ### Prerequisites
 
-- **[Go](https://go.dev/)**: any one of the **three latest major** [releases](https://go.dev/doc/devel/release) (we test it with these).
+Gin requires [Go](https://go.dev/) version [1.21](https://go.dev/doc/devel/release#go1.21.0) or above.
 
 ### Getting Gin
 
-With [Go module](https://github.com/golang/go/wiki/Modules) support, simply add the following import
+With [Go's module support](https://go.dev/wiki/Modules#how-to-use-modules), `go [build|run|test]` automatically fetches the necessary dependencies when you add the import in your code:
 
-```
+```sh
 import "github.com/gin-gonic/gin"
 ```
 
-to your code, and then `go [build|run|test]` will automatically fetch the necessary dependencies.
-
-Otherwise, run the following Go command to install the `gin` package:
+Alternatively, use `go get`:
 
 ```sh
-$ go get -u github.com/gin-gonic/gin
+go get -u github.com/gin-gonic/gin
 ```
 
 ### Running Gin
 
-First you need to import Gin package for using Gin, one simplest example likes the follow `example.go`:
+A basic example:
 
 ```go
 package main
@@ -72,29 +70,29 @@ func main() {
 }
 ```
 
-And use the Go command to run the demo:
+To run the code, use the `go run` command, like:
 
-```
-# run example.go and visit 0.0.0.0:8080/ping on browser
+```sh
 $ go run example.go
 ```
 
-### Learn more examples
+Then visit [`0.0.0.0:8080/ping`](http://0.0.0.0:8080/ping) in your browser to see the response!
+
+### See more examples
 
 #### Quick Start
 
-Learn and practice more examples, please read the [Gin Quick Start](docs/doc.md) which includes API examples and builds tag.
+Learn and practice with the [Gin Quick Start](docs/doc.md), which includes API examples and builds tag.
 
 #### Examples
 
-A number of ready-to-run examples demonstrating various use cases of Gin on the [Gin examples](https://github.com/gin-gonic/examples) repository.
-
+A number of ready-to-run examples demonstrating various use cases of Gin are available in the [Gin examples](https://github.com/gin-gonic/examples) repository.
 
 ## Documentation
 
-See [API documentation and descriptions](https://godoc.org/github.com/gin-gonic/gin) for package.
+See the [API documentation on godoc.org](https://godoc.org/github.com/gin-gonic/gin).
 
-All documentation is available on the Gin website.
+The documentation is also available on [gin-gonic.com](https://gin-gonic.com) in several languages:
 
 - [English](https://gin-gonic.com/docs/)
 - [简体中文](https://gin-gonic.com/zh-cn/docs/)
@@ -105,15 +103,13 @@ All documentation is available on the Gin website.
 - [Turkish](https://gin-gonic.com/tr/docs/)
 - [Persian](https://gin-gonic.com/fa/docs/)
 
-### Articles about Gin
-
-A curated list of awesome Gin framework.
+### Articles
 
 - [Tutorial: Developing a RESTful API with Go and Gin](https://go.dev/doc/tutorial/web-service-gin)
 
 ## Benchmarks
 
-Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter), [see all benchmarks details](/BENCHMARKS.md).
+Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter), [see all benchmarks](/BENCHMARKS.md).
 
 | Benchmark name                 |       (1) |             (2) |          (3) |             (4) |
 | ------------------------------ | ---------:| ---------------:| ------------:| ---------------:|
@@ -153,26 +149,23 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr
 - (3): Heap Memory (B/op), lower is better
 - (4): Average Allocations per Repetition (allocs/op), lower is better
 
-
-## Middlewares
+## Middleware
 
 You can find many useful Gin middlewares at [gin-contrib](https://github.com/gin-contrib).
 
+## Uses
 
-## Users
-
-Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework.
-
-* [gorush](https://github.com/appleboy/gorush): A push notification server written in Go.
-* [fnproject](https://github.com/fnproject/fn): The container native, cloud agnostic serverless platform.
-* [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow.
-* [lura](https://github.com/luraproject/lura): Ultra performant API Gateway with middlewares.
-* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go.
-* [dkron](https://github.com/distribworks/dkron): Distributed, fault tolerant job scheduling system.
+Here are some awesome projects that are using the [Gin](https://github.com/gin-gonic/gin) web framework.
 
+- [gorush](https://github.com/appleboy/gorush): A push notification server.
+- [fnproject](https://github.com/fnproject/fn): A container native, cloud agnostic serverless platform.
+- [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Google TensorFlow.
+- [lura](https://github.com/luraproject/lura): Ultra performant API Gateway with middleware.
+- [picfit](https://github.com/thoas/picfit): An image resizing server.
+- [dkron](https://github.com/distribworks/dkron): Distributed, fault tolerant job scheduling system.
 
 ## Contributing
 
 Gin is the work of hundreds of contributors. We appreciate your help!
 
-Please see [CONTRIBUTING](CONTRIBUTING.md) for details on submitting patches and the contribution workflow.
+Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on submitting patches and the contribution workflow.
diff --git a/binding/binding.go b/binding/binding.go
index 9472387..702d0e8 100644
--- a/binding/binding.go
+++ b/binding/binding.go
@@ -84,6 +84,7 @@ var (
 	YAML          BindingBody = yamlBinding{}
 	Uri           BindingUri  = uriBinding{}
 	Header        Binding     = headerBinding{}
+	Plain         BindingBody = plainBinding{}
 	TOML          BindingBody = tomlBinding{}
 )
 
diff --git a/binding/binding_nomsgpack.go b/binding/binding_nomsgpack.go
index 552a86b..c8e6131 100644
--- a/binding/binding_nomsgpack.go
+++ b/binding/binding_nomsgpack.go
@@ -81,6 +81,7 @@ var (
 	Uri           = uriBinding{}
 	Header        = headerBinding{}
 	TOML          = tomlBinding{}
+	Plain         = plainBinding{}
 )
 
 // Default returns the appropriate Binding instance based on the HTTP method
diff --git a/binding/default_validator.go b/binding/default_validator.go
index ac43d7c..44b7a2a 100644
--- a/binding/default_validator.go
+++ b/binding/default_validator.go
@@ -5,8 +5,8 @@
 package binding
 
 import (
-	"fmt"
 	"reflect"
+	"strconv"
 	"strings"
 	"sync"
 
@@ -22,25 +22,20 @@ type SliceValidationError []error
 
 // Error concatenates all error elements in SliceValidationError into a single string separated by \n.
 func (err SliceValidationError) Error() string {
-	n := len(err)
-	switch n {
-	case 0:
+	if len(err) == 0 {
 		return ""
-	default:
-		var b strings.Builder
-		if err[0] != nil {
-			fmt.Fprintf(&b, "[%d]: %s", 0, err[0].Error())
-		}
-		if n > 1 {
-			for i := 1; i < n; i++ {
-				if err[i] != nil {
-					b.WriteString("\n")
-					fmt.Fprintf(&b, "[%d]: %s", i, err[i].Error())
-				}
+	}
+
+	var b strings.Builder
+	for i := 0; i < len(err); i++ {
+		if err[i] != nil {
+			if b.Len() > 0 {
+				b.WriteString("\n")
 			}
+			b.WriteString("[" + strconv.Itoa(i) + "]: " + err[i].Error())
 		}
-		return b.String()
 	}
+	return b.String()
 }
 
 var _ StructValidator = (*defaultValidator)(nil)
diff --git a/binding/form_mapping.go b/binding/form_mapping.go
index 77a1bde..108606f 100644
--- a/binding/form_mapping.go
+++ b/binding/form_mapping.go
@@ -165,6 +165,23 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
 	return setter.TrySet(value, field, tagValue, setOpt)
 }
 
+// BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
+type BindUnmarshaler interface {
+	// UnmarshalParam decodes and assigns a value from an form or query param.
+	UnmarshalParam(param string) error
+}
+
+// trySetCustom tries to set a custom type value
+// If the value implements the BindUnmarshaler interface, it will be used to set the value, we will return `true`
+// to skip the default value setting.
+func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
+	switch v := value.Addr().Interface().(type) {
+	case BindUnmarshaler:
+		return true, v.UnmarshalParam(val)
+	}
+	return false, nil
+}
+
 func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {
 	vs, ok := form[tagValue]
 	if !ok && !opt.isDefaultExists {
@@ -194,6 +211,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
 		if len(vs) > 0 {
 			val = vs[0]
 		}
+		if ok, err := trySetCustom(val, value); ok {
+			return ok, err
+		}
 		return true, setWithProperType(val, value, field)
 	}
 }
@@ -377,11 +397,8 @@ func setTimeDuration(val string, value reflect.Value) error {
 }
 
 func head(str, sep string) (head string, tail string) {
-	idx := strings.Index(str, sep)
-	if idx < 0 {
-		return str, ""
-	}
-	return str[:idx], str[idx+len(sep):]
+	head, tail, _ = strings.Cut(str, sep)
+	return head, tail
 }
 
 func setFormMap(ptr any, form map[string][]string) error {
diff --git a/binding/plain.go b/binding/plain.go
new file mode 100644
index 0000000..3b250bb
--- /dev/null
+++ b/binding/plain.go
@@ -0,0 +1,56 @@
+package binding
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"reflect"
+
+	"github.com/gin-gonic/gin/internal/bytesconv"
+)
+
+type plainBinding struct{}
+
+func (plainBinding) Name() string {
+	return "plain"
+}
+
+func (plainBinding) Bind(req *http.Request, obj interface{}) error {
+	all, err := io.ReadAll(req.Body)
+	if err != nil {
+		return err
+	}
+
+	return decodePlain(all, obj)
+}
+
+func (plainBinding) BindBody(body []byte, obj any) error {
+	return decodePlain(body, obj)
+}
+
+func decodePlain(data []byte, obj any) error {
+	if obj == nil {
+		return nil
+	}
+
+	v := reflect.ValueOf(obj)
+
+	for v.Kind() == reflect.Ptr {
+		if v.IsNil() {
+			return nil
+		}
+		v = v.Elem()
+	}
+
+	if v.Kind() == reflect.String {
+		v.SetString(bytesconv.BytesToString(data))
+		return nil
+	}
+
+	if _, ok := v.Interface().([]byte); ok {
+		v.SetBytes(data)
+		return nil
+	}
+
+	return fmt.Errorf("type (%T) unknown type", v)
+}
diff --git a/context.go b/context.go
index afc3c35..3525066 100644
--- a/context.go
+++ b/context.go
@@ -614,7 +614,7 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error
 	}
 	defer src.Close()
 
-	if err = os.MkdirAll(filepath.Dir(dst), 0750); err != nil {
+	if err = os.MkdirAll(filepath.Dir(dst), 0o750); err != nil {
 		return err
 	}
 
@@ -667,6 +667,11 @@ func (c *Context) BindTOML(obj any) error {
 	return c.MustBindWith(obj, binding.TOML)
 }
 
+// BindPlain is a shortcut for c.MustBindWith(obj, binding.Plain).
+func (c *Context) BindPlain(obj any) error {
+	return c.MustBindWith(obj, binding.Plain)
+}
+
 // BindHeader is a shortcut for c.MustBindWith(obj, binding.Header).
 func (c *Context) BindHeader(obj any) error {
 	return c.MustBindWith(obj, binding.Header)
@@ -732,6 +737,11 @@ func (c *Context) ShouldBindTOML(obj any) error {
 	return c.ShouldBindWith(obj, binding.TOML)
 }
 
+// ShouldBindPlain is a shortcut for c.ShouldBindWith(obj, binding.Plain).
+func (c *Context) ShouldBindPlain(obj any) error {
+	return c.ShouldBindWith(obj, binding.Plain)
+}
+
 // ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
 func (c *Context) ShouldBindHeader(obj any) error {
 	return c.ShouldBindWith(obj, binding.Header)
@@ -739,7 +749,7 @@ func (c *Context) ShouldBindHeader(obj any) error {
 
 // ShouldBindUri binds the passed struct pointer using the specified binding engine.
 func (c *Context) ShouldBindUri(obj any) error {
-	m := make(map[string][]string)
+	m := make(map[string][]string, len(c.Params))
 	for _, v := range c.Params {
 		m[v.Key] = []string{v.Value}
 	}
@@ -794,6 +804,11 @@ func (c *Context) ShouldBindBodyWithTOML(obj any) error {
 	return c.ShouldBindBodyWith(obj, binding.TOML)
 }
 
+// ShouldBindBodyWithJSON is a shortcut for c.ShouldBindBodyWith(obj, binding.JSON).
+func (c *Context) ShouldBindBodyWithPlain(obj any) error {
+	return c.ShouldBindBodyWith(obj, binding.Plain)
+}
+
 // ClientIP implements one best effort algorithm to return the real client IP.
 // It calls c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
 // If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).
diff --git a/debug.go b/debug.go
index 1761fe3..62085c5 100644
--- a/debug.go
+++ b/debug.go
@@ -10,14 +10,15 @@ import (
 	"runtime"
 	"strconv"
 	"strings"
+	"sync/atomic"
 )
 
-const ginSupportMinGoVer = 18
+const ginSupportMinGoVer = 21
 
 // IsDebugging returns true if the framework is running in debug mode.
 // Use SetMode(gin.ReleaseMode) to disable debug mode.
 func IsDebugging() bool {
-	return ginMode == debugCode
+	return atomic.LoadInt32(&ginMode) == debugCode
 }
 
 // DebugPrintRouteFunc indicates debug log output format.
@@ -77,7 +78,7 @@ func getMinVer(v string) (uint64, error) {
 
 func debugPrintWARNINGDefault() {
 	if v, e := getMinVer(runtime.Version()); e == nil && v < ginSupportMinGoVer {
-		debugPrint(`[WARNING] Now Gin requires Go 1.18+.
+		debugPrint(`[WARNING] Now Gin requires Go 1.21+.
 
 `)
 	}
diff --git a/docs/doc.md b/docs/doc.md
index 70c9f27..177c447 100644
--- a/docs/doc.md
+++ b/docs/doc.md
@@ -27,6 +27,7 @@
   - [Only Bind Query String](#only-bind-query-string)
   - [Bind Query String or Post Data](#bind-query-string-or-post-data)
   - [Bind Uri](#bind-uri)
+  - [Bind custom unmarshaler](#bind-custom-unmarshaler)
   - [Bind Header](#bind-header)
   - [Bind HTML checkboxes](#bind-html-checkboxes)
   - [Multipart/Urlencoded binding](#multiparturlencoded-binding)
@@ -899,6 +900,46 @@ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
 curl -v localhost:8088/thinkerou/not-uuid
 ```
 
+### Bind custom unmarshaler
+
+```go
+package main
+
+import (
+  "github.com/gin-gonic/gin"
+  "strings"
+)
+
+type Birthday string
+
+func (b *Birthday) UnmarshalParam(param string) error {
+  *b = Birthday(strings.Replace(param, "-", "/", -1))
+  return nil
+}
+
+func main() {
+  route := gin.Default()
+  var request struct {
+    Birthday Birthday `form:"birthday"`
+  }
+  route.GET("/test", func(ctx *gin.Context) {
+    _ = ctx.BindQuery(&request)
+    ctx.JSON(200, request.Birthday)
+  })
+  route.Run(":8088")
+}
+```
+
+Test it with:
+
+```sh
+curl 'localhost:8088/test?birthday=2000-01-01'
+```
+Result
+```sh
+"2000/01/01"
+```
+
 ### Bind Header
 
 ```go
diff --git a/fs.go b/fs.go
index f17d743..51c3db8 100644
--- a/fs.go
+++ b/fs.go
@@ -9,37 +9,43 @@ import (
 	"os"
 )
 
-type onlyFilesFS struct {
-	fs http.FileSystem
+// OnlyFilesFS implements an http.FileSystem without `Readdir` functionality.
+type OnlyFilesFS struct {
+	FileSystem http.FileSystem
 }
 
-type neuteredReaddirFile struct {
+// Open passes `Open` to the upstream implementation without `Readdir` functionality.
+func (o OnlyFilesFS) Open(name string) (http.File, error) {
+	f, err := o.FileSystem.Open(name)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return neutralizedReaddirFile{f}, nil
+}
+
+// neutralizedReaddirFile wraps http.File with a specific implementation of `Readdir`.
+type neutralizedReaddirFile struct {
 	http.File
 }
 
-// Dir returns a http.FileSystem that can be used by http.FileServer(). It is used internally
-// in router.Static().
-// if listDirectory == true, then it works the same as http.Dir() otherwise it returns
-// a filesystem that prevents http.FileServer() to list the directory files.
+// Readdir overrides the http.File default implementation and always returns nil.
+func (n neutralizedReaddirFile) Readdir(_ int) ([]os.FileInfo, error) {
+	// this disables directory listing
+	return nil, nil
+}
+
+// Dir returns an http.FileSystem that can be used by http.FileServer().
+// It is used internally in router.Static().
+// if listDirectory == true, then it works the same as http.Dir(),
+// otherwise it returns a filesystem that prevents http.FileServer() to list the directory files.
 func Dir(root string, listDirectory bool) http.FileSystem {
 	fs := http.Dir(root)
+
 	if listDirectory {
 		return fs
 	}
-	return &onlyFilesFS{fs}
-}
-
-// Open conforms to http.Filesystem.
-func (fs onlyFilesFS) Open(name string) (http.File, error) {
-	f, err := fs.fs.Open(name)
-	if err != nil {
-		return nil, err
-	}
-	return neuteredReaddirFile{f}, nil
-}
 
-// Readdir overrides the http.File default implementation.
-func (f neuteredReaddirFile) Readdir(_ int) ([]os.FileInfo, error) {
-	// this disables directory listing
-	return nil, nil
+	return &OnlyFilesFS{FileSystem: fs}
 }
diff --git a/gin.go b/gin.go
index 1633fe1..57f8c2a 100644
--- a/gin.go
+++ b/gin.go
@@ -17,6 +17,8 @@ import (
 
 	"github.com/gin-gonic/gin/internal/bytesconv"
 	"github.com/gin-gonic/gin/render"
+
+	"github.com/quic-go/quic-go/http3"
 	"golang.org/x/net/http2"
 	"golang.org/x/net/http2/h2c"
 )
@@ -316,7 +318,7 @@ func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
 	return engine
 }
 
-// With returns a new Engine instance with the provided options.
+// With returns a Engine with the configuration set in the OptionFunc.
 func (engine *Engine) With(opts ...OptionFunc) *Engine {
 	for _, opt := range opts {
 		opt(engine)
@@ -383,23 +385,6 @@ func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
 	return routes
 }
 
-// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
-// It is a shortcut for http.ListenAndServe(addr, router)
-// Note: this method will block the calling goroutine indefinitely unless an error happens.
-func (engine *Engine) Run(addr ...string) (err error) {
-	defer func() { debugPrintError(err) }()
-
-	if engine.isUnsafeTrustedProxies() {
-		debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
-			"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
-	}
-
-	address := resolveAddress(addr)
-	debugPrint("Listening and serving HTTP on %s\n", address)
-	err = http.ListenAndServe(address, engine.Handler())
-	return
-}
-
 func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) {
 	if engine.trustedProxies == nil {
 		return nil, nil
@@ -503,6 +488,23 @@ func parseIP(ip string) net.IP {
 	return parsedIP
 }
 
+// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
+// It is a shortcut for http.ListenAndServe(addr, router)
+// Note: this method will block the calling goroutine indefinitely unless an error happens.
+func (engine *Engine) Run(addr ...string) (err error) {
+	defer func() { debugPrintError(err) }()
+
+	if engine.isUnsafeTrustedProxies() {
+		debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
+			"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
+	}
+
+	address := resolveAddress(addr)
+	debugPrint("Listening and serving HTTP on %s\n", address)
+	err = http.ListenAndServe(address, engine.Handler())
+	return
+}
+
 // RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests.
 // It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
 // Note: this method will block the calling goroutine indefinitely unless an error happens.
@@ -512,7 +514,7 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
 
 	if engine.isUnsafeTrustedProxies() {
 		debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
-			"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
+			"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
 	}
 
 	err = http.ListenAndServeTLS(addr, certFile, keyFile, engine.Handler())
@@ -564,6 +566,22 @@ func (engine *Engine) RunFd(fd int) (err error) {
 	return
 }
 
+// RunQUIC attaches the router to a http.Server and starts listening and serving QUIC requests.
+// It is a shortcut for http3.ListenAndServeQUIC(addr, certFile, keyFile, router)
+// Note: this method will block the calling goroutine indefinitely unless an error happens.
+func (engine *Engine) RunQUIC(addr, certFile, keyFile string) (err error) {
+	debugPrint("Listening and serving QUIC on %s\n", addr)
+	defer func() { debugPrintError(err) }()
+
+	if engine.isUnsafeTrustedProxies() {
+		debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
+			"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
+	}
+
+	err = http3.ListenAndServeQUIC(addr, certFile, keyFile, engine.Handler())
+	return
+}
+
 // RunListener attaches the router to a http.Server and starts listening and serving HTTP requests
 // through the specified net.Listener
 func (engine *Engine) RunListener(listener net.Listener) (err error) {
diff --git a/go.mod b/go.mod
index 11ce23e..4937d2b 100644
--- a/go.mod
+++ b/go.mod
@@ -1,37 +1,48 @@
 module github.com/gin-gonic/gin
 
-go 1.20
+go 1.21.0
 
 require (
-	github.com/bytedance/sonic v1.11.0
+	github.com/bytedance/sonic v1.11.6
 	github.com/gin-contrib/sse v0.1.0
-	github.com/go-playground/validator/v10 v10.18.0
+	github.com/go-playground/validator/v10 v10.20.0
 	github.com/goccy/go-json v0.10.2
 	github.com/json-iterator/go v1.1.12
 	github.com/mattn/go-isatty v0.0.20
-	github.com/pelletier/go-toml/v2 v2.1.1
-	github.com/stretchr/testify v1.8.4
+	github.com/pelletier/go-toml/v2 v2.2.2
+	github.com/quic-go/quic-go v0.43.1
+	github.com/stretchr/testify v1.9.0
 	github.com/ugorji/go/codec v1.2.12
-	golang.org/x/net v0.21.0
-	google.golang.org/protobuf v1.33.0
+	golang.org/x/net v0.25.0
+	google.golang.org/protobuf v1.34.1
 	gopkg.in/yaml.v3 v3.0.1
 )
 
 require (
-	github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
-	github.com/chenzhuoyu/iasm v0.9.1 // indirect
+	github.com/bytedance/sonic/loader v0.1.1 // indirect
+	github.com/cloudwego/base64x v0.1.4 // indirect
+	github.com/cloudwego/iasm v0.2.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
-	github.com/klauspost/cpuid/v2 v2.2.6 // indirect
+	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
+	github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
+	github.com/klauspost/cpuid/v2 v2.0.9 // indirect
+	github.com/kr/pretty v0.3.1 // indirect
 	github.com/leodido/go-urn v1.4.0 // indirect
-	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/onsi/ginkgo/v2 v2.9.5 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/quic-go/qpack v0.4.0 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
-	golang.org/x/arch v0.7.0 // indirect
-	golang.org/x/crypto v0.19.0 // indirect
-	golang.org/x/sys v0.17.0 // indirect
-	golang.org/x/text v0.14.0 // indirect
+	go.uber.org/mock v0.4.0 // indirect
+	golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
+	golang.org/x/crypto v0.23.0 // indirect
+	golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
+	golang.org/x/mod v0.11.0 // indirect
+	golang.org/x/sys v0.20.0 // indirect
+	golang.org/x/text v0.15.0 // indirect
+	golang.org/x/tools v0.9.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 49eae34..44af4cc 100644
--- a/go.sum
+++ b/go.sum
@@ -1,14 +1,15 @@
-github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
-github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
-github.com/bytedance/sonic v1.11.0 h1:FwNNv6Vu4z2Onf1++LNzxB/QhitD8wuTdpZzMTGITWo=
-github.com/bytedance/sonic v1.11.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
-github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
-github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
-github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
-github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
-github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
-github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
-github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
+github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
+github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
+github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
+github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -16,68 +17,104 @@ github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uq
 github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
 github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
 github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
+github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U=
-github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
+github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
 github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
-github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
-github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
-github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
 github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
-github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
+github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
+github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
+github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
+github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
+github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
+github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
+github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
+github.com/quic-go/quic-go v0.43.1 h1:fLiMNfQVe9q2JvSsiXo4fXOEguXHGGl9+6gLp4RPeZQ=
+github.com/quic-go/quic-go v0.43.1/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
 github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
+go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
 golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
-golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
-golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
-golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
-golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
+golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
+golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
+golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/bytesconv/bytesconv_1.20.go b/internal/bytesconv/bytesconv.go
similarity index 97%
rename from internal/bytesconv/bytesconv_1.20.go
rename to internal/bytesconv/bytesconv.go
index 5b6040a..a02c53c 100644
--- a/internal/bytesconv/bytesconv_1.20.go
+++ b/internal/bytesconv/bytesconv.go
@@ -2,8 +2,6 @@
 // Use of this source code is governed by a MIT style
 // license that can be found in the LICENSE file.
 
-//go:build go1.20
-
 package bytesconv
 
 import (
diff --git a/internal/bytesconv/bytesconv_1.19.go b/internal/bytesconv/bytesconv_1.19.go
deleted file mode 100644
index 669c9c9..0000000
--- a/internal/bytesconv/bytesconv_1.19.go
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright 2020 Gin Core Team. All rights reserved.
-// Use of this source code is governed by a MIT style
-// license that can be found in the LICENSE file.
-
-//go:build !go1.20
-
-package bytesconv
-
-import (
-	"unsafe"
-)
-
-// StringToBytes converts string to byte slice without a memory allocation.
-func StringToBytes(s string) []byte {
-	return *(*[]byte)(unsafe.Pointer(
-		&struct {
-			string
-			Cap int
-		}{s, len(s)},
-	))
-}
-
-// BytesToString converts byte slice to string without a memory allocation.
-func BytesToString(b []byte) string {
-	return *(*string)(unsafe.Pointer(&b))
-}
diff --git a/mode.go b/mode.go
index fd26d90..13aa3be 100644
--- a/mode.go
+++ b/mode.go
@@ -8,6 +8,7 @@ import (
 	"flag"
 	"io"
 	"os"
+	"sync/atomic"
 
 	"github.com/gin-gonic/gin/binding"
 )
@@ -43,10 +44,8 @@ var DefaultWriter io.Writer = os.Stdout
 // DefaultErrorWriter is the default io.Writer used by Gin to debug errors
 var DefaultErrorWriter io.Writer = os.Stderr
 
-var (
-	ginMode  = debugCode
-	modeName = DebugMode
-)
+var ginMode int32 = debugCode
+var modeName atomic.Value
 
 func init() {
 	mode := os.Getenv(EnvGinMode)
@@ -64,17 +63,16 @@ func SetMode(value string) {
 	}
 
 	switch value {
-	case DebugMode:
-		ginMode = debugCode
+	case DebugMode, "":
+		atomic.StoreInt32(&ginMode, debugCode)
 	case ReleaseMode:
-		ginMode = releaseCode
+		atomic.StoreInt32(&ginMode, releaseCode)
 	case TestMode:
-		ginMode = testCode
+		atomic.StoreInt32(&ginMode, testCode)
 	default:
 		panic("gin mode unknown: " + value + " (available mode: debug release test)")
 	}
-
-	modeName = value
+	modeName.Store(value)
 }
 
 // DisableBindValidation closes the default validator.
@@ -96,5 +94,5 @@ func EnableJsonDecoderDisallowUnknownFields() {
 
 // Mode returns current gin mode.
 func Mode() string {
-	return modeName
+	return modeName.Load().(string)
 }
diff --git a/routergroup.go b/routergroup.go
index c833fe8..b2540ec 100644
--- a/routergroup.go
+++ b/routergroup.go
@@ -218,7 +218,7 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS
 	fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
 
 	return func(c *Context) {
-		if _, noListing := fs.(*onlyFilesFS); noListing {
+		if _, noListing := fs.(*OnlyFilesFS); noListing {
 			c.Writer.WriteHeader(http.StatusNotFound)
 		}
 
diff --git a/version.go b/version.go
index 85462e5..93ad965 100644
--- a/version.go
+++ b/version.go
@@ -5,4 +5,4 @@
 package gin
 
 // Version is the current gin framework's version.
-const Version = "v1.9.1"
+const Version = "v1.10.0"


Test patch:
diff --git a/binding/binding_test.go b/binding/binding_test.go
index feb8eed..c59e5e9 100644
--- a/binding/binding_test.go
+++ b/binding/binding_test.go
@@ -1342,6 +1342,46 @@ func (h hook) Read([]byte) (int, error) {
 	return 0, errors.New("error")
 }
 
+type failRead struct{}
+
+func (f *failRead) Read(b []byte) (n int, err error) {
+	return 0, errors.New("my fail")
+}
+
+func (f *failRead) Close() error {
+	return nil
+}
+
+func TestPlainBinding(t *testing.T) {
+	p := Plain
+	assert.Equal(t, "plain", p.Name())
+
+	var s string
+	req := requestWithBody("POST", "/", "test string")
+	assert.NoError(t, p.Bind(req, &s))
+	assert.Equal(t, s, "test string")
+
+	var bs []byte
+	req = requestWithBody("POST", "/", "test []byte")
+	assert.NoError(t, p.Bind(req, &bs))
+	assert.Equal(t, bs, []byte("test []byte"))
+
+	var i int
+	req = requestWithBody("POST", "/", "test fail")
+	assert.Error(t, p.Bind(req, &i))
+
+	req = requestWithBody("POST", "/", "")
+	req.Body = &failRead{}
+	assert.Error(t, p.Bind(req, &s))
+
+	req = requestWithBody("POST", "/", "")
+	assert.Nil(t, p.Bind(req, nil))
+
+	var ptr *string
+	req = requestWithBody("POST", "/", "")
+	assert.Nil(t, p.Bind(req, ptr))
+}
+
 func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
 	assert.Equal(t, name, b.Name())
 
diff --git a/binding/default_validator_benchmark_test.go b/binding/default_validator_benchmark_test.go
index 9292e2a..4454741 100644
--- a/binding/default_validator_benchmark_test.go
+++ b/binding/default_validator_benchmark_test.go
@@ -12,11 +12,15 @@ import (
 
 func BenchmarkSliceValidationError(b *testing.B) {
 	const size int = 100
+	e := make(SliceValidationError, size)
+	for j := 0; j < size; j++ {
+		e[j] = errors.New(strconv.Itoa(j))
+	}
+
+	b.ReportAllocs()
+	b.ResetTimer()
+
 	for i := 0; i < b.N; i++ {
-		e := make(SliceValidationError, size)
-		for j := 0; j < size; j++ {
-			e[j] = errors.New(strconv.Itoa(j))
-		}
 		if len(e.Error()) == 0 {
 			b.Errorf("error")
 		}
diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go
index 16527eb..ed01a08 100644
--- a/binding/form_mapping_test.go
+++ b/binding/form_mapping_test.go
@@ -5,8 +5,11 @@
 package binding
 
 import (
+	"fmt"
 	"mime/multipart"
 	"reflect"
+	"strconv"
+	"strings"
 	"testing"
 	"time"
 
@@ -323,3 +326,99 @@ func TestMappingIgnoredCircularRef(t *testing.T) {
 	err := mappingByPtr(&s, formSource{}, "form")
 	assert.NoError(t, err)
 }
+
+type customUnmarshalParamHex int
+
+func (f *customUnmarshalParamHex) UnmarshalParam(param string) error {
+	v, err := strconv.ParseInt(param, 16, 64)
+	if err != nil {
+		return err
+	}
+	*f = customUnmarshalParamHex(v)
+	return nil
+}
+
+func TestMappingCustomUnmarshalParamHexWithFormTag(t *testing.T) {
+	var s struct {
+		Foo customUnmarshalParamHex `form:"foo"`
+	}
+	err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "form")
+	assert.NoError(t, err)
+
+	assert.EqualValues(t, 245, s.Foo)
+}
+
+func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) {
+	var s struct {
+		Foo customUnmarshalParamHex `uri:"foo"`
+	}
+	err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "uri")
+	assert.NoError(t, err)
+
+	assert.EqualValues(t, 245, s.Foo)
+}
+
+type customUnmarshalParamType struct {
+	Protocol string
+	Path     string
+	Name     string
+}
+
+func (f *customUnmarshalParamType) UnmarshalParam(param string) error {
+	parts := strings.Split(param, ":")
+	if len(parts) != 3 {
+		return fmt.Errorf("invalid format")
+	}
+	f.Protocol = parts[0]
+	f.Path = parts[1]
+	f.Name = parts[2]
+	return nil
+}
+
+func TestMappingCustomStructTypeWithFormTag(t *testing.T) {
+	var s struct {
+		FileData customUnmarshalParamType `form:"data"`
+	}
+	err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
+	assert.NoError(t, err)
+
+	assert.EqualValues(t, "file", s.FileData.Protocol)
+	assert.EqualValues(t, "/foo", s.FileData.Path)
+	assert.EqualValues(t, "happiness", s.FileData.Name)
+}
+
+func TestMappingCustomStructTypeWithURITag(t *testing.T) {
+	var s struct {
+		FileData customUnmarshalParamType `uri:"data"`
+	}
+	err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
+	assert.NoError(t, err)
+
+	assert.EqualValues(t, "file", s.FileData.Protocol)
+	assert.EqualValues(t, "/foo", s.FileData.Path)
+	assert.EqualValues(t, "happiness", s.FileData.Name)
+}
+
+func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
+	var s struct {
+		FileData *customUnmarshalParamType `form:"data"`
+	}
+	err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
+	assert.NoError(t, err)
+
+	assert.EqualValues(t, "file", s.FileData.Protocol)
+	assert.EqualValues(t, "/foo", s.FileData.Path)
+	assert.EqualValues(t, "happiness", s.FileData.Name)
+}
+
+func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
+	var s struct {
+		FileData *customUnmarshalParamType `uri:"data"`
+	}
+	err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
+	assert.NoError(t, err)
+
+	assert.EqualValues(t, "file", s.FileData.Protocol)
+	assert.EqualValues(t, "/foo", s.FileData.Path)
+	assert.EqualValues(t, "happiness", s.FileData.Name)
+}
diff --git a/context_1.18_test.go b/context_1.18_test.go
deleted file mode 100644
index 6118bea..0000000
--- a/context_1.18_test.go
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright 2021 Gin Core Team. All rights reserved.
-// Use of this source code is governed by a MIT style
-// license that can be found in the LICENSE file.
-
-//go:build !go1.19
-
-package gin
-
-import (
-	"bytes"
-	"mime/multipart"
-	"net/http"
-	"net/http/httptest"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func TestContextFormFileFailed18(t *testing.T) {
-	buf := new(bytes.Buffer)
-	mw := multipart.NewWriter(buf)
-	defer func(mw *multipart.Writer) {
-		err := mw.Close()
-		if err != nil {
-			assert.Error(t, err)
-		}
-	}(mw)
-	c, _ := CreateTestContext(httptest.NewRecorder())
-	c.Request, _ = http.NewRequest("POST", "/", nil)
-	c.Request.Header.Set("Content-Type", mw.FormDataContentType())
-	c.engine.MaxMultipartMemory = 8 << 20
-	assert.Panics(t, func() {
-		f, err := c.FormFile("file")
-		assert.Error(t, err)
-		assert.Nil(t, f)
-	})
-}
diff --git a/context_1.19_test.go b/context_1.19_test.go
deleted file mode 100644
index dd75325..0000000
--- a/context_1.19_test.go
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright 2022 Gin Core Team. All rights reserved.
-// Use of this source code is governed by a MIT style
-// license that can be found in the LICENSE file.
-
-//go:build go1.19
-
-package gin
-
-import (
-	"bytes"
-	"mime/multipart"
-	"net/http"
-	"net/http/httptest"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func TestContextFormFileFailed19(t *testing.T) {
-	buf := new(bytes.Buffer)
-	mw := multipart.NewWriter(buf)
-	mw.Close()
-	c, _ := CreateTestContext(httptest.NewRecorder())
-	c.Request, _ = http.NewRequest("POST", "/", nil)
-	c.Request.Header.Set("Content-Type", mw.FormDataContentType())
-	c.engine.MaxMultipartMemory = 8 << 20
-	f, err := c.FormFile("file")
-	assert.Error(t, err)
-	assert.Nil(t, f)
-}
diff --git a/context_test.go b/context_test.go
index e9bbae5..36d6e34 100644
--- a/context_test.go
+++ b/context_test.go
@@ -90,6 +90,19 @@ func TestContextFormFile(t *testing.T) {
 	assert.NoError(t, c.SaveUploadedFile(f, "test"))
 }
 
+func TestContextFormFileFailed(t *testing.T) {
+	buf := new(bytes.Buffer)
+	mw := multipart.NewWriter(buf)
+	mw.Close()
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	c.Request, _ = http.NewRequest("POST", "/", nil)
+	c.Request.Header.Set("Content-Type", mw.FormDataContentType())
+	c.engine.MaxMultipartMemory = 8 << 20
+	f, err := c.FormFile("file")
+	assert.Error(t, err)
+	assert.Nil(t, f)
+}
+
 func TestContextMultipartForm(t *testing.T) {
 	buf := new(bytes.Buffer)
 	mw := multipart.NewWriter(buf)
@@ -1657,6 +1670,31 @@ func TestContextBindWithXML(t *testing.T) {
 	assert.Equal(t, 0, w.Body.Len())
 }
 
+func TestContextBindPlain(t *testing.T) {
+
+	// string
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+	c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(`test string`))
+	c.Request.Header.Add("Content-Type", MIMEPlain)
+
+	var s string
+
+	assert.NoError(t, c.BindPlain(&s))
+	assert.Equal(t, "test string", s)
+	assert.Equal(t, 0, w.Body.Len())
+
+	// []byte
+	c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(`test []byte`))
+	c.Request.Header.Add("Content-Type", MIMEPlain)
+
+	var bs []byte
+
+	assert.NoError(t, c.BindPlain(&bs))
+	assert.Equal(t, []byte("test []byte"), bs)
+	assert.Equal(t, 0, w.Body.Len())
+}
+
 func TestContextBindHeader(t *testing.T) {
 	w := httptest.NewRecorder()
 	c, _ := CreateTestContext(w)
@@ -1803,6 +1841,31 @@ func TestContextShouldBindWithXML(t *testing.T) {
 	assert.Equal(t, 0, w.Body.Len())
 }
 
+func TestContextShouldBindPlain(t *testing.T) {
+	// string
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+	c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(`test string`))
+	c.Request.Header.Add("Content-Type", MIMEPlain)
+
+	var s string
+
+	assert.NoError(t, c.ShouldBindPlain(&s))
+	assert.Equal(t, "test string", s)
+	assert.Equal(t, 0, w.Body.Len())
+	// []byte
+
+	c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(`test []byte`))
+	c.Request.Header.Add("Content-Type", MIMEPlain)
+
+	var bs []byte
+
+	assert.NoError(t, c.ShouldBindPlain(&bs))
+	assert.Equal(t, []byte("test []byte"), bs)
+	assert.Equal(t, 0, w.Body.Len())
+
+}
+
 func TestContextShouldBindHeader(t *testing.T) {
 	w := httptest.NewRecorder()
 	c, _ := CreateTestContext(w)
@@ -2234,6 +2297,80 @@ func TestContextShouldBindBodyWithTOML(t *testing.T) {
 	}
 }
 
+func TestContextShouldBindBodyWithPlain(t *testing.T) {
+	for _, tt := range []struct {
+		name        string
+		bindingBody binding.BindingBody
+		body        string
+	}{
+		{
+			name:        " JSON & JSON-BODY ",
+			bindingBody: binding.JSON,
+			body:        `{"foo":"FOO"}`,
+		},
+		{
+			name:        " JSON & XML-BODY ",
+			bindingBody: binding.XML,
+			body: `<?xml version="1.0" encoding="UTF-8"?>
+<root>
+<foo>FOO</foo>
+</root>`,
+		},
+		{
+			name:        " JSON & YAML-BODY ",
+			bindingBody: binding.YAML,
+			body:        `foo: FOO`,
+		},
+		{
+			name:        " JSON & TOM-BODY ",
+			bindingBody: binding.TOML,
+			body:        `foo=FOO`,
+		},
+		{
+			name:        " JSON & Plain-BODY ",
+			bindingBody: binding.Plain,
+			body:        `foo=FOO`,
+		},
+	} {
+		t.Logf("testing: %s", tt.name)
+
+		w := httptest.NewRecorder()
+		c, _ := CreateTestContext(w)
+
+		c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(tt.body))
+
+		type typeJSON struct {
+			Foo string `json:"foo" binding:"required"`
+		}
+		objJSON := typeJSON{}
+
+		if tt.bindingBody == binding.Plain {
+			body := ""
+			assert.NoError(t, c.ShouldBindBodyWithPlain(&body))
+			assert.Equal(t, body, "foo=FOO")
+		}
+
+		if tt.bindingBody == binding.JSON {
+			assert.NoError(t, c.ShouldBindBodyWithJSON(&objJSON))
+			assert.Equal(t, typeJSON{"FOO"}, objJSON)
+		}
+
+		if tt.bindingBody == binding.XML {
+			assert.Error(t, c.ShouldBindBodyWithJSON(&objJSON))
+			assert.Equal(t, typeJSON{}, objJSON)
+		}
+
+		if tt.bindingBody == binding.YAML {
+			assert.Error(t, c.ShouldBindBodyWithJSON(&objJSON))
+			assert.Equal(t, typeJSON{}, objJSON)
+		}
+
+		if tt.bindingBody == binding.TOML {
+			assert.Error(t, c.ShouldBindBodyWithJSON(&objJSON))
+			assert.Equal(t, typeJSON{}, objJSON)
+		}
+	}
+}
 func TestContextGolangContext(t *testing.T) {
 	c, _ := CreateTestContext(httptest.NewRecorder())
 	c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
diff --git a/debug_test.go b/debug_test.go
index 2d5e9a5..1e57668 100644
--- a/debug_test.go
+++ b/debug_test.go
@@ -104,7 +104,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
 	})
 	m, e := getMinVer(runtime.Version())
 	if e == nil && m < ginSupportMinGoVer {
-		assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.18+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
+		assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.21+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
 	} else {
 		assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
 	}
diff --git a/fs_test.go b/fs_test.go
new file mode 100644
index 0000000..a1690cd
--- /dev/null
+++ b/fs_test.go
@@ -0,0 +1,71 @@
+package gin
+
+import (
+	"errors"
+	"net/http"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type mockFileSystem struct {
+	open func(name string) (http.File, error)
+}
+
+func (m *mockFileSystem) Open(name string) (http.File, error) {
+	return m.open(name)
+}
+
+func TestOnlyFilesFS_Open(t *testing.T) {
+	var testFile *os.File
+	mockFS := &mockFileSystem{
+		open: func(name string) (http.File, error) {
+			return testFile, nil
+		},
+	}
+	fs := &OnlyFilesFS{FileSystem: mockFS}
+
+	file, err := fs.Open("foo")
+
+	assert.NoError(t, err)
+	assert.Equal(t, testFile, file.(neutralizedReaddirFile).File)
+}
+
+func TestOnlyFilesFS_Open_err(t *testing.T) {
+	testError := errors.New("mock")
+	mockFS := &mockFileSystem{
+		open: func(_ string) (http.File, error) {
+			return nil, testError
+		},
+	}
+	fs := &OnlyFilesFS{FileSystem: mockFS}
+
+	file, err := fs.Open("foo")
+
+	assert.ErrorIs(t, err, testError)
+	assert.Nil(t, file)
+}
+
+func Test_neuteredReaddirFile_Readdir(t *testing.T) {
+	n := neutralizedReaddirFile{}
+
+	res, err := n.Readdir(0)
+
+	assert.NoError(t, err)
+	assert.Nil(t, res)
+}
+
+func TestDir_listDirectory(t *testing.T) {
+	testRoot := "foo"
+	fs := Dir(testRoot, true)
+
+	assert.Equal(t, http.Dir(testRoot), fs)
+}
+
+func TestDir(t *testing.T) {
+	testRoot := "foo"
+	fs := Dir(testRoot, false)
+
+	assert.Equal(t, &OnlyFilesFS{FileSystem: http.Dir(testRoot)}, fs)
+}
diff --git a/gin_integration_test.go b/gin_integration_test.go
index 02b9622..2125df9 100644
--- a/gin_integration_test.go
+++ b/gin_integration_test.go
@@ -274,6 +274,22 @@ func TestBadUnixSocket(t *testing.T) {
 	assert.Error(t, router.RunUnix("#/tmp/unix_unit_test"))
 }
 
+func TestRunQUIC(t *testing.T) {
+	router := New()
+	go func() {
+		router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
+
+		assert.NoError(t, router.RunQUIC(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
+	}()
+
+	// have to wait for the goroutine to start and run the server
+	// otherwise the main thread will complete
+	time.Sleep(5 * time.Millisecond)
+
+	assert.Error(t, router.RunQUIC(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
+	testRequest(t, "https://localhost:8443/example")
+}
+
 func TestFileDescriptor(t *testing.T) {
 	router := New()
 
diff --git a/gin_test.go b/gin_test.go
index 4550a7e..e68f1ce 100644
--- a/gin_test.go
+++ b/gin_test.go
@@ -14,6 +14,7 @@ import (
 	"net/http/httptest"
 	"reflect"
 	"strconv"
+	"strings"
 	"sync/atomic"
 	"testing"
 	"time"
@@ -730,3 +731,26 @@ func TestWithOptionFunc(t *testing.T) {
 	assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest1"})
 	assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest2"})
 }
+
+type Birthday string
+
+func (b *Birthday) UnmarshalParam(param string) error {
+	*b = Birthday(strings.Replace(param, "-", "/", -1))
+	return nil
+}
+
+func TestCustomUnmarshalStruct(t *testing.T) {
+	route := Default()
+	var request struct {
+		Birthday Birthday `form:"birthday"`
+	}
+	route.GET("/test", func(ctx *Context) {
+		_ = ctx.BindQuery(&request)
+		ctx.JSON(200, request.Birthday)
+	})
+	req := httptest.NewRequest("GET", "/test?birthday=2000-01-01", nil)
+	w := httptest.NewRecorder()
+	route.ServeHTTP(w, req)
+	assert.Equal(t, 200, w.Code)
+	assert.Equal(t, `"2000/01/01"`, w.Body.String())
+}
diff --git a/mode_test.go b/mode_test.go
index 2407f46..be03a9d 100644
--- a/mode_test.go
+++ b/mode_test.go
@@ -5,8 +5,8 @@
 package gin
 
 import (
-	"flag"
 	"os"
+	"sync/atomic"
 	"testing"
 
 	"github.com/gin-gonic/gin/binding"
@@ -18,31 +18,24 @@ func init() {
 }
 
 func TestSetMode(t *testing.T) {
-	assert.Equal(t, testCode, ginMode)
+	assert.Equal(t, int32(testCode), atomic.LoadInt32(&ginMode))
 	assert.Equal(t, TestMode, Mode())
 	os.Unsetenv(EnvGinMode)
 
 	SetMode("")
-	assert.Equal(t, testCode, ginMode)
+	assert.Equal(t, int32(testCode), atomic.LoadInt32(&ginMode))
 	assert.Equal(t, TestMode, Mode())
 
-	tmp := flag.CommandLine
-	flag.CommandLine = flag.NewFlagSet("", flag.ContinueOnError)
-	SetMode("")
-	assert.Equal(t, debugCode, ginMode)
-	assert.Equal(t, DebugMode, Mode())
-	flag.CommandLine = tmp
-
 	SetMode(DebugMode)
-	assert.Equal(t, debugCode, ginMode)
+	assert.Equal(t, int32(debugCode), atomic.LoadInt32(&ginMode))
 	assert.Equal(t, DebugMode, Mode())
 
 	SetMode(ReleaseMode)
-	assert.Equal(t, releaseCode, ginMode)
+	assert.Equal(t, int32(releaseCode), atomic.LoadInt32(&ginMode))
 	assert.Equal(t, ReleaseMode, Mode())
 
 	SetMode(TestMode)
-	assert.Equal(t, testCode, ginMode)
+	assert.Equal(t, int32(testCode), atomic.LoadInt32(&ginMode))
 	assert.Equal(t, TestMode, Mode())
 
 	assert.Panics(t, func() { SetMode("unknown") })