Compare commits

...

5 Commits

Author SHA1 Message Date
Asbjørn Ulsberg
0a2e4c0d08
Merge ce6939ce4090aa41861cd93daccce4b68f891b25 into 58135f06cf206a9ff713eb14150ef04a05b031d4 2025-11-15 22:35:05 +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
Asbjørn Ulsberg
ce6939ce40
feat: add Engine.PathFor function 2023-04-30 23:12:07 +02:00
12 changed files with 159 additions and 21 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)

View File

@ -9,6 +9,7 @@
- [Using GET, POST, PUT, PATCH, DELETE and OPTIONS](#using-get-post-put-patch-delete-and-options)
- [Parameters in path](#parameters-in-path)
- [Querystring parameters](#querystring-parameters)
- [Retrieving the path of a registered handler](#retrieving-the-path-of-a-registered-handler)
- [Multipart/Urlencoded Form](#multiparturlencoded-form)
- [Another example: query + post form](#another-example-query--post-form)
- [Map as querystring or postform parameters](#map-as-querystring-or-postform-parameters)
@ -184,6 +185,29 @@ func main() {
}
```
### Retrieving the path of a registered handler
Once a handler is registered with the router, you can retrieve its path with the `PathFor` method:
```go
func main() {
// Creates a gin router with default middleware:
// logger and recovery (crash-free) middleware
router := gin.Default()
getUser := func(c *Context) {
// Handle the GET request.
}
router.GET("/users/:name", getUser)
path := router.PathFor(getUser, ":name", "gopher")
print(path) // Prints /users/gopher
router.Run()
}
router := gin.Default()
```
### Multipart/Urlencoded Form
```go

66
gin.go
View File

@ -384,6 +384,24 @@ func (engine *Engine) Routes() (routes RoutesInfo) {
return routes
}
// Routes returns a slice of registered routes, including some useful information, such as:
// the http method, path and the handler name.
func (engine *Engine) Route(handler HandlerFunc) (route RouteInfo, ok bool) {
handlerName := nameOfFunction(handler)
routes := RoutesInfo{}
for _, tree := range engine.trees {
routes = iterate("", tree.method, routes, tree.root)
for _, route := range routes {
if route.Handler == handlerName {
return route, true
}
}
}
return RouteInfo{}, false
}
func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
path += root.path
if len(root.handlers) > 0 {
@ -659,6 +677,43 @@ func (engine *Engine) HandleContext(c *Context) {
c.handlers = oldHandlers
}
// PathFor returns the path registered for the specified handler function.
// Route values are passed pair-wise as key value.
func (engine *Engine) PathFor(handler HandlerFunc, values ...interface{}) string {
route, ok := engine.Route(handler)
if !ok || len(route.Path) == 0 || len(values)%2 != 0 {
return ""
}
url := route.Path
params := make(map[string]string)
if len(values) > 0 {
key := ""
for k, v := range values {
if k%2 == 0 {
key = fmt.Sprint(v)
} else {
params[key] = fmt.Sprint(v)
}
}
}
urls := strings.Split(url, "/")
for _, v := range urls {
if v == "" {
continue
}
if v[0:1] == ":" {
if u, ok := params[v]; ok {
delete(params, v)
url = strings.Replace(url, v, u, 1)
}
}
}
return url + toQuerystring(params)
}
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
@ -787,3 +842,14 @@ func redirectRequest(c *Context) {
http.Redirect(c.Writer, req, rURL, code)
c.writermem.WriteHeaderNow()
}
func toQuerystring(params map[string]string) string {
if len(params) == 0 {
return ""
}
u := "?"
for k, v := range params {
u += k + "=" + v + "&"
}
return strings.TrimRight(u, "&")
}

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

@ -825,6 +825,25 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
}
}
func TestPathFor(t *testing.T) {
r := New()
getIndex := func(c *Context) {}
r.GET("/", getIndex)
users := r.Group("/users")
postUser := func(c *Context) {}
users.POST("", postUser)
getUser := func(c *Context) {}
users.GET("/:name", getUser)
assert.Equal(t, "/", r.PathFor(getIndex))
assert.Equal(t, "/users", r.PathFor(postUser))
assert.Equal(t, "/users/gopher", r.PathFor(getUser, ":name", "gopher"))
}
func parseCIDR(cidr string) *net.IPNet {
_, parsedCIDR, err := net.ParseCIDR(cidr)
if err != nil {

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

@ -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")
}
}