Open randall77 opened 3 years ago
A few minutes of searching github turned up a few instances.
Both of these look like very confused code, where this vet check might at least point someone in the right direction:
https://github.com/hedzr/assert/blob/e76ed4c3b339a79c3b788bc3fdf4605511203e14/equal.go lines 106-110
https://github.com/cuirixin/phoenix_corelib/blob/fb4035076004c2eca44d4840d390191e052a4d7b/libs/gotransformer/gotransformer_test.go line 19, 37, ...
This one is probably someone who hit this bug, fixed their code, and left a comment about it:
https://github.com/coreos/vcontext/blob/ee043618d38dc1daf804fe352433b5ce2428ea32/validate/validate_test.go lines 212-221
Another example (now fixed) at https://github.com/go-openapi/validate/issues/137.
Perhaps we should define that when you pass a reflect.Value to DeepEqual, DeepEqual uses that directly instead of calling reflect.ValueOf on it. Then instead of warning about broken code we just make it not broken.
That would work in my example case. Not sure what the right behavior is for, e.g., a struct with a reflect.Value
-typed field.
I agree about not applying that to reflect.Value-typed fields, but vet was not going to catch those either.
We have some history here where fmt.Print interprets a reflect.Value by using what it contains.
case reflect.Value:
// Handle extractable values with special methods
// since printValue does not handle them at depth 0.
if f.IsValid() && f.CanInterface() {
p.arg = f.Interface()
if p.handleMethods(verb) {
return
}
}
p.printValue(f, verb, 0)
@ianlancetaylor says he ran into this recently due to a Go 1.16 internal change and it was not at all clear given the call (like in Keith's example at the top) that there was even a problem. So we should do either the vet change or the "make it work" change.
I'm leaning toward "make it work".
This proposal has been added to the active column of the proposals project and will now be reviewed at the weekly proposal review meetings. โ rsc for the proposal review group
Does anyone object to just making DeepEqual handle reflect.Value arguments by interpreting what the Values represent? (That is, it would avoid calling reflect.ValueOf on those arguments and then proceed as before.)
avoid calling reflect.ValueOf
typo? avoid calling .Interface()
?
Does it apply to reflect.Value
values with interfaces as underlying values?
And what about the underlying values of the underlying interface values are also reflect.Value
values,
And ...
avoid calling reflect.ValueOf
typo? avoid calling .Interface()?
It's not a typo. The current code is
v1 := ValueOf(x)
v2 := ValueOf(y)
The suggestion is to change that to something like
var v1, v2 Value
xv, xok := x.(Value)
yv, yok := y.(Value)
if xok && yok {
v1 = xv
v2 = xv
} else {
v1 = ValueOf(x)
v2 = ValueOf(y)
}
Does it apply to reflect.Value values with interfaces as underlying values?
No.
And what about the underlying values of the underlying interface values are also reflect.Value values,
No.
And ...
No.
OK, I misinterpreted it affects user code. It affects std lib code actually.
But is it required both of the arguments are Value
? Should it be like
var v1, v2 Value
if xv, xok := x.(Value); xok {
v1 = xv
} else {
v1 = ValueOf(x)
}
if yv, yok := y.(Value); yok {
v2 = yv
} else {
v2 = ValueOf(y)
}
Good question.
IMO reflect.Value
should be treated as analogous to a pointer type. We treat &x
as semantically different from x
,ยน so we should also treat reflect.ValueOf(x)
as semantically different from x
itself.
@ianlancetaylor says he ran into this recently due to a Go 1.16 internal change
Yeah, we just hit this as well with the following code in a validation library that was attempting to check zero value:
reflect.DeepEqual(reflect.Zero(reflect.TypeOf(data)), reflect.ValueOf(data))
As it turns out, what it intended to do with a zero-value condition was incorrect, but the incorrect branch was never triggered before.
We'll deal with fixing that issue in that particular library, but is it acceptable that the following code has a different result in go1.16 than it did in all prior go versions?
func main() {
data := ""
fmt.Println(reflect.DeepEqual(reflect.Zero(reflect.TypeOf(data)), reflect.ValueOf(data)))
}
Looks like https://go-review.googlesource.com/c/go/+/192331 was likely the relevant change
@liggitt
Yes, it is unfortunate that it changed in 1.16 but the code is relying on an implementation detail of reflect.Value
.
Kind of like &struct{} == &struct{}
, it all depends on whether the implementation dedups two zero values into identical reflect.Value
s, or just two equivalent ones.
// To compare two Values, compare the results of the Interface method. // Using == on two Values does not compare the underlying values // they represent.
==
is the same as DeepEqual
in this situation (which is what this issue is proposing changing).
Still probably worth a mention in the 1.16 release notes, since the externally visible behavior of reflect.Zero changed. Highlighting problematic comparisons of reflect.Values that would be affected would have prompted us to audit for that.
Still probably worth a mention in the 1.16 release notes, since the externally visible behavior of reflect.Zero changed.
It's only visible because of violations of reflect.Value
's contract. Not sure what you'd put in the release notes about that. Have a suggestion?
Highlighting problematic comparisons of reflect.Values that would be affected would have prompted us to audit for that.
This issue (and #18871) was originally about highlighting problematic comparisons, hopefully more robustly than a release note would. i think we'll just end up fixing the behavior to do the right thing instead.
It's only visible because of violations of
reflect.Value
's contract. Not sure what you'd put in the release notes about that. Have a suggestion?
Something like what was described in https://golang.org/doc/go1.10#reflect, describing the change made to the reflect package and describing potential impact for callers relying on incorrect or unspecified behavior. Perhaps something like:
The
Zero
function has been optimized to avoid allocations in many scenarios. Code which incorrectly compares the returnedValue
to anotherValue
item using==
orDeepEqual
can now evaluate totrue
, where previous versions ofgo
always evaluated tofalse
. To compare twoValues
, compare the results of theInterface
method. Using==
orDeepEqual
on twoValues
does not compare the underlying values they represent.
Change https://golang.org/cl/300992 mentions this issue: [release-branch.go1.16] doc: describe how Zero optimization might change results of incorrect reflect.Value comparison
Change https://golang.org/cl/302269 mentions this issue: doc: describe how Zero optimization might change results of incorrect reflect.Value comparison
Given that everyone on Go 1.16 is already broken and we're not hearing a big uproar, it seems like a vet check and keeping the ValueOf semantics simpler is a better path forward.
Based on the discussion above, this proposal seems like a likely accept. โ rsc for the proposal review group
What about also flagging use of ==
on reflect.Value
s? That would fix #18871 but avoid the main objection there, which was use of reflect.Value
as a map key for sets of reflect.Value
s.
We'd have to allow comparing to the zero Value
(aka reflect.Value{}
) as Dominik noted.
It seems OK to put the == check in as well, with an exception for reflect.Value{}. If it breaks too much, we can always back it out. Will leave this in likely accept for another week.
No change in consensus, so accepted. ๐ This issue now tracks the work of implementing the proposal. โ rsc for the proposal review group
Change https://golang.org/cl/308209 mentions this issue: go/tools: add vet check for direct comparion of reflect.Values
Change https://golang.org/cl/308769 mentions this issue: text/template: compare reflect.Value instances differently
Unfortunately, handling ==
runs up against some code in the stdlib. There are 3 kinds of false positives:
zero := reflect.Value{}; x == zero
We can handle x == reflect.Value{}
, but with the above we'd have to detect that there are no other writes to the zero
variable to squash the report. That's difficult. (in text/template
)var singleton = reflect.ValueOf(...a value of some otherwise unused type...) ; if x == singleton
Code builds a guaranteed-unique reflect.Value
by using a unique type that's used nowhere else, and uses that Value as a special marker. (in text/template
)v.Elem().Elem() == v
When descending though a data structure using reflection, we sometimes want to know if we've reached a place where we've been before. (in encoding/json
)These false positives seem like they may be common enough to warrant rolling back the ==
part of this proposal.
I think maybe we do just DeepEqual
. Cases 3 at least make no sense if done with DeepEqual
. Case 2 also seems unlikely. Case 1 could come up using DeepEqual
, I guess, but the fix is easy for case 1: just use IsValid
instead.
Thank you everyone for the discourse and for the approvals. So @randall77 your work in implementing this static analyzer in golang.org/x/tools/go/analysis/passes/reflectvaluecompare, got it done, I am just now bringing it into the standard library and it has flagged problems in encoding/json and text/template
encoding/json/decode.go:475:46: avoid using == with reflect.Value
text/template/exec.go:813:6: avoid using != with reflect.Value
Change https://go.dev/cl/626116 mentions this issue: encoding/json, text/template: use reflect.Value.Equal instead of ==
Change https://go.dev/cl/626397 mentions this issue: cmd/vendor: update to latest dependencies
Change https://go.dev/cl/626398 mentions this issue: all, cmd/vet: bring in reflectvaluecompare static analyzer
This code doesn't do what you think it does:
It compares the internal details of the two
reflect.Value
structs, not their contents. The correct code would bereflect.DeepEqual(x.Interface(), y.Interface())
.I don't see any reason why you'd ever want to use
DeepEqual
on areflect.Value
. Hence no false positives.If you really wanted to compare two
reflect.Value
s (but you shouldn't),==
is equivalent toDeepEqual
.Seems like an easy mistake to make (see #43986 ). Not sure how common it might be, but I suspect it might be common enough to warrant a check.