Compare commits

...

8 Commits

Author SHA1 Message Date
unbyte
b2ddea48e0
Merge 96f63d68d3086cd0883f77ac39c5cc73aedd530d into 58135f06cf206a9ff713eb14150ef04a05b031d4 2025-11-15 22:35:04 +08:00
AtoriUzawa
58135f06cf
docs(context): add example comments for ShouldBind* methods (#4428)
- Added detailed example for ShouldBindJSON
- Added consistent descriptive comments for ShouldBindXML, ShouldBindQuery, ShouldBindYAML, ShouldBindTOML, ShouldBindPlain, ShouldBindHeader, ShouldBindUri
- Makes binding method usage clearer for new users
2025-11-15 19:46:45 +08:00
efcking
a85ef5ce4d
refactor: use b.Loop() to simplify the code and improve performance (#4432)
Signed-off-by: efcking <efcking@outlook.com>
2025-11-15 19:22:18 +08:00
Bo-Yi Wu
fb27ef26c2
ci(lint): refactor test assertions and linter configuration (#4436)
- Update golangci-lint GitHub Action version from v2.1.6 to v2.6
- Remove the gci formatter and exclusions for third_party, builtin, and examples from the linter config
- Fix argument order for assert.EqualValues and assert.Exactly in context tests for clarity
- Refactor integration tests to build response strings using strings.Builder instead of direct concatenation for improved performance and readability

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-11-15 19:21:42 +08:00
Bo-Yi Wu
96f63d68d3
Merge branch 'master' into fix-cors 2025-05-11 22:36:26 +08:00
unbyte
5f94d6f3e8
Merge branch 'master' into fix-cors 2021-12-28 15:15:08 +08:00
unbyte
51ef7a6a10
Merge branch 'master' into fix-cors 2021-11-02 15:12:48 +08:00
Helios
22b88e0ed1 AutoRedirect API to handle before auto redirection 2020-08-23 00:41:04 +08:00
12 changed files with 143 additions and 24 deletions

View File

@ -26,7 +26,7 @@ jobs:
- name: Setup golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.1.6
version: v2.6
args: --verbose
test:
needs: lint

View File

@ -68,7 +68,6 @@ linters:
- examples$
formatters:
enable:
- gci
- gofmt
- gofumpt
- goimports
@ -80,7 +79,4 @@ formatters:
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
- gin.go

View File

@ -18,9 +18,8 @@ func BenchmarkSliceValidationError(b *testing.B) {
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
if len(e.Error()) == 0 {
b.Errorf("error")
}

View File

@ -31,7 +31,7 @@ type structFull struct {
func BenchmarkMapFormFull(b *testing.B) {
var s structFull
for i := 0; i < b.N; i++ {
for b.Loop() {
err := mapForm(&s, form)
if err != nil {
b.Fatalf("Error on a form mapping")
@ -54,7 +54,7 @@ type structName struct {
func BenchmarkMapFormName(b *testing.B) {
var s structName
for i := 0; i < b.N; i++ {
for b.Loop() {
err := mapForm(&s, form)
if err != nil {
b.Fatalf("Error on a form mapping")

View File

@ -830,41 +830,71 @@ func (c *Context) ShouldBind(obj any) error {
}
// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON).
//
// Example:
//
// POST /user
// Content-Type: application/json
//
// Request Body:
// {
// "name": "Manu",
// "age": 20
// }
//
// type User struct {
// Name string `json:"name"`
// Age int `json:"age"`
// }
//
// var user User
// if err := c.ShouldBindJSON(&user); err != nil {
// c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
// return
// }
// c.JSON(http.StatusOK, user)
func (c *Context) ShouldBindJSON(obj any) error {
return c.ShouldBindWith(obj, binding.JSON)
}
// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).
// It works like ShouldBindJSON but binds the request body as XML data.
func (c *Context) ShouldBindXML(obj any) error {
return c.ShouldBindWith(obj, binding.XML)
}
// ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query).
// It works like ShouldBindJSON but binds query parameters from the URL.
func (c *Context) ShouldBindQuery(obj any) error {
return c.ShouldBindWith(obj, binding.Query)
}
// ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML).
// It works like ShouldBindJSON but binds the request body as YAML data.
func (c *Context) ShouldBindYAML(obj any) error {
return c.ShouldBindWith(obj, binding.YAML)
}
// ShouldBindTOML is a shortcut for c.ShouldBindWith(obj, binding.TOML).
// It works like ShouldBindJSON but binds the request body as TOML data.
func (c *Context) ShouldBindTOML(obj any) error {
return c.ShouldBindWith(obj, binding.TOML)
}
// ShouldBindPlain is a shortcut for c.ShouldBindWith(obj, binding.Plain).
// It works like ShouldBindJSON but binds plain text data from the request body.
func (c *Context) ShouldBindPlain(obj any) error {
return c.ShouldBindWith(obj, binding.Plain)
}
// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
// It works like ShouldBindJSON but binds values from HTTP headers.
func (c *Context) ShouldBindHeader(obj any) error {
return c.ShouldBindWith(obj, binding.Header)
}
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
// It works like ShouldBindJSON but binds parameters from the URI.
func (c *Context) ShouldBindUri(obj any) error {
m := make(map[string][]string, len(c.Params))
for _, v := range c.Params {

View File

@ -292,7 +292,7 @@ func TestContextReset(t *testing.T) {
assert.Empty(t, c.Errors.Errors())
assert.Empty(t, c.Errors.ByType(ErrorTypeAny))
assert.Empty(t, c.Params)
assert.EqualValues(t, c.index, -1)
assert.EqualValues(t, -1, c.index)
assert.Equal(t, c.Writer.(*responseWriter), &c.writermem)
}
@ -384,7 +384,7 @@ func TestContextSetGetValues(t *testing.T) {
c.Set("intInterface", a)
assert.Exactly(t, "this is a string", c.MustGet("string").(string))
assert.Exactly(t, c.MustGet("int32").(int32), int32(-42))
assert.Exactly(t, int32(-42), c.MustGet("int32").(int32))
assert.Exactly(t, int64(42424242424242), c.MustGet("int64").(int64))
assert.Exactly(t, uint64(42), c.MustGet("uint64").(uint64))
assert.InDelta(t, float32(4.2), c.MustGet("float32").(float32), 0.01)

22
gin.go
View File

@ -170,8 +170,10 @@ type Engine struct {
FuncMap template.FuncMap
allNoRoute HandlersChain
allNoMethod HandlersChain
allAutoRedirect HandlersChain
noRoute HandlersChain
noMethod HandlersChain
autoRedirect HandlersChain
pool sync.Pool
trees methodTrees
maxParams uint16
@ -324,6 +326,13 @@ func (engine *Engine) NoMethod(handlers ...HandlerFunc) {
engine.rebuild405Handlers()
}
// AutoRedirect sets the handlers called when auto redirected
// (RedirectTrailingSlash and RedirectFixedPath)
func (engine *Engine) AutoRedirect(handlers ...HandlerFunc) {
engine.autoRedirect = handlers
engine.rebuildAutoRedirectHandlers()
}
// Use attaches a global middleware to the router. i.e. the middleware attached through Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
@ -331,6 +340,7 @@ func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
engine.rebuildAutoRedirectHandlers()
return engine
}
@ -351,6 +361,10 @@ func (engine *Engine) rebuild405Handlers() {
engine.allNoMethod = engine.combineHandlers(engine.noMethod)
}
func (engine *Engine) rebuildAutoRedirectHandlers() {
engine.allAutoRedirect = engine.combineHandlers(engine.autoRedirect)
}
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
assert1(path[0] == '/', "path must begin with '/'")
assert1(method != "", "HTTP method can not be empty")
@ -692,6 +706,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
return
}
if httpMethod != http.MethodConnect && rPath != "/" {
c.handlers = engine.allAutoRedirect
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
@ -776,13 +791,14 @@ func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
func redirectRequest(c *Context) {
req := c.Request
rPath := req.URL.Path
rURL := req.URL.String()
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
if req.Method != http.MethodGet {
code = http.StatusTemporaryRedirect
}
c.Next()
rPath := req.URL.Path
rURL := req.URL.String()
debugPrint("redirecting request %d: %s --> %s", code, rPath, rURL)
http.Redirect(c.Writer, req, rURL, code)
c.writermem.WriteHeaderNow()

View File

@ -16,6 +16,7 @@ import (
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
@ -261,10 +262,11 @@ func TestUnixSocket(t *testing.T) {
fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
var response string
var responseBuilder strings.Builder
for scanner.Scan() {
response += scanner.Text()
responseBuilder.WriteString(scanner.Text())
}
response := responseBuilder.String()
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}
@ -322,10 +324,11 @@ func TestFileDescriptor(t *testing.T) {
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
var response string
var responseBuilder strings.Builder
for scanner.Scan() {
response += scanner.Text()
responseBuilder.WriteString(scanner.Text())
}
response := responseBuilder.String()
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}
@ -354,10 +357,11 @@ func TestListener(t *testing.T) {
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
var response string
var responseBuilder strings.Builder
for scanner.Scan() {
response += scanner.Text()
responseBuilder.WriteString(scanner.Text())
}
response := responseBuilder.String()
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}

View File

@ -578,6 +578,59 @@ func TestNoMethodWithGlobalHandlers(t *testing.T) {
compareFunc(t, router.allNoMethod[2], middleware0)
}
func TestAutoRedirectWithoutGlobalHandlers(t *testing.T) {
var middleware0 HandlerFunc = func(c *Context) {}
var middleware1 HandlerFunc = func(c *Context) {}
router := New()
router.AutoRedirect(middleware0)
assert.Nil(t, router.Handlers)
assert.Len(t, router.autoRedirect, 1)
assert.Len(t, router.allAutoRedirect, 1)
compareFunc(t, router.autoRedirect[0], middleware0)
compareFunc(t, router.allAutoRedirect[0], middleware0)
router.AutoRedirect(middleware1, middleware0)
assert.Len(t, router.autoRedirect, 2)
assert.Len(t, router.allAutoRedirect, 2)
compareFunc(t, router.autoRedirect[0], middleware1)
compareFunc(t, router.allAutoRedirect[0], middleware1)
compareFunc(t, router.autoRedirect[1], middleware0)
compareFunc(t, router.allAutoRedirect[1], middleware0)
}
func TestAutoRedirectWithGlobalHandlers(t *testing.T) {
var middleware0 HandlerFunc = func(c *Context) {}
var middleware1 HandlerFunc = func(c *Context) {}
var middleware2 HandlerFunc = func(c *Context) {}
router := New()
router.Use(middleware2)
router.AutoRedirect(middleware0)
assert.Len(t, router.allAutoRedirect, 2)
assert.Len(t, router.Handlers, 1)
assert.Len(t, router.autoRedirect, 1)
compareFunc(t, router.Handlers[0], middleware2)
compareFunc(t, router.autoRedirect[0], middleware0)
compareFunc(t, router.allAutoRedirect[0], middleware2)
compareFunc(t, router.allAutoRedirect[1], middleware0)
router.Use(middleware1)
assert.Len(t, router.allAutoRedirect, 3)
assert.Len(t, router.Handlers, 2)
assert.Len(t, router.autoRedirect, 1)
compareFunc(t, router.Handlers[0], middleware2)
compareFunc(t, router.Handlers[1], middleware1)
compareFunc(t, router.autoRedirect[0], middleware0)
compareFunc(t, router.allAutoRedirect[0], middleware2)
compareFunc(t, router.allAutoRedirect[1], middleware1)
compareFunc(t, router.allAutoRedirect[2], middleware0)
}
func compareFunc(t *testing.T, a, b any) {
sf1 := reflect.ValueOf(a)
sf2 := reflect.ValueOf(b)

View File

@ -94,7 +94,7 @@ func TestPathCleanMallocs(t *testing.T) {
func BenchmarkPathClean(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
for _, test := range cleanTests {
cleanPath(test.path)
}
@ -134,10 +134,10 @@ func TestPathCleanLong(t *testing.T) {
func BenchmarkPathCleanLong(b *testing.B) {
cleanTests := genLongPaths()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
for _, test := range cleanTests {
cleanPath(test.path)
}

View File

@ -273,6 +273,27 @@ func TestRouteRedirectFixedPath(t *testing.T) {
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
}
func TestRouteRedirectWithHandler(t *testing.T) {
router := New()
router.RedirectTrailingSlash = true
router.GET("/path", func(c *Context) {})
passed := []bool{false, false}
router.Use(func(c *Context) {
passed[0] = true
c.Next()
})
router.AutoRedirect(func(c *Context) {
passed[1] = true
c.Next()
})
w := performRequest(router, http.MethodGet, "/path/")
assert.Equal(t, "/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
assert.True(t, passed[0])
assert.True(t, passed[1])
}
// TestContextParamsGet tests that a parameter can be parsed from the URL.
func TestRouteParamsByName(t *testing.T) {
name := ""

View File

@ -19,7 +19,7 @@ func init() {
}
func BenchmarkParseAccept(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8")
}
}