-
Notifications
You must be signed in to change notification settings - Fork 17.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
proposal: Go 2: equality between an interface and nil should by default compare only by value (and not also type) #60786
Comments
Note that as discussed at https://go.googlesource.com/proposal/+/refs/heads/master/design/28221-go2-transitions.md#go-2 it is unlikely that there will ever be an incompatible Go 2.
I don't fully understand this. If If my understanding is correct, then personally I think this would just trade one kind of confusion for another. |
Yes.
I have not seen any real world use cases where such check would be used for interface and not its value. I have seen this comment though. Is this what you had in mind? Or is there more? Thanks for this list. I have not seen most of them, but I have read through all of them now. It is really interesting to see all this lively discussion. After reading all that I got convinced that the right (and not breaking the language too much) approach would be to do something like #24635, but even simpler: I would propose to have if err != nil {
...
} to be written like: if err? {
...
} And: if err == nil {
...
} to be written like: if !err? {
...
} I think this is better than alternative proposals because:
I can open another proposal if this seems reasonable? |
My concern is that I think that adding an operator may be missing the point but I also believe that fixing the issue is going to be more involved. The issue seems to be that, in the case of interface values, we use equality for what ought to be a type assertion against Maybe we could also have another interface/type for typed nils that would allow to assert whether an interface holds a nil value. That would be the set of all typed nil values. Then, we see that currently, nil equality is used two ways:
The reason behind your issue is that we are missing assertions against typed nils for interface types, if I'm not mistaken. So I tend to agree with you overall. Said differently, there should be a difference in asserting whether an interface is empty or whether it contains a value that points to nil. In both cases, we are not actually checking that an interface value is actually a nil something... We are checking that it contains a nil something. So maybe |
Isn't this current
And that could be new |
It is but maybe it can be changed without it being a breaking change so that it would then assert that an interface contains any kind of nil? Or if it breaks, perhaps a migration strategy could be found?
If we assume that the above is possible, So it's kind of trying to flip the idea on its head. |
I think I understood @ianlancetaylor that changing semantics of existing code is a no-go for Go 2, see https://go.googlesource.com/proposal/+/refs/heads/master/design/28221-go2-transitions.md#go-2 -> programs should continue to work the same, or they should fail compiling. Silently changing semantics is a no-go. So because of this I am proposing
I realized that while I agree that disjoint assertions would be best, again, for the sake of common case I would prefer in fact that If you then do need to know if interface is not nil but if it contains nil pointer, you can do |
That's the whole question. It's a bit similar to the changes being made to for loops. Do people actually think that the equality operator on interface types simply checks that an interface value is empty or do they think that it checks whether the boxed value is nil? If that makes sense and can be done, that would avoid introducing any sigil. |
It would be interesting to change |
Also relevant comment from @DeedleFake in this issue #60933 (comment) I actually hadn't realized that non interface values could be compared to interface values. A typed nil pointer and its boxed value would now return Anyway, a hunch won't replace hard evidence... |
I have occasionally written code that just uses the invocant as a placeholder for despatch, not holding any useful state, and those cases are tidier using typed nils, without actually instantiating a (useless) object. Such code would indeed be broken by having |
@kurahaupo: My original proposal was that there is a difference between
That comment made me thinking that maybe the original issue here is the non-explicit casting which happens here? The example code from FAQ should be made invalid: func returnsError() error {
var p *MyError = nil
if bad() {
p = ErrBad
}
return p // Will always return a non-nil error.
} And would require:
And that could then be the place that somebody does:
So the idea here is that explicit casting gives developer a place to think if they really want to return typed nil. |
@mitar it's not really a conversion. It's simply the normal assignment rules to interfaces (error is an interface). "If" we could change however, one benefit would be that a boxed typed nil would return true when compared to nil which makes implementing custom errors such as error lists (as a slice of error values) much more convenient. @kurahaupo care to share that code if possible? |
@atdiar unfortunately no, as the code belongs to a previous employer. But for example, we had a case where we needed to keep several versions of a code library available, selectable at run-time. Since the code had been written as functions rather than methods, after conversation to methods there was no actual use for the invocant, so we just used typed nils. And we needed to confirm that a version has been selected by checking that the interface was not nil. |
@kurahaupo ok, thanks still, no worries. There are still ways to circumvent this and that would have had to be done anyway and perhaps that it's even the proper migration path. (e.g. current nil checking would have to be transformed into a nil type assertion when going from old go semantics to new go semantics) That's speculative on my end but a Otherwise the old semantics would be used. Old code should run as before. Which means compiling with a newer toolchain would also be a kind of semantics-preserving rewrite. Just an idea, I reiterate that it's speculative on my end. |
Just to clarify:
Nothing changes unless someone wants to bump the toolchain version in go mod. In which case it may require an automated rewrite to translate the old semantics into the new semantics. Otherwise a newer compiler should be able to use older libraries without problem, generating instructions that are equivalent to an implicit rewrite.
Pros of such transition:
So basically, can't change semantics and break code, but should be able to transition semantics mostly automatically. Cons: Ideally, most usages of a == nil would probably not require a rewrite to a.(nil) but not sure an automated rewrite can differentiate. This is mostly an issue when someone wants to bump the version of the toolchain dependency for a library. I don't see a way to statically determine whether a typed nil is assigned to an interface value easily. Probably feasible using the cfg in a conservative way that has good enough precision. |
@atdiar I am not sure if you read documents @ianlancetaylor linked, but there is clear that it is a no-go for Go 2 to have something which silently changes semantics so that any program just starts misbehaving without a compiler error. The document goes pretty well into details why "toolchain version" also does not work out. This is why (unless somebody shows some hard data like that no public package fails with this semantic change) I think the only reasonable way is to introduce some new syntax like |
The original proposal here is not backward compatible, so we aren't going to adopt it. The modified suggestion of using Therefore, this is a likely decline. Leaving open for three weeks for final comments. |
Silent incompatibility is a no-go, but would the following pass muster?
Combining these, we get
|
I guess I should fill in a few more blanks:
|
@kurahaupo, so current |
@mitar When I say The problem with literally writing I suspect something like this might be preferred by most people:
There is some subtlety to all this. Currently constants only have a static type, which (i surmise) is why casting What I'm proposing would necessitate the ability to define a constant whose value is an interface value (a type+pointer pair). Such a value could be assigned to variable whose type is an interface, but would not be assignable to a variable whose type is an ordinary pointer (unless you use a type assertion cast). At no point have I suggested using a typeswitch on an error value; I agree, that would indeed be a bad idea. (¹ not currently valid) |
@kurahaupo Anything that includes "make something that is currently common illegal" is a non-starter. See https://github.com/golang/proposal#language-changes. |
@zephyrtronium rather than the compiler rejecting it as an error, it could just be deprecated in the style guides. What about just issuing a warning? |
In Ruby, a |
As mentioned above, this would break |
@DeedleFake So you are saying that instead of my proposed |
I'm suggesting that if !err.(nil) {
// ...
} |
That seems reasonable, but I'd just like to highlight what I actually said:
Could I please have feedback on both suggestions in that sentence please? |
Go doesn't do warnings. From the Go FAQ:
|
@mitar Sorry for the late answer. Yes I've read it. toolchain version does not work out for language removals. What I described above is simply a way to transition semantics. It is backward compatible. Even further, such automated rewrites would be limited to libraries that need to have their minimum toolchain dependency version bumped up. Otherwise, ti doesn';t change. So, I don't think there would be much of a problem here.
What I exposed above should work without failing. It preserves semantics. People should be able to use Then if they want the old semantics for some reason, they will just write [edit] The one caveat though is that one should know which toolchain is required when making additions to a library, so that they know which semantics are in use. If however go.mod says go1.28, the semantics would be the newer ones. To migrate a library from a go1.20 to go1.28requirement, the rewrite tool can be used for safety. Or go.mod can be updated manually if there is no reliance on go1.20 specific semantics. (the code won't change). Basically, the language can evolve as long as all ancient code can be expressed as newer code automatically. (precludes removals and mere redefinitions). The onus is still on coders to know which language/toolchain version their library depends on. Adding code that uses go 1.28 semantics to a codebase that uses 1.20 semantics may be wrong. But that's not a backward-compatibility issue. But to be fair, It might not be that great, it might require a clearer difference between new code and old code so that, old code can be turned into new code, but new code cannot be added to old code. The reason being that although language can be versioned at the library level, in the wild, it might be difficult to attach such difference. Perhaps for the present case it's not a huge issue but in general, I think that would be problematic still. |
@fzipp thanks for the explanation. It's been many years since I wrote Go for a living, and I'd forgotten that. (I had also forgotten that (unlike in C) Go casts require brackets around the value, so some the examples in my proposed suggestion for "nil as a type" look rather odd.) |
Thinking about it some more, the approach I've suggested above is a substitution + a redefinition. However, for this one particular case, I think that the tradeoffs can be worth examining because it can also be considered a long-standing bug. It's not clear to me why:we have the below: package main
import "fmt"
func main() {
var p *int
var v any = p
a, b, c := (p == nil), (p == v), (v == nil)
fmt.Println(a, b, c) // true true false while we would ideally want it to be true true true
} https://go.dev/play/p/j0TMQ1oPvo7 I know the goal is not necessarily to optimize for mathematical purity etc. and that the stability of the ecosystem probably matters more here, but I think that the loss of the transitivity wrt |
The reason that prints "true true false" is that the name |
I had seen that issue but I must admit I don't understand how it fixes the problem. It doesn't seem to be merely an issue with nil but also an issue with the meaning of Comparing a value to its boxed value is always transitive except when the value is a nil. It would be less complex if comparing to nil was always returning true for any nil, whether typed or untyped. It simplifies interface comparison by always making it a boxed value comparison instead of making cases (nilpointer, nilinterface, non-nil interface etc...) In a sense, we agree, it's the meaning and behavior when encountering untyped nil as the type of the nil value that may need fixing. |
From what I can tell, the issue solves the problem by eventually making Edit: Specifically not slices, maps, channels, or functions. Odd. |
Because To put it another way, you say that |
@ianlancetaylor I see. And that's the issue I think, if it was made to be a comparison to a nil unboxed value, there wouldn't be a problem then. (since nil is not differentiated into nil interface or nil unboxed value in client Go code, it's just a predeclared identifier) And to still have the former behavior which is a comparison to a "nil-interface value" , one way (there are probably other ways or other possible notations), could be to make it a type assertion against untyped nil type. Or have another identifier for the zero value of an interface (that is not nil). Still most cases of err == nil should work as-is with the change nil always meaning unboxed value. To note though that it's really meaningful a suggestion if it is on the good side of the tradeoff balance... That, I'm not sure. It's still a redefinition. |
No change in consensus. |
It turns out that compiler modifications to nil detection behaviour do not affect code compatibility https://go.dev/play/p/UVwrXyiWbh7 https://go.dev/play/p/afoh5UraI6t
|
Author background
I consider myself an experienced Go programmer with many years of programming experience in Go and many other languages (Python, Haskell, C, etc.).
Related proposals
The issue of typed nils have been discussed before, I have found at least:
I think they all try to address the same pain point which many Go programmers experience. They also both very well demonstrate the large scope of the issue (many StackOverflow questions, many google-nuts threads, many blog posts around the web on the issue, even FAQ has an entry on it and Google search reveals many pages linking to that entry.
This proposal is trying to propose a different approach at addressing the issue and I could not find one proposing this exact solution, so I am opening a new proposal.
Proposal
TLDR: I am proposing that equality with (and meaning of) nil is handled differently in Go 2.
I will be using syntax used in the FAQ entry where an interface has type T and a value V. Then, I am proposing that:
I think this addresses most if not all issues people have with nil comparison and is also what most people intuitively expect. I am not aware really of use cases where one would require comparison with explicitly typed nil, but for those use cases I propose that an explicit interface type casting is required:
So only
nil
is a "wildcard nil" while typed nils behave like they do currently.Costs
I think this would make Go much easier to learn as it is more intuitive behavior, as demonstrated by many issues programmers are having with this.
The main cost I think might be in how nil is represented in memory to allow for explicitly typed vs non-typed nil comparison. I do not know enough details about this though to evaluate it.
I think most existing programs would continue to work as I have personally not yet seen code which relies on nil comparison with nil where a type would matter. But I might be mistaken.
Compilation time and run time changes should be negligible here, I suspect.
The text was updated successfully, but these errors were encountered: