mirror of
https://github.com/gin-gonic/gin.git
synced 2026-09-04 22:53:34 +08:00
fix: avoid an allocation in GetQuery* when query string is empty
When requesting a URL without a query string (e.g. `http://example.com/`), Gin was allocating a map in the heap unnecessarily, which was showing up on our benchmarks
This commit is contained in:
parent
34dac209ff
commit
b8e1de3745
10
context.go
10
context.go
@ -588,6 +588,11 @@ func (c *Context) initQueryCache() {
|
|||||||
// GetQueryArray returns a slice of strings for a given query key, plus
|
// GetQueryArray returns a slice of strings for a given query key, plus
|
||||||
// a boolean value whether at least one value exists for the given key.
|
// a boolean value whether at least one value exists for the given key.
|
||||||
func (c *Context) GetQueryArray(key string) (values []string, ok bool) {
|
func (c *Context) GetQueryArray(key string) (values []string, ok bool) {
|
||||||
|
if c.Request == nil || c.Request.URL == nil || c.Request.URL.RawQuery == "" {
|
||||||
|
// Fast path to save unnecessary allocations when the query string is empty
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
c.initQueryCache()
|
c.initQueryCache()
|
||||||
values, ok = c.queryCache[key]
|
values, ok = c.queryCache[key]
|
||||||
return
|
return
|
||||||
@ -602,6 +607,11 @@ func (c *Context) QueryMap(key string) (dicts map[string]string) {
|
|||||||
// GetQueryMap returns a map for a given query key, plus a boolean value
|
// GetQueryMap returns a map for a given query key, plus a boolean value
|
||||||
// whether at least one value exists for the given key.
|
// whether at least one value exists for the given key.
|
||||||
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
|
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
|
||||||
|
if c.Request == nil || c.Request.URL == nil || c.Request.URL.RawQuery == "" {
|
||||||
|
// Fast path to save unnecessary allocations when the query string is empty
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
c.initQueryCache()
|
c.initQueryCache()
|
||||||
return getMapFromFormData(c.queryCache, key)
|
return getMapFromFormData(c.queryCache, key)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -846,9 +846,38 @@ func TestContextQuery(t *testing.T) {
|
|||||||
assert.Empty(t, c.PostForm("foo"))
|
assert.Empty(t, c.PostForm("foo"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextQueryAllocations(t *testing.T) {
|
||||||
|
// URLs with empty query strings should not cause allocations
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
|
c.Request, _ = http.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||||
|
|
||||||
|
allocs := testing.AllocsPerRun(100, func() {
|
||||||
|
// Reset the query cache before each run because testing.AllocsPerRun does a warm-up for each test
|
||||||
|
c.queryCache = nil
|
||||||
|
_, _ = c.GetQuery("foo")
|
||||||
|
})
|
||||||
|
assert.Zero(t, allocs, "expected GetQuery to not cause allocations with empty query strings")
|
||||||
|
|
||||||
|
allocs = testing.AllocsPerRun(100, func() {
|
||||||
|
// Reset the query cache before each run because testing.AllocsPerRun does a warm-up for each test
|
||||||
|
c.queryCache = nil
|
||||||
|
_, _ = c.GetQueryArray("foo")
|
||||||
|
})
|
||||||
|
assert.Zero(t, allocs, "expected GetQueryArray to not cause allocations with empty query strings")
|
||||||
|
|
||||||
|
allocs = testing.AllocsPerRun(100, func() {
|
||||||
|
// Reset the query cache before each run because testing.AllocsPerRun does a warm-up for each test
|
||||||
|
c.queryCache = nil
|
||||||
|
_, _ = c.GetQueryMap("foo")
|
||||||
|
})
|
||||||
|
assert.Zero(t, allocs, "expected GetQueryMap to not cause allocations with empty query strings")
|
||||||
|
}
|
||||||
|
|
||||||
func TestContextInitQueryCache(t *testing.T) {
|
func TestContextInitQueryCache(t *testing.T) {
|
||||||
validURL, err := url.Parse("https://github.com/gin-gonic/gin/pull/3969?key=value&otherkey=othervalue")
|
validURL, err := url.Parse("https://github.com/gin-gonic/gin/pull/3969?key=value&otherkey=othervalue")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
noQSURL, err := url.Parse("https://github.com/gin-gonic/gin/pull/3969")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
testName string
|
testName string
|
||||||
@ -873,6 +902,13 @@ func TestContextInitQueryCache(t *testing.T) {
|
|||||||
testContext: &Context{Request: &http.Request{URL: nil}}, // explicit nil for readability
|
testContext: &Context{Request: &http.Request{URL: nil}}, // explicit nil for readability
|
||||||
expectedQueryCache: url.Values{},
|
expectedQueryCache: url.Values{},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
testName: "queryCache should be empty when URL has no query string",
|
||||||
|
testContext: &Context{
|
||||||
|
Request: &http.Request{URL: noQSURL},
|
||||||
|
},
|
||||||
|
expectedQueryCache: url.Values{},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
testName: "queryCache should be populated when it not yet populated and Request + Request.URL are non nil",
|
testName: "queryCache should be populated when it not yet populated and Request + Request.URL are non nil",
|
||||||
testContext: &Context{Request: &http.Request{URL: validURL}}, // explicit nil for readability
|
testContext: &Context{Request: &http.Request{URL: validURL}}, // explicit nil for readability
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user