Repository: goadesign/goa (go)

Issue:
Generated server code parses query parameters repeatedly
When goa generates server code for query parameters, it checks each parameter one after another with code that looks something like this:

```go
		advRaw := r.URL.Query().Get("adv")
		if advRaw != "" {
			adv = &advRaw
		}
		afRaw := r.URL.Query().Get("af")
		if afRaw != "" {
			af = &afRaw
		}
		apbndlRaw := r.URL.Query().Get("apbndl")
		if apbndlRaw != "" {
			apbndl = &apbndlRaw
		}
```

The problem with this code, is that `r.URL.Query()` is called repeatedly. Each call parses `r.URL.RawQuery` into a `url.Values` map. One values is pulled from the map, then the map is left to be garbage collected. It'd be much better to parse the query parameters once and store the result in a variable to be referenced for each query parameter to check.

In a test on one of our projects on an endpoint with many query parameters defined, this resulted in 24.11GB (41.4%) of heap allocations (using the alloc profile) coming from calls to `url.(*URL).Query` within the generated `server.DecodeXXXXRequest.func1`.

Pull request:
Save r.URL.Query() in a variable
If there are multiple query parameters, save the call to Query() in a variable.

Prevents r.URL.Query() from being called multiple times for a handling a single request. Prevents unneeded churn on generated code by only applying that change to code that generates mutliple query parameters.

Closes #3505

Patch:
diff --git a/http/codegen/templates/partial/request_elements.go.tpl b/http/codegen/templates/partial/request_elements.go.tpl
index d18caecd..0e490ede 100644
--- a/http/codegen/templates/partial/request_elements.go.tpl
+++ b/http/codegen/templates/partial/request_elements.go.tpl
@@ -42,15 +42,20 @@
 		{{- end }}
 {{- end }}
 
+{{- $qpVar := "r.URL.Query()" }}
+{{- if gt (len .QueryParams) 1 }}
+{{- $qpVar = "qp" }}
+qp := r.URL.Query()
+{{- end }}
 {{- range .QueryParams }}
 	{{- if and (or (eq .Type.Name "string") (eq .Type.Name "any")) .Required }}
-		{{ .VarName }} = r.URL.Query().Get("{{ .HTTPName }}")
+		{{ .VarName }} = {{$qpVar}}.Get("{{ .HTTPName }}")
 		if {{ .VarName }} == "" {
 			err = goa.MergeErrors(err, goa.MissingFieldError("{{ .Name }}", "query string"))
 		}
 
 	{{- else if (or (eq .Type.Name "string") (eq .Type.Name "any")) }}
-		{{ .VarName }}Raw := r.URL.Query().Get("{{ .HTTPName }}")
+		{{ .VarName }}Raw := {{$qpVar}}.Get("{{ .HTTPName }}")
 		if {{ .VarName }}Raw != "" {
 			{{ .VarName }} = {{ if and (eq .Type.Name "string") .Pointer }}&{{ end }}{{ .VarName }}Raw
 		}
@@ -60,7 +65,7 @@
 		{{- end }}
 
 	{{- else if .StringSlice }}
-		{{ .VarName }} = r.URL.Query()["{{ .HTTPName }}"]
+		{{ .VarName }} = {{$qpVar}}["{{ .HTTPName }}"]
 		{{- if .Required }}
 		if {{ .VarName }} == nil {
 			err = goa.MergeErrors(err, goa.MissingFieldError("{{ .Name }}", "query string"))
@@ -77,7 +82,7 @@
 
 	{{- else if .Slice }}
 	{
-		{{ .VarName }}Raw := r.URL.Query()["{{ .HTTPName }}"]
+		{{ .VarName }}Raw := {{$qpVar}}["{{ .HTTPName }}"]
 		{{- if .Required }}
 		if {{ .VarName }}Raw == nil {
 			err = goa.MergeErrors(err, goa.MissingFieldError("{{ .Name }}", "query string"))
@@ -100,7 +105,7 @@
 
 	{{- else if .Map }}
 	{
-		{{ .VarName }}Raw := r.URL.Query()
+		{{ .VarName }}Raw := {{$qpVar}}
 		{{- if .Required }}
 		if len({{ .VarName }}Raw) == 0 {
 			err = goa.MergeErrors(err, goa.MissingFieldError("{{ .Name }}", "query string"))
@@ -127,7 +132,7 @@
 
 	{{- else if .MapQueryParams }}
 	{
-		{{ .VarName }}Raw := r.URL.Query()
+		{{ .VarName }}Raw := {{$qpVar}}
 		{{- if .Required }}
 		if len({{ .VarName }}Raw) == 0 {
 			err = goa.MergeErrors(err, goa.MissingFieldError("{{ .Name }}", "query string"))
@@ -182,7 +187,7 @@
 
 	{{- else }}{{/* not string, not any, not slice and not map */}}
 	{
-		{{ .VarName }}Raw := r.URL.Query().Get("{{ .HTTPName }}")
+		{{ .VarName }}Raw := {{$qpVar}}.Get("{{ .HTTPName }}")
 		{{- if .Required }}
 		if {{ .VarName }}Raw == "" {
 			err = goa.MergeErrors(err, goa.MissingFieldError("{{ .Name }}", "query string"))
@@ -340,4 +345,4 @@
 		{{ .Validate }}
 	{{- end }}
 {{- end }}
-{{- end }}
\ No newline at end of file
+{{- end -}}


Test patch:
diff --git a/http/codegen/testdata/payload_decode_functions.go b/http/codegen/testdata/payload_decode_functions.go
index af54e9e4..ca873407 100644
--- a/http/codegen/testdata/payload_decode_functions.go
+++ b/http/codegen/testdata/payload_decode_functions.go
@@ -3688,6 +3688,7 @@ func DecodeMethodCookieStringDefaultValidateRequest(mux goahttp.Muxer, decoder f
 	}
 }
 `
+
 var PayloadCookiePrimitiveStringDefaultDecodeCode = `// DecodeMethodCookiePrimitiveStringDefaultRequest returns a decoder for
 // requests sent to the ServiceCookiePrimitiveStringDefault
 // MethodCookiePrimitiveStringDefault endpoint.
@@ -5125,8 +5126,9 @@ func DecodeMethodARequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp
 			}
 			path = uint(v)
 		}
+		qp := r.URL.Query()
 		{
-			optionalRaw := r.URL.Query().Get("optional")
+			optionalRaw := qp.Get("optional")
 			if optionalRaw != "" {
 				v, err2 := strconv.ParseInt(optionalRaw, 10, strconv.IntSize)
 				if err2 != nil {
@@ -5137,7 +5139,7 @@ func DecodeMethodARequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp
 			}
 		}
 		{
-			optionalButRequiredParamRaw := r.URL.Query().Get("optional_but_required_param")
+			optionalButRequiredParamRaw := qp.Get("optional_but_required_param")
 			if optionalButRequiredParamRaw == "" {
 				err = goa.MergeErrors(err, goa.MissingFieldError("optional_but_required_param", "query string"))
 			}
@@ -5182,8 +5184,9 @@ func DecodeMethodARequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp
 			int64_ *int64
 			err    error
 		)
+		qp := r.URL.Query()
 		{
-			int_Raw := r.URL.Query().Get("int")
+			int_Raw := qp.Get("int")
 			if int_Raw != "" {
 				v, err2 := strconv.ParseInt(int_Raw, 10, strconv.IntSize)
 				if err2 != nil {
@@ -5194,7 +5197,7 @@ func DecodeMethodARequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp
 			}
 		}
 		{
-			int32_Raw := r.URL.Query().Get("int32")
+			int32_Raw := qp.Get("int32")
 			if int32_Raw != "" {
 				v, err2 := strconv.ParseInt(int32_Raw, 10, 32)
 				if err2 != nil {
@@ -5205,7 +5208,7 @@ func DecodeMethodARequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp
 			}
 		}
 		{
-			int64_Raw := r.URL.Query().Get("int64")
+			int64_Raw := qp.Get("int64")
 			if int64_Raw != "" {
 				v, err2 := strconv.ParseInt(int64_Raw, 10, 64)
 				if err2 != nil {
@@ -5234,8 +5237,9 @@ func DecodeMethodARequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp
 			int64_ *int64
 			err    error
 		)
+		qp := r.URL.Query()
 		{
-			int_Raw := r.URL.Query().Get("int")
+			int_Raw := qp.Get("int")
 			if int_Raw != "" {
 				v, err2 := strconv.ParseInt(int_Raw, 10, strconv.IntSize)
 				if err2 != nil {
@@ -5251,7 +5255,7 @@ func DecodeMethodARequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp
 			}
 		}
 		{
-			int32_Raw := r.URL.Query().Get("int32")
+			int32_Raw := qp.Get("int32")
 			if int32_Raw != "" {
 				v, err2 := strconv.ParseInt(int32_Raw, 10, 32)
 				if err2 != nil {
@@ -5267,7 +5271,7 @@ func DecodeMethodARequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp
 			}
 		}
 		{
-			int64_Raw := r.URL.Query().Get("int64")
+			int64_Raw := qp.Get("int64")
 			if int64_Raw != "" {
 				v, err2 := strconv.ParseInt(int64_Raw, 10, 64)
 				if err2 != nil {
@@ -5360,6 +5364,7 @@ func DecodeMethodARequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp
 	}
 }
 `
+
 var QueryMapAliasDecodeCode = `// DecodeMethodARequest returns a decoder for requests sent to the
 // ServiceQueryMapAlias MethodA endpoint.
 func DecodeMethodARequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) {
@@ -5718,6 +5723,7 @@ func DecodeMethodPathCustomInt32Request(mux goahttp.Muxer, decoder func(*http.Re
 	}
 }
 `
+
 var PayloadPathCustomInt64DecodeCode = `// DecodeMethodPathCustomInt64Request returns a decoder for requests sent to
 // the ServicePathCustomInt64 MethodPathCustomInt64 endpoint.
 func DecodeMethodPathCustomInt64Request(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) {
@@ -5745,6 +5751,7 @@ func DecodeMethodPathCustomInt64Request(mux goahttp.Muxer, decoder func(*http.Re
 	}
 }
 `
+
 var PayloadPathCustomUIntDecodeCode = `// DecodeMethodPathCustomUIntRequest returns a decoder for requests sent to the
 // ServicePathCustomUInt MethodPathCustomUInt endpoint.
 func DecodeMethodPathCustomUIntRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) {
@@ -5772,6 +5779,7 @@ func DecodeMethodPathCustomUIntRequest(mux goahttp.Muxer, decoder func(*http.Req
 	}
 }
 `
+
 var PayloadPathCustomUInt32DecodeCode = `// DecodeMethodPathCustomUInt32Request returns a decoder for requests sent to
 // the ServicePathCustomUInt32 MethodPathCustomUInt32 endpoint.
 func DecodeMethodPathCustomUInt32Request(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) {
@@ -5799,6 +5807,7 @@ func DecodeMethodPathCustomUInt32Request(mux goahttp.Muxer, decoder func(*http.R
 	}
 }
 `
+
 var PayloadPathCustomUInt64DecodeCode = `// DecodeMethodPathCustomUInt64Request returns a decoder for requests sent to
 // the ServicePathCustomUInt64 MethodPathCustomUInt64 endpoint.
 func DecodeMethodPathCustomUInt64Request(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) {