-
-
Notifications
You must be signed in to change notification settings - Fork 6.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Go][Server] Convert Optional Parameters to Pointers #16980
[Go][Server] Convert Optional Parameters to Pointers #16980
Conversation
…ng optional params pointers
Cleanup pointer handling. Everything is a pointer with required params being de-referenced at the function call
if actual == "" { | ||
return empty, false, errors.New(errMsgRequiredMissing) | ||
return &empty, false, errors.New(errMsgRequiredMissing) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can simply return nil
|
||
// parseFloat64 parses a string parameter to an float64. | ||
func parseFloat64(param string) (float64, error) { | ||
func parseFloat64(param string) (*float64, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It isn't a good idea to return pointers for all the parse* functions. This more likely than not results in heap allocation and slows it down considerably.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A heap allocation will be required at some point for all optional query parameters. My thought process was to keep the mustache template as simple as I could keep it. I tried out not returning a pointer from here, but the changes in the controller-api.mustache
became quite large.
I will argue that the heap allocation is negligible, especially for the primitive types being handled by these functions. I included two Benchmarks and their results from my local machine below. The summary is that the difference is a 1/1000th of a nanosecond.
I believe that this time penalty is not noticeable and should not be taken into account for how maintainable the code is.
// goos: linux
// goarch: amd64
// cpu: Intel(R) Core(TM) i9-9900 CPU @ 3.10GHz
// BenchmarkPointerReturn
// BenchmarkPointerReturn-16 1000000000 0.2164 ns/op
// PASS
func BenchmarkPointerReturn(b *testing.B) {
for i := 0; i < b.N; i++ {
p := pointerReturn()
_ = p
}
}
// goos: linux
// goarch: amd64
// cpu: Intel(R) Core(TM) i9-9900 CPU @ 3.10GHz
// BenchmarkValueReturn
// BenchmarkValueReturn-16 1000000000 0.2156 ns/op
// PASS
func BenchmarkValueReturn(b *testing.B) {
for i := 0; i < b.N; i++ {
v := valueReturn()
_ = v
}
}
func pointerReturn() *int {
a := 1
return &a
}
func valueReturn() int {
a := 1
return a
}```
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to print allocations.
Also, you need to disable inlining and prevent escaping. I suggest you run the benchmark on the parsers in the router.go if you want a better comparison.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just ran the benchmarks on the parsers in router.go and got similar results. The overhead of parsing the strings via strconv
greatly outweighs any overhead an allocation on the heap that may occur for these primitive types.
//goos: linux
//goarch: amd64
//pkg: github.com/SpectraLogic/.../api/openapi
//cpu: Intel(R) Core(TM) i9-9900 CPU @ 3.10GHz
//BenchmarkPointerReturn
//BenchmarkPointerReturn-16 1000000000 0.9425 ns/op
//PASS
func BenchmarkPointerReturnBool(b *testing.B) {
for i := 0; i < b.N; i++ {
p, _ := parseBool("true")
_ = p
}
}
// goos: linux
// goarch: amd64
// pkg: github.com/SpectraLogic/.../api/openapi
// cpu: Intel(R) Core(TM) i9-9900 CPU @ 3.10GHz
// BenchmarkValReturn
// BenchmarkValReturn-16 1000000000 0.9367 ns/op
// PASS
func BenchmarkValReturnBool(b *testing.B) {
for i := 0; i < b.N; i++ {
p, _ := parseBoolVal("true")
_ = p
}
}
// goos: linux
// goarch: amd64
// pkg: github.com/SpectraLogic/.../api/openapi
// cpu: Intel(R) Core(TM) i9-9900 CPU @ 3.10GHz
// BenchmarkPointerReturnFloat64
// BenchmarkPointerReturnFloat64-16 70159354 17.99 ns/op
// PASS
func BenchmarkPointerReturnFloat64(b *testing.B) {
for i := 0; i < b.N; i++ {
p, _ := parseFloat64("1.0")
_ = p
}
}
// goos: linux
// goarch: amd64
// pkg: github.com/SpectraLogic/.../api/openapi
// cpu: Intel(R) Core(TM) i9-9900 CPU @ 3.10GHz
// BenchmarkValReturnFloat64
// BenchmarkValReturnFloat64-16 59044398 17.80 ns/op
// PASS
func BenchmarkValReturnFloat64(b *testing.B) {
for i := 0; i < b.N; i++ {
p, _ := parseFloat64Val("1.0")
_ = p
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BenchmarkNumNew-8 48563181 25.93 ns/op 4 B/op 1 allocs/op
BenchmarkBoolNew-8 100000000 11.35 ns/op 1 B/op 1 allocs/op
BenchmarkNumOld-8 71088042 16.85 ns/op 0 B/op 0 allocs/op
BenchmarkBoolOld-8 243797635 4.883 ns/op 0 B/op 0 allocs/op
1.5+x slower on Num (float32)
2+x slower on Bool
inlining turned off
package new
import (
"testing"
)
var result float32
var result2 bool
func BenchmarkNumNew(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
r, err := parseNumericParameter[float32](
"5",
WithParse[float32](parseFloat32),
WithMinimum[float32](0),
)
if err != nil {
b.Fatal(err)
}
result = *r
}
}
func BenchmarkBoolNew(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
r, err := parseBoolParameter(
"true",
WithParse[bool](parseBool),
)
if err != nil {
b.Fatal(err)
}
result2 = *r
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do understand that it is slower, but it is on the order of a few nanoseconds slower per parameter.
I have tried a couple of different ways to write the controller-api.mustache
file without the parsers returning pointers and all of the ways so far have been significantly less maintainable and uglier. Additionally, I haven't been able to smooth out all the wrinkles in the other attempts to get it to work even on my companies REST API Server.
is this PR targeting query, path, or header? I see changes to the path param. The last I saw in 3.0, the path param is required. https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameter-locations. And thus it should never be passed as a pointer you may want to scope the PR to affect query first since you can clearly check if the query is provided by using query.Has |
The changes for the Path Parameters were made in a minimal fashion such that all parameters could be handled the same way at the bottom of the template: This kept the rest of the template much simpler instead of having to juggle what is a pointer and what is not a pointer. |
The only time pointers are used is when the param is optional and does not have defaults. That means majority cases (each combination is case) are going to be non-pointers. Also. path param will never use pointers. Why make the performance worst for majority of the cases when you can just target the minority case |
Like you said headers still need more work, and adding many parts can get complicated. You may want to create a PR to target optional query parameters with no defaults and convert that to a pointer first. In this case, you wouldn't need to change the Parse* functions. Small and concise PRs are appreciated. |
Path params are required so no need to check for nil in this PR Revert a time parsing fix that is now in a separate PR
for optional query params with no default values you can change the code from booleanTestParam, err := parseBoolParameter(
query.Get("boolean_test"),
WithParse[bool](parseBool),
)
if err != nil {
c.errorHandler(w, r, &ParsingError{Err: err}, nil)
return
} var booleanTestParam *bool
if query.Has("boolean_test") {
param, err := parseBoolParameter(
query.Get("boolean_test"),
WithParse[bool](parseBool),
)
if err != nil {
c.errorHandler(w, r, &ParsingError{Err: err}, nil)
return
}
booleanTestParam = ¶m
} That's the only change required. You can revert all the parse* functions in router.go |
so for every type there are 2 cases, where optional non-default is handled separately
|
This one gets tricky for |
Also, that |
Without adding a new JSON field, each type would now have 3 cases for Query, Header, and FormParams. I know right now this would only be targeting Query params, but the other two also need to be addressed soon. Take a look at the below for what this looks like for the
This ends up being a lot of extra branching logic to not return pointers from the parsing functions. I really don't want to add 3x the code and branching into The other option I explored was using a Another option I went with for awhile until I decided it just wasn't readable or maintainable enough as it is doing some harder to follow templating when compared to making the parsers return a pointer.
I scrapped the above options before fully implementing them so I did not run the OpenAPI tests or my company's tests against them. |
Please see https://github.com/OpenAPITools/openapi-generator/pull/17051/files Any review points are appreciated |
@icubbon @lwj5 Thanks for working on this issue. I believe I face a similar problem in terms of root cause, but different manifestation. I believe the problem is not really with the parsing logic, but a bit more fundamental: the generator generates invalid type for optional non-nullable parameters. I wanted to ask @icubbon if you happen to had explored this alternative. Being a bit more specific: example:
would incorrectly generate
instead of
This causes problems both for request parameters and also for responses. In case of request parameter, one cannot distinguish if In case of response, similar problem exists. There is no way to serialize zero value fo What do you think? |
Hi @furiel, just set the field as nullable and it will be a pointer |
@lwj5 I see, but doesn't that mean a different protocol then? My understanding is optionality and nullability are orthogonal concepts. Optionality means keys can be omitted. It does not set restrictions to the values. In use cases where nil is not an interpretable value for a domain, and users would prefer not sending nil for signalling optionality: optional + non-nullable is the intended combination, right? |
Yes, optional and nullable are orthogonal. You are right, if you require ability to omit values, setting optional is the correct call. But setting this does not mean the that the value is able to take on a null or in the case of Go, nil value. It is supposed to make the value untouched. I think openapi specs 3.1 does make it clearer. The nullable key is removed and instead you have to explicitly state the types, a null + a concrete type (e.g., null + string). Yes and your last part paragraph is absolutely right. |
I do not have time this month to clear up the PR so probably only in Dec I can clean it up |
@furiel Great question! Yes, I've had to tackle this same exact problem for Models. Here's an example of this where
I would like to eventually port the Go-Client Models into the Go-Server since they are more functional in my opinion. |
Thank you both for your insights. |
When a parameter is optional, make a best effort to make a parameter that wasn't provided
nil
versus the empty/0 value.The concept of this change was to make all the parsing of parameters return a pointer to the value, that way a
nil
could be returned if the input wasn't provided.Header Parameters and Path Parameters can still use more work as it is possible to get the underlying
map
of params to check if the value was provided.Snapshot the existing Go-Server into a new generator called
Go-Server-Deprecated
PR checklist
This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
These must match the expectations made by your contribution.
You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example
./bin/generate-samples.sh bin/configs/java*
.For Windows users, please run the script in Git BASH.
master
(upcoming 7.1.0 minor release - breaking changes with fallbacks),8.0.x
(breaking changes without fallbacks)@wing328 @lwj5