-
Notifications
You must be signed in to change notification settings - Fork 18k
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
flag: FlagSet should be marked as noCopy to prevent buggy behavior #51507
Comments
cc @robpike per https://dev.golang.org/owners for flag, and @matloob @timothy-king for cmd/vet |
Yet another option is to change the field |
https://go-review.googlesource.com/c/go/+/390234 fixes the test code. |
Thoughts:
|
Responding to your second point: note that I'm not proposing an entirely new vet check, but rather adding the Doing a bit of research on this is relatively easy, though. Using GitHub's new code search, we can already find over a hundred cases. It's very likely that not all of them call |
As for your third point: if we wanted to explicitly allow the user to make a copy of a FlagSet, I think we would need to add a method for it that does so carefully (e.g. by also not sharing its pointer-like fields like the maps and slice). I personally don't think it's going to be needed, but we can also try the |
I had a test that called
flag.NewFlagSet
followed byFlagSet.SetOutput(io.Discard)
. Yet, in some cases, the flagset would print straight to stderr, ignoring my instructions.I reduced the code into this reproducer: https://go.dev/play/p/KgDpqcSeR2c
When reduced, it's relatively straightforward to see what is going on in the non-pointer case:
NewFlagSet
, which creates pointer P1, and setsf.Usage = f.defaultUsage
with P1 as the method receiver parameter.NewFlagSet
, resulting in a variable of non-pointer typeFlagSet
, making a shallow copy of the struct.SetOutput
, a method with a pointer receiver. It's equivalent to calling(&f2).SetOutput
; note that this is a pointer to the stack, P2. P2'soutput
field is updated, but not P1's.f2.Parse
, we ultimately end up callingf2.Usage
on P2, which is still to P1'sdefaultUsage
method.output
field to print the usage, not P2's.Arguably this is my mistake for dereferencing
NewFlagSet
, and I tend to agree. The buggy test was, in fact, in the standard library itself:go/src/flag/flag_test.go
Line 289 in 45f4544
I think we should add a
noCopy
field to theFlagSet
struct to make vet error when users make full copies of flagsets. In some cases that can be fine, such as when the user doesn't callSetOutput
, but in general I think it's confusing and can easily lead to bugs. And, most importantly, I don't see why anyone would ever want to make a shallow copy of aFlagSet
like this. If we wanted to support a full "clone" operation, it would have to be a new feature without theoutput
field bug above.Another option would be to remove the
f.Usage = f.defaultUsage
line fromNewFlagSet
, so that then P2's call to theusage
method would go intoif f.Usage == nil { f.defaultUsage }
, calling P2'sdefaultUsage
rather than P1's. However, I think this could be a backwards incompatible change, asf := NewFlagSet(); f.Usage()
works today, and it would start panicking with such a change.Yet another option is to document this pointer dereference gotcha in the
FlagSet
docs. Certainly better than nothing, but I reckon that a vet check is still a much better outcome.The text was updated successfully, but these errors were encountered: