Merge branch 'master' of https://github.com/gin-gonic/gin into gin-gonic-master

This commit is contained in:
laojianzi 2021-03-27 16:28:35 +08:00
commit e3f85d8161
27 changed files with 346 additions and 204 deletions

View File

@ -7,7 +7,7 @@ matrix:
env: GO111MODULE=on env: GO111MODULE=on
- go: 1.13.x - go: 1.13.x
- go: 1.13.x - go: 1.13.x
env: env:
- TESTTAGS=nomsgpack - TESTTAGS=nomsgpack
- go: 1.14.x - go: 1.14.x
- go: 1.14.x - go: 1.14.x

View File

@ -156,7 +156,7 @@ People and companies, who have contributed, in alphabetical order.
- Fix variadic parameter in the flexible render API - Fix variadic parameter in the flexible render API
- Fix Corrupted plain render - Fix Corrupted plain render
- Add Pluggable View Renderer Example - Add Pluggable View Renderer Example
**@msemenistyi (Mykyta Semenistyi)** **@msemenistyi (Mykyta Semenistyi)**
- update Readme.md. Add code to String method - update Readme.md. Add code to String method

View File

@ -1,11 +1,11 @@
# Benchmark System # Benchmark System
**VM HOST:** Travis **VM HOST:** Travis
**Machine:** Ubuntu 16.04.6 LTS x64 **Machine:** Ubuntu 16.04.6 LTS x64
**Date:** May 04th, 2020 **Date:** May 04th, 2020
**Version:** Gin v1.6.3 **Version:** Gin v1.6.3
**Go Version:** 1.14.2 linux/amd64 **Go Version:** 1.14.2 linux/amd64
**Source:** [Go HTTP Router Benchmark](https://github.com/gin-gonic/go-http-routing-benchmark) **Source:** [Go HTTP Router Benchmark](https://github.com/gin-gonic/go-http-routing-benchmark)
**Result:** [See the gist](https://gist.github.com/appleboy/b5f2ecfaf50824ae9c64dcfb9165ae5e) or [Travis result](https://travis-ci.org/github/gin-gonic/go-http-routing-benchmark/jobs/682947061) **Result:** [See the gist](https://gist.github.com/appleboy/b5f2ecfaf50824ae9c64dcfb9165ae5e) or [Travis result](https://travis-ci.org/github/gin-gonic/go-http-routing-benchmark/jobs/682947061)

View File

@ -215,12 +215,12 @@
## Gin 1.1 ## Gin 1.1
- [NEW] Implement QueryArray and PostArray methods - [NEW] Implement QueryArray and PostArray methods
- [NEW] Refactor GetQuery and GetPostForm - [NEW] Refactor GetQuery and GetPostForm
- [NEW] Add contribution guide - [NEW] Add contribution guide
- [FIX] Corrected typos in README - [FIX] Corrected typos in README
- [FIX] Removed additional Iota - [FIX] Removed additional Iota
- [FIX] Changed imports to gopkg instead of github in README (#733) - [FIX] Changed imports to gopkg instead of github in README (#733)
- [FIX] Logger: skip ANSI color commands if output is not a tty - [FIX] Logger: skip ANSI color commands if output is not a tty
## Gin 1.0rc2 (...) ## Gin 1.0rc2 (...)

View File

@ -1,4 +1,4 @@
## Contributing ## Contributing
- With issues: - With issues:
- Use the search tool before opening a new issue. - Use the search tool before opening a new issue.

View File

@ -103,7 +103,7 @@ import "net/http"
``` ```
## Quick start ## Quick start
```sh ```sh
# assume the following codes in example.go file # assume the following codes in example.go file
$ cat example.go $ cat example.go
@ -588,44 +588,44 @@ func main() {
::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" " ::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" "
``` ```
### Controlling Log output coloring ### Controlling Log output coloring
By default, logs output on console should be colorized depending on the detected TTY. By default, logs output on console should be colorized depending on the detected TTY.
Never colorize logs: Never colorize logs:
```go ```go
func main() { func main() {
// Disable log's color // Disable log's color
gin.DisableConsoleColor() gin.DisableConsoleColor()
// Creates a gin router with default middleware: // Creates a gin router with default middleware:
// logger and recovery (crash-free) middleware // logger and recovery (crash-free) middleware
router := gin.Default() router := gin.Default()
router.GET("/ping", func(c *gin.Context) { router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong") c.String(200, "pong")
}) })
router.Run(":8080") router.Run(":8080")
} }
``` ```
Always colorize logs: Always colorize logs:
```go ```go
func main() { func main() {
// Force log's color // Force log's color
gin.ForceConsoleColor() gin.ForceConsoleColor()
// Creates a gin router with default middleware: // Creates a gin router with default middleware:
// logger and recovery (crash-free) middleware // logger and recovery (crash-free) middleware
router := gin.Default() router := gin.Default()
router.GET("/ping", func(c *gin.Context) { router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong") c.String(200, "pong")
}) })
router.Run(":8080") router.Run(":8080")
} }
``` ```
@ -667,12 +667,12 @@ func main() {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
if json.User != "manu" || json.Password != "123" { if json.User != "manu" || json.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return return
} }
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
}) })
@ -688,12 +688,12 @@ func main() {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
if xml.User != "manu" || xml.Password != "123" { if xml.User != "manu" || xml.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return return
} }
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
}) })
@ -705,12 +705,12 @@ func main() {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
if form.User != "manu" || form.Password != "123" { if form.User != "manu" || form.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return return
} }
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
}) })
@ -807,7 +807,7 @@ $ curl "localhost:8085/bookable?check_in=2030-03-10&check_out=2030-03-09"
{"error":"Key: 'Booking.CheckOut' Error:Field validation for 'CheckOut' failed on the 'gtfield' tag"} {"error":"Key: 'Booking.CheckOut' Error:Field validation for 'CheckOut' failed on the 'gtfield' tag"}
$ curl "localhost:8085/bookable?check_in=2000-03-09&check_out=2000-03-10" $ curl "localhost:8085/bookable?check_in=2000-03-09&check_out=2000-03-10"
{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"}% {"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"}%
``` ```
[Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way. [Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way.
@ -1145,7 +1145,7 @@ func main() {
data := gin.H{ data := gin.H{
"foo": "bar", "foo": "bar",
} }
//callback is x //callback is x
// Will output : x({\"foo\":\"bar\"}) // Will output : x({\"foo\":\"bar\"})
c.JSONP(http.StatusOK, data) c.JSONP(http.StatusOK, data)
@ -1190,21 +1190,21 @@ This feature is unavailable in Go 1.6 and lower.
```go ```go
func main() { func main() {
r := gin.Default() r := gin.Default()
// Serves unicode entities // Serves unicode entities
r.GET("/json", func(c *gin.Context) { r.GET("/json", func(c *gin.Context) {
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"html": "<b>Hello, world!</b>", "html": "<b>Hello, world!</b>",
}) })
}) })
// Serves literal characters // Serves literal characters
r.GET("/purejson", func(c *gin.Context) { r.GET("/purejson", func(c *gin.Context) {
c.PureJSON(200, gin.H{ c.PureJSON(200, gin.H{
"html": "<b>Hello, world!</b>", "html": "<b>Hello, world!</b>",
}) })
}) })
// listen and serve on 0.0.0.0:8080 // listen and serve on 0.0.0.0:8080
r.Run(":8080") r.Run(":8080")
} }
@ -1793,8 +1793,8 @@ func main() {
// Initializing the server in a goroutine so that // Initializing the server in a goroutine so that
// it won't block the graceful shutdown handling below // it won't block the graceful shutdown handling below
go func() { go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) {
log.Fatalf("listen: %s\n", err) log.Printf("listen: %s\n", err)
} }
}() }()
@ -1812,10 +1812,11 @@ func main() {
// the request it is currently handling // the request it is currently handling
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
if err := srv.Shutdown(ctx); err != nil { if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err) log.Fatal("Server forced to shutdown:", err)
} }
log.Println("Server exiting") log.Println("Server exiting")
} }
``` ```

View File

@ -5,6 +5,7 @@
package gin package gin
import ( import (
"crypto/subtle"
"encoding/base64" "encoding/base64"
"net/http" "net/http"
"strconv" "strconv"
@ -30,7 +31,7 @@ func (a authPairs) searchCredential(authValue string) (string, bool) {
return "", false return "", false
} }
for _, pair := range a { for _, pair := range a {
if pair.value == authValue { if subtle.ConstantTimeCompare([]byte(pair.value), []byte(authValue)) == 1 {
return pair.user, true return pair.user, true
} }
} }

View File

@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !nomsgpack
// +build !nomsgpack // +build !nomsgpack
package binding package binding
@ -51,7 +52,8 @@ type BindingUri interface {
// https://github.com/go-playground/validator/tree/v8.18.2. // https://github.com/go-playground/validator/tree/v8.18.2.
type StructValidator interface { type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right. // ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
// If the received type is not a struct, any validation should be skipped and nil must be returned. // If the received type is a slice|array, the validation should be performed travel on every element.
// If the received type is not a struct or slice|array, any validation should be skipped and nil must be returned.
// If the received type is a struct or pointer to a struct, the validation should be performed. // If the received type is a struct or pointer to a struct, the validation should be performed.
// If the struct is not valid or the validation itself fails, a descriptive error should be returned. // If the struct is not valid or the validation itself fails, a descriptive error should be returned.
// Otherwise nil must be returned. // Otherwise nil must be returned.

View File

@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !nomsgpack
// +build !nomsgpack // +build !nomsgpack
package binding package binding

View File

@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build nomsgpack
// +build nomsgpack // +build nomsgpack
package binding package binding

View File

@ -35,7 +35,7 @@ type QueryTest struct {
} }
type FooStruct struct { type FooStruct struct {
Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required"` Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required,max=32"`
} }
type FooBarStruct struct { type FooBarStruct struct {
@ -181,6 +181,20 @@ func TestBindingJSON(t *testing.T) {
`{"foo": "bar"}`, `{"bar": "foo"}`) `{"foo": "bar"}`, `{"bar": "foo"}`)
} }
func TestBindingJSONSlice(t *testing.T) {
EnableDecoderDisallowUnknownFields = true
defer func() {
EnableDecoderDisallowUnknownFields = false
}()
testBodyBindingSlice(t, JSON, "json", "/", "/", `[]`, ``)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{}]`)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": ""}]`)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": 123}]`)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"bar": 123}]`)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": "123456789012345678901234567890123"}]`)
}
func TestBindingJSONUseNumber(t *testing.T) { func TestBindingJSONUseNumber(t *testing.T) {
testBodyBindingUseNumber(t, testBodyBindingUseNumber(t,
JSON, "json", JSON, "json",
@ -1181,6 +1195,20 @@ func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody
assert.Error(t, err) assert.Error(t, err)
} }
func testBodyBindingSlice(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())
var obj1 []FooStruct
req := requestWithBody("POST", path, body)
err := b.Bind(req, &obj1)
assert.NoError(t, err)
var obj2 []FooStruct
req = requestWithBody("POST", badPath, badBody)
err = JSON.Bind(req, &obj2)
assert.Error(t, err)
}
func testBodyBindingStringMap(t *testing.T, b Binding, path, badPath, body, badBody string) { func testBodyBindingStringMap(t *testing.T, b Binding, path, badPath, body, badBody string) {
obj := make(map[string]string) obj := make(map[string]string)
req := requestWithBody("POST", path, body) req := requestWithBody("POST", path, body)

View File

@ -5,7 +5,9 @@
package binding package binding
import ( import (
"fmt"
"reflect" "reflect"
"strings"
"sync" "sync"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
@ -16,22 +18,54 @@ type defaultValidator struct {
validate *validator.Validate validate *validator.Validate
} }
type sliceValidateError []error
func (err sliceValidateError) Error() string {
var errMsgs []string
for i, e := range err {
if e == nil {
continue
}
errMsgs = append(errMsgs, fmt.Sprintf("[%d]: %s", i, e.Error()))
}
return strings.Join(errMsgs, "\n")
}
var _ StructValidator = &defaultValidator{} var _ StructValidator = &defaultValidator{}
// ValidateStruct receives any kind of type, but only performed struct or pointer to struct type. // ValidateStruct receives any kind of type, but only performed struct or pointer to struct type.
func (v *defaultValidator) ValidateStruct(obj interface{}) error { func (v *defaultValidator) ValidateStruct(obj interface{}) error {
if obj == nil {
return nil
}
value := reflect.ValueOf(obj) value := reflect.ValueOf(obj)
valueType := value.Kind() switch value.Kind() {
if valueType == reflect.Ptr { case reflect.Ptr:
valueType = value.Elem().Kind() return v.ValidateStruct(value.Elem().Interface())
} case reflect.Struct:
if valueType == reflect.Struct { return v.validateStruct(obj)
v.lazyinit() case reflect.Slice, reflect.Array:
if err := v.validate.Struct(obj); err != nil { count := value.Len()
return err validateRet := make(sliceValidateError, 0)
for i := 0; i < count; i++ {
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
validateRet = append(validateRet, err)
}
} }
if len(validateRet) == 0 {
return nil
}
return validateRet
default:
return nil
} }
return nil }
// validateStruct receives struct type
func (v *defaultValidator) validateStruct(obj interface{}) error {
v.lazyinit()
return v.validate.Struct(obj)
} }
// Engine returns the underlying validator engine which powers the default // Engine returns the underlying validator engine which powers the default

View File

@ -0,0 +1,68 @@
// 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.
package binding
import (
"errors"
"testing"
)
func TestSliceValidateError(t *testing.T) {
tests := []struct {
name string
err sliceValidateError
want string
}{
{"has nil elements", sliceValidateError{errors.New("test error"), nil}, "[0]: test error"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.err.Error(); got != tt.want {
t.Errorf("sliceValidateError.Error() = %v, want %v", got, tt.want)
}
})
}
}
func TestDefaultValidator(t *testing.T) {
type exampleStruct struct {
A string `binding:"max=8"`
B int `binding:"gt=0"`
}
tests := []struct {
name string
v *defaultValidator
obj interface{}
wantErr bool
}{
{"validate nil obj", &defaultValidator{}, nil, false},
{"validate int obj", &defaultValidator{}, 3, false},
{"validate struct failed-1", &defaultValidator{}, exampleStruct{A: "123456789", B: 1}, true},
{"validate struct failed-2", &defaultValidator{}, exampleStruct{A: "12345678", B: 0}, true},
{"validate struct passed", &defaultValidator{}, exampleStruct{A: "12345678", B: 1}, false},
{"validate *struct failed-1", &defaultValidator{}, &exampleStruct{A: "123456789", B: 1}, true},
{"validate *struct failed-2", &defaultValidator{}, &exampleStruct{A: "12345678", B: 0}, true},
{"validate *struct passed", &defaultValidator{}, &exampleStruct{A: "12345678", B: 1}, false},
{"validate []struct failed-1", &defaultValidator{}, []exampleStruct{{A: "123456789", B: 1}}, true},
{"validate []struct failed-2", &defaultValidator{}, []exampleStruct{{A: "12345678", B: 0}}, true},
{"validate []struct passed", &defaultValidator{}, []exampleStruct{{A: "12345678", B: 1}}, false},
{"validate []*struct failed-1", &defaultValidator{}, []*exampleStruct{{A: "123456789", B: 1}}, true},
{"validate []*struct failed-2", &defaultValidator{}, []*exampleStruct{{A: "12345678", B: 0}}, true},
{"validate []*struct passed", &defaultValidator{}, []*exampleStruct{{A: "12345678", B: 1}}, false},
{"validate *[]struct failed-1", &defaultValidator{}, &[]exampleStruct{{A: "123456789", B: 1}}, true},
{"validate *[]struct failed-2", &defaultValidator{}, &[]exampleStruct{{A: "12345678", B: 0}}, true},
{"validate *[]struct passed", &defaultValidator{}, &[]exampleStruct{{A: "12345678", B: 1}}, false},
{"validate *[]*struct failed-1", &defaultValidator{}, &[]*exampleStruct{{A: "123456789", B: 1}}, true},
{"validate *[]*struct failed-2", &defaultValidator{}, &[]*exampleStruct{{A: "12345678", B: 0}}, true},
{"validate *[]*struct passed", &defaultValidator{}, &[]*exampleStruct{{A: "12345678", B: 1}}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.v.ValidateStruct(tt.obj); (err != nil) != tt.wantErr {
t.Errorf("defaultValidator.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !nomsgpack
// +build !nomsgpack // +build !nomsgpack
package binding package binding

View File

@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !nomsgpack
// +build !nomsgpack // +build !nomsgpack
package binding package binding

View File

@ -1,9 +1,10 @@
// +build appengine
// Copyright 2017 Manu Martinez-Almeida. All rights reserved. // Copyright 2017 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build appengine
// +build appengine
package gin package gin
func init() { func init() {

View File

@ -12,7 +12,7 @@ import (
"strings" "strings"
) )
const ginSupportMinGoVer = 10 const ginSupportMinGoVer = 12
// IsDebugging returns true if the framework is running in debug mode. // IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode. // Use SetMode(gin.ReleaseMode) to disable debug mode.
@ -67,7 +67,7 @@ func getMinVer(v string) (uint64, error) {
func debugPrintWARNINGDefault() { func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer { if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.11 or later and Go 1.12 will be required soon. debugPrint(`[WARNING] Now Gin requires Go 1.12+.
`) `)
} }

View File

@ -104,7 +104,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
}) })
m, e := getMinVer(runtime.Version()) m, e := getMinVer(runtime.Version())
if e == nil && m <= ginSupportMinGoVer { if e == nil && m <= ginSupportMinGoVer {
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.11 or later and Go 1.12 will be required soon.\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.12+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} else { } else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} }

View File

@ -1,3 +1,4 @@
//go:build go1.13
// +build go1.13 // +build go1.13
package gin package gin

View File

@ -5,16 +5,17 @@
package bytesconv package bytesconv
import ( import (
"reflect"
"unsafe" "unsafe"
) )
// StringToBytes converts string to byte slice without a memory allocation. // StringToBytes converts string to byte slice without a memory allocation.
func StringToBytes(s string) (b []byte) { func StringToBytes(s string) []byte {
sh := *(*reflect.StringHeader)(unsafe.Pointer(&s)) return *(*[]byte)(unsafe.Pointer(
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) &struct {
bh.Data, bh.Len, bh.Cap = sh.Data, sh.Len, sh.Len string
return b Cap int
}{s, len(s)},
))
} }
// BytesToString converts byte slice to string without a memory allocation. // BytesToString converts byte slice to string without a memory allocation.

View File

@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !jsoniter
// +build !jsoniter // +build !jsoniter
package json package json

View File

@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build jsoniter
// +build jsoniter // +build jsoniter
package json package json

View File

@ -63,7 +63,7 @@ func SetMode(value string) {
case TestMode: case TestMode:
ginMode = testCode ginMode = testCode
default: default:
panic("gin mode unknown: " + value) panic("gin mode unknown: " + value + " (available mode: debug release test)")
} }
modeName = value modeName = value

View File

@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !nomsgpack
// +build !nomsgpack // +build !nomsgpack
package render package render

View File

@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !nomsgpack
// +build !nomsgpack // +build !nomsgpack
package render package render

View File

@ -238,7 +238,6 @@ func TestRouteParamsByName(t *testing.T) {
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, name, c.Param("name")) assert.Equal(t, name, c.Param("name"))
assert.Equal(t, name, c.Param("name"))
assert.Equal(t, lastName, c.Param("last_name")) assert.Equal(t, lastName, c.Param("last_name"))
assert.Empty(t, c.Param("wtf")) assert.Empty(t, c.Param("wtf"))
@ -272,7 +271,6 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, name, c.Param("name")) assert.Equal(t, name, c.Param("name"))
assert.Equal(t, name, c.Param("name"))
assert.Equal(t, lastName, c.Param("last_name")) assert.Equal(t, lastName, c.Param("last_name"))
assert.Empty(t, c.Param("wtf")) assert.Empty(t, c.Param("wtf"))

277
tree.go
View File

@ -119,7 +119,6 @@ func (n *node) incrementChildPrio(pos int) int {
for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- { for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
// Swap node positions // Swap node positions
cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1] cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
} }
// Build new index char string // Build new index char string
@ -559,8 +558,8 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]by
// Use a static sized buffer on the stack in the common case. // Use a static sized buffer on the stack in the common case.
// If the path is too long, allocate a buffer on the heap instead. // If the path is too long, allocate a buffer on the heap instead.
buf := make([]byte, 0, stackBufSize) buf := make([]byte, 0, stackBufSize)
if l := len(path) + 1; l > stackBufSize { if length := len(path) + 1; length > stackBufSize {
buf = make([]byte, 0, l) buf = make([]byte, 0, length)
} }
ciPath := n.findCaseInsensitivePathRec( ciPath := n.findCaseInsensitivePathRec(
@ -600,142 +599,7 @@ walk: // Outer loop for walking the tree
path = path[npLen:] path = path[npLen:]
ciPath = append(ciPath, n.path...) ciPath = append(ciPath, n.path...)
if len(path) > 0 { if len(path) == 0 {
// If this node does not have a wildcard (param or catchAll) child,
// we can just look up the next child node and continue to walk down
// the tree
if !n.wildChild {
// Skip rune bytes already processed
rb = shiftNRuneBytes(rb, npLen)
if rb[0] != 0 {
// Old rune not finished
idxc := rb[0]
for i, c := range []byte(n.indices) {
if c == idxc {
// continue with child node
n = n.children[i]
npLen = len(n.path)
continue walk
}
}
} else {
// Process a new rune
var rv rune
// Find rune start.
// Runes are up to 4 byte long,
// -4 would definitely be another rune.
var off int
for max := min(npLen, 3); off < max; off++ {
if i := npLen - off; utf8.RuneStart(oldPath[i]) {
// read rune from cached path
rv, _ = utf8.DecodeRuneInString(oldPath[i:])
break
}
}
// Calculate lowercase bytes of current rune
lo := unicode.ToLower(rv)
utf8.EncodeRune(rb[:], lo)
// Skip already processed bytes
rb = shiftNRuneBytes(rb, off)
idxc := rb[0]
for i, c := range []byte(n.indices) {
// Lowercase matches
if c == idxc {
// must use a recursive approach since both the
// uppercase byte and the lowercase byte might exist
// as an index
if out := n.children[i].findCaseInsensitivePathRec(
path, ciPath, rb, fixTrailingSlash,
); out != nil {
return out
}
break
}
}
// If we found no match, the same for the uppercase rune,
// if it differs
if up := unicode.ToUpper(rv); up != lo {
utf8.EncodeRune(rb[:], up)
rb = shiftNRuneBytes(rb, off)
idxc := rb[0]
for i, c := range []byte(n.indices) {
// Uppercase matches
if c == idxc {
// Continue with child node
n = n.children[i]
npLen = len(n.path)
continue walk
}
}
}
}
// Nothing found. We can recommend to redirect to the same URL
// without a trailing slash if a leaf exists for that path
if fixTrailingSlash && path == "/" && n.handlers != nil {
return ciPath
}
return nil
}
n = n.children[0]
switch n.nType {
case param:
// Find param end (either '/' or path end)
end := 0
for end < len(path) && path[end] != '/' {
end++
}
// Add param value to case insensitive path
ciPath = append(ciPath, path[:end]...)
// We need to go deeper!
if end < len(path) {
if len(n.children) > 0 {
// Continue with child node
n = n.children[0]
npLen = len(n.path)
path = path[end:]
continue
}
// ... but we can't
if fixTrailingSlash && len(path) == end+1 {
return ciPath
}
return nil
}
if n.handlers != nil {
return ciPath
}
if fixTrailingSlash && len(n.children) == 1 {
// No handle found. Check if a handle for this path + a
// trailing slash exists
n = n.children[0]
if n.path == "/" && n.handlers != nil {
return append(ciPath, '/')
}
}
return nil
case catchAll:
return append(ciPath, path...)
default:
panic("invalid node type")
}
} else {
// We should have reached the node containing the handle. // We should have reached the node containing the handle.
// Check if this node has a handle registered. // Check if this node has a handle registered.
if n.handlers != nil { if n.handlers != nil {
@ -758,6 +622,141 @@ walk: // Outer loop for walking the tree
} }
return nil return nil
} }
// If this node does not have a wildcard (param or catchAll) child,
// we can just look up the next child node and continue to walk down
// the tree
if !n.wildChild {
// Skip rune bytes already processed
rb = shiftNRuneBytes(rb, npLen)
if rb[0] != 0 {
// Old rune not finished
idxc := rb[0]
for i, c := range []byte(n.indices) {
if c == idxc {
// continue with child node
n = n.children[i]
npLen = len(n.path)
continue walk
}
}
} else {
// Process a new rune
var rv rune
// Find rune start.
// Runes are up to 4 byte long,
// -4 would definitely be another rune.
var off int
for max := min(npLen, 3); off < max; off++ {
if i := npLen - off; utf8.RuneStart(oldPath[i]) {
// read rune from cached path
rv, _ = utf8.DecodeRuneInString(oldPath[i:])
break
}
}
// Calculate lowercase bytes of current rune
lo := unicode.ToLower(rv)
utf8.EncodeRune(rb[:], lo)
// Skip already processed bytes
rb = shiftNRuneBytes(rb, off)
idxc := rb[0]
for i, c := range []byte(n.indices) {
// Lowercase matches
if c == idxc {
// must use a recursive approach since both the
// uppercase byte and the lowercase byte might exist
// as an index
if out := n.children[i].findCaseInsensitivePathRec(
path, ciPath, rb, fixTrailingSlash,
); out != nil {
return out
}
break
}
}
// If we found no match, the same for the uppercase rune,
// if it differs
if up := unicode.ToUpper(rv); up != lo {
utf8.EncodeRune(rb[:], up)
rb = shiftNRuneBytes(rb, off)
idxc := rb[0]
for i, c := range []byte(n.indices) {
// Uppercase matches
if c == idxc {
// Continue with child node
n = n.children[i]
npLen = len(n.path)
continue walk
}
}
}
}
// Nothing found. We can recommend to redirect to the same URL
// without a trailing slash if a leaf exists for that path
if fixTrailingSlash && path == "/" && n.handlers != nil {
return ciPath
}
return nil
}
n = n.children[0]
switch n.nType {
case param:
// Find param end (either '/' or path end)
end := 0
for end < len(path) && path[end] != '/' {
end++
}
// Add param value to case insensitive path
ciPath = append(ciPath, path[:end]...)
// We need to go deeper!
if end < len(path) {
if len(n.children) > 0 {
// Continue with child node
n = n.children[0]
npLen = len(n.path)
path = path[end:]
continue
}
// ... but we can't
if fixTrailingSlash && len(path) == end+1 {
return ciPath
}
return nil
}
if n.handlers != nil {
return ciPath
}
if fixTrailingSlash && len(n.children) == 1 {
// No handle found. Check if a handle for this path + a
// trailing slash exists
n = n.children[0]
if n.path == "/" && n.handlers != nil {
return append(ciPath, '/')
}
}
return nil
case catchAll:
return append(ciPath, path...)
default:
panic("invalid node type")
}
} }
// Nothing found. // Nothing found.