diff --git a/context.go b/context.go index 1dc730e3..b1d8dac4 100644 --- a/context.go +++ b/context.go @@ -588,6 +588,11 @@ func (c *Context) initQueryCache() { // 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. 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() values, ok = c.queryCache[key] 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 // whether at least one value exists for the given key. 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() return getMapFromFormData(c.queryCache, key) } diff --git a/context_test.go b/context_test.go index e8d305e4..2e88a0d4 100644 --- a/context_test.go +++ b/context_test.go @@ -846,9 +846,38 @@ func TestContextQuery(t *testing.T) { 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) { validURL, err := url.Parse("https://github.com/gin-gonic/gin/pull/3969?key=value&otherkey=othervalue") require.NoError(t, err) + noQSURL, err := url.Parse("https://github.com/gin-gonic/gin/pull/3969") + require.NoError(t, err) tests := []struct { testName string @@ -873,6 +902,13 @@ func TestContextInitQueryCache(t *testing.T) { testContext: &Context{Request: &http.Request{URL: nil}}, // explicit nil for readability 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", testContext: &Context{Request: &http.Request{URL: validURL}}, // explicit nil for readability