Skip to content
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

Closed
mitar opened this issue Jun 14, 2023 · 38 comments
Labels
LanguageChange Proposal Proposal-FinalCommentPeriod v2 A language change or incompatible library change
Milestone

Comments

@mitar
Copy link
Contributor

mitar commented Jun 14, 2023

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:

  • (T=nil, V is not set) == nil => true (what currently holds)
  • (T is any, V=nil) == nil => true (proposed addition)

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:

  • (T=nil, V is not set) == t(nil) => true if t == nil
  • (T is any, V=nil) == t(nil) => true if t == T

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.

@gopherbot gopherbot added this to the Proposal milestone Jun 14, 2023
@ianlancetaylor ianlancetaylor added LanguageChange v2 A language change or incompatible library change labels Jun 14, 2023
@ianlancetaylor
Copy link
Contributor

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.

(T is any, V=nil) == nil => true (proposed addition)

I don't fully understand this. If T is int, then V can't be nil. It can only be a value of type int. Does this mean that if T is some type whose value can be set to nil, and if the value is indeed nil, then using == to compare that interface value to nil will return true?

If my understanding is correct, then personally I think this would just trade one kind of confusion for another.

See also #21538, #24635, #24682, #27890, #30294, #30865.

@mitar
Copy link
Contributor Author

mitar commented Jun 14, 2023

Does this mean that if T is some type whose value can be set to nil, and if the value is indeed nil, then using == to compare that interface value to nil will return true?

Yes.

If my understanding is correct, then personally I think this would just trade one kind of confusion for another.

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?

See also #21538, #24635, #24682, #27890, #30294, #30865.

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 ? operator to work on an interface and it returns true if the interface's value is nil (or if the interface itself is nil). This would allow:

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:

  • It addresses exactly the main pain point of many Go programmers, namely checking err != nil but being surprised when condition is not true.
  • It is shorter than any of alternatives and also existing err != nil, motivating people to adopt it even if they do not know why it is better than err != nil - they could see it just as a syntactic sugar.
  • It could even be introduced in Go 1. No existing program can be valid with this syntax.
  • To me it looks like proposal: Go 2: add kind-specific nil predeclared identifier constants #22729 is going in the direction of adding just nilinterface with main purpose of equality checking. ? does the same, but it is simpler/shorter.
  • Some other programming languages (TypeScript, CoffeeScript) have similar operator to check for null.

I can open another proposal if this seems reasonable?

@atdiar
Copy link

atdiar commented Jun 15, 2023

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 {untyped nil} (which should actually be a first class type then).
If we define a type as holding a set of values, then it would correspond to the singleton comprised of untyped nil that we could call the nil type.

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:

  • to assert against typed nil for non interface types (the specific type is elided)
  • to assert against the (untyped) nil type for interface types

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.
Just that maybe there could be a migration path where the semantics of the equality assertion are not a combination.

Said differently, there should be a difference in asserting whether an interface is empty or whether it contains a value that points to nil.
With the operator you propose, there is still no such difference if I understand properly.

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 T == nil should be true as long as the value is any typed or untyped nil in the first place (the semantics of your proposed operator) and that a T. (nil) should be used to check whether an interface contains untyped nil.
That ship seems to have sailed but perhaps there are ways to recover somehow, don't know.
(could allow us to implement custom error types more easily too)

@mitar
Copy link
Contributor Author

mitar commented Jun 15, 2023

Said differently, there should be a difference in asserting whether an interface is empty

Isn't this current == nil?

whether it contains a value that points to nil.

And that could be new ? operator?

@atdiar
Copy link

atdiar commented Jun 15, 2023

Said differently, there should be a difference in asserting whether an interface is empty

Isn't this current == nil?

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?
(depends on whether code has ever relied on an interface containing a typed nil instead of an untyped one before?)

whether it contains a value that points to nil.

And that could be new ? operator?

If we assume that the above is possible,
then we would just have to introduce nil as a first class type and be able to type assert to check whether an interface is empty.

So it's kind of trying to flip the idea on its head.

@mitar
Copy link
Contributor Author

mitar commented Jun 15, 2023

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 ? and an opt-in behavior to use it. People can still use == nil, programs still work as currently, only they might have silent bugs like they have currently. That should motivate them to start using ? instead in new code or update old code to use it. And also because it is shorter, people might anyway prefer it.

whether it contains a value that points to nil.

And that could be new ? operator?

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 ? is defined as "whether it contains a value that points to nil OR asserting whether an interface is empty". Otherwise one would have to write if err? || err == nil which is very verbose.

If you then do need to know if interface is not nil but if it contains nil pointer, you can do x? && x != nil. Verbose, but I do not think this is so common case and I think it might be also very clear what exactly are you testing for.

@atdiar
Copy link

atdiar commented Jun 15, 2023

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.

That's the whole question.
Can the semantics be changed/or transitioned painlessly.

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?
In other terms, is there code that relies on the kind of nil that is boxed by an interface, typed or untyped? (I'm not sure)

If that makes sense and can be done, that would avoid introducing any sigil.
This is a personal opinion but I am quite sigil-averse for legibility reason.

@mitar
Copy link
Contributor Author

mitar commented Jun 15, 2023

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?

It would be interesting to change == nil semantics in the compiler and try to run it over test suite of the compiler itself and packages available on https://pkg.go.dev and see how many break. I also suspect not many would and thus my original proposal above, but we would need hard evidence to support such a claim.

@atdiar
Copy link

atdiar commented Jun 23, 2023

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.
Currently, that makes comparison to nil non-transitive. If (...and that remains a big if) we could change nil comparison for interface types to be a non-interface value comparison (using the boxed value), that would reestablish transitivity.
Possibly a good property to have.

A typed nil pointer and its boxed value would now return true when compared to nil as opposed to the boxed value returning false now.
Depends on whether any current code relies on that behavior in a really meaningful way.
In theory, I doubt it because interfaces don't distinguish between pointer values and values for instance. So essentially, people don't check for interface emptiness but rather for pointer value nilness before calling methods that may trigger a dereference. So the type of nil shouldn't matter.
The current behavior might be described as being similar to an over-restriction in checking for nil, in a sense.

Anyway, a hunch won't replace hard evidence...

@kurahaupo
Copy link

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 SomeInterface == nil ignore the "type" half of SomeInterface.

@mitar
Copy link
Contributor Author

mitar commented Jun 24, 2023

@kurahaupo: My original proposal was that there is a difference between nil and explicitly casted nil like SomeInterface(nil). You could use the later case to do the explicit check if you expect an interface there.

Also relevant comment from @DeedleFake in this issue #60933 (comment)

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:

func returnsError() error {
    var p *MyError = nil
    if bad() {
        p = ErrBad
    }
    return error(p) // Will always return a non-nil error.
}

And that could then be the place that somebody does:

func returnsError() error {
    var p *MyError = nil
    if bad() {
        p = ErrBad
    }
    if p == nil {
        return nil
    }
    return error(p)
}

So the idea here is that explicit casting gives developer a place to think if they really want to return typed nil.

@atdiar
Copy link

atdiar commented Jun 24, 2023

@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?

@kurahaupo
Copy link

@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.

@atdiar
Copy link

atdiar commented Jun 28, 2023

@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 go mod tidy from go1.X to go2.x would turn in a code rewrite from a == nil to a.(nil) to check for emptiness.
(perhaps another dedicated subcommand, update rather than tidy? )

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.

@atdiar
Copy link

atdiar commented Jul 1, 2023

Just to clarify:

  • the goal is to use a == nil in probably 99% of the places it is used today. The problem is that if the semantics need to change, there has to be a way to express the initial semantics.

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.

  • the current semantics for a == nil are equivalent to checking that the dynamic type of the nil pointer is untyped nil (nil type?).
    So this is equivalent to a type assertion a.(nil) where nil denotes the nil type.
    It tests for interface emptiness and its usage would be quite limited even after the semantic transition.

Pros of such transition:

  • it gets rid of the misconception around nil checking and typed nil in interfaces
  • nil equality is now transitive
  • it makes easier to write custom error types and check for nil as is currently done
  • looking forward, if it turns out that interfaces can be generalized into unions and interface types are acceptable as union terms, having a nil type might be required anyway.
    The clear difference between an empty interface and an interface that contains a typed nil will be appreciable.

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.
That would avoid having a.(nil) everywhere even if not required.

@mitar
Copy link
Contributor Author

mitar commented Jul 14, 2023

@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 ? operator which behaves they way we want. I think because if err? is simply shorter it will catch up and be used, while doing exactly what one would expect with comparison to nil.

@ianlancetaylor
Copy link
Contributor

The original proposal here is not backward compatible, so we aren't going to adopt it.

The modified suggestion of using x? to mean "x is not nil and x does not contain a pointer type with a value of nil" introduces a very subtle new operator to the language. While the behavior of non-nil interfaces is indeed confusing for new Go programmers, the solution shouldn't be a subtle new operator that may compound that confusion.

Therefore, this is a likely decline. Leaving open for three weeks for final comments.

@kurahaupo
Copy link

kurahaupo commented Jul 20, 2023

@ianlancetaylor

Silent incompatibility is a no-go, but would the following pass muster?

  1. Make it illegal to directly compare an interface value and a bare nil literal, or at least have it eliciting a warning (expression == nil where the static type of expression is an interface)
  2. Generalise the use of expression.(type) to various places where a type designator would be expected. In particular, in a cast, so that (x.(type))nil evaluates to a nil value of the same dynamic type as x.
  3. Allow nil to act as a type designator in various situations:
    1. as a case in a typeswitch, like switch x.(type) { case nil: ... }
    2. in nil.(type).

Combining these, we get

  • x == (nil.(type))nil as an explicit check for a nil/nil interface value (the same as x == nil now).
  • x == (x.(type))nil as an explicit check for an any/nil interface value, which was the original point of this proposal.
  • Code that currently uses
    if x == nil {
      /*handle nil*/
    } else {
      switch x.(type) {
        case Foo:
          /*handle Foo*/
        case Bar:
          /*handle Bar*/
      }
    }
    
    could instead be written:
    switch x.(type) {
      case nil:
        /*handle nil*/
      case Foo:
        /*handle Foo*/
      case Bar:
        /*handle Bar*/
    }
    

@kurahaupo
Copy link

I guess I should fill in a few more blanks:

  • (x.(type))y throws a type assertion exception if x and y don't have the same dynamic type.
  • (nil.(type))x throws a type assertion exception if x is not nil.

@mitar
Copy link
Contributor Author

mitar commented Jul 20, 2023

@kurahaupo, so current if err == nil would be written if x == (x.(type))nil? Or would you even be required to use switch? Very verbose for a very common pattern. Also note that switch on error types is an anti-pattern and you should be using errors.As instead.

@kurahaupo
Copy link

kurahaupo commented Jul 20, 2023

@mitar When I say x == (nil.(type))nil¹ is the same as the current x == nil, I'm defining its behaviour, not requiring its use. Other ways to write (nil.(type))nil could include (any)nil², any{}³, any.nil⁴, and nilInterface (after declaring var nilInterface SomeInterfaceType).

The problem with literally writing if err == (any)nil is that "any" implies the opposite of what it really means: it's not "a nil value of any type", it's "the nil value of the any type".

I suspect something like this might be preferred by most people:

const NoError = (any)nil
...
if err == NoError { ... }

There is some subtlety to all this. Currently constants only have a static type, which (i surmise) is why casting nil is currently illegal.

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)
(² not currently valid)
(³ not currently valid)
(⁴ not currently valid; see #22729)

@zephyrtronium
Copy link
Contributor

@kurahaupo Anything that includes "make something that is currently common illegal" is a non-starter. See https://github.com/golang/proposal#language-changes.

@kurahaupo
Copy link

@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?

@DeedleFake
Copy link

In Ruby, a nil check is generally done via the .nil? method instead of checking == nil. The above proposed ? operator kind of makes me think of that, but I agree that just the operator alone is a bit too subtle. There was a .(nil) proposed above that I also thought of separately and I think that might be the best option for that route of design. That even kind of makes sense because a type assertion is supposed to give access to the underlying value, and, if #61372 is adopted, it could also be naturally extended to a .(zero) syntax that checks not only for nil but also for the zero value. My one reservation is that the existing behavior of nil with a type switch would contrast with it, but I don't think that's a huge problem. In fact, that might be fixable with a GODEBUG as it's significantly less common and I think that it would be less likely to break in the cases where it is used than simply changing all == nil usages would be.

@ianlancetaylor
Copy link
Contributor

Make it illegal to directly compare an interface value and a bare nil literal, or at least have it eliciting a warning (expression == nil where the static type of expression is an interface)

As mentioned above, this would break if err != nil, which means that it would break every single Go program that exists. We can't make a change that big.

@mitar
Copy link
Contributor Author

mitar commented Jul 20, 2023

@DeedleFake So you are saying that instead of my proposed if err? one would write if err.(nil) but otherwise semantics would be the same? Or should it be if err.(nil) == nil?

@DeedleFake
Copy link

DeedleFake commented Jul 20, 2023

I'm suggesting that v.(nil) would return a bool, so

if !err.(nil) {
  // ...
}

@kurahaupo
Copy link

@ianlancetaylor

As mentioned above, this would break if err != nil, which means that it would break every single Go program that exists. We can't make a change that big.

That seems reasonable, but I'd just like to highlight what I actually said:

Make it illegal to directly compare an interface value and a bare nil literal, or at least have it eliciting a warning

Could I please have feedback on both suggestions in that sentence please?

@fzipp
Copy link
Contributor

fzipp commented Jul 21, 2023

@kurahaupo

or at least have it eliciting a warning

Go doesn't do warnings. From the Go FAQ:

There are two reasons for having no warnings. First, if it's worth complaining about, it's worth fixing in the code. (And if it's not worth fixing, it's not worth mentioning.) Second, having the compiler generate warnings encourages the implementation to warn about weak cases that can make compilation noisy, masking real errors that should be fixed.

@atdiar
Copy link

atdiar commented Jul 21, 2023

@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.

@mitar Sorry for the late answer. Yes I've read it. toolchain version does not work out for language removals.
My suggestion is not a removal however.

What I described above is simply a way to transition semantics. It is backward compatible.
Forward compatibility requires an automatic rewrite tool. But even such rewrite would be conservative.
If someone has proper tests, they could also just bump the toolchain dependency version in go.mod manually.
Most code doesn't store typed nil in interface values.

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.

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 ? operator which behaves they way we want. I think because if err? is simply shorter it will catch up and be used, while doing exactly what one would expect with comparison to nil.

What I exposed above should work without failing. It preserves semantics.
This is an duping+redefinition+substitution operation.
The advantage is that we won't need to introduce some new operator with some sigil.

People should be able to use if err! = nil as they intuit (any typed nil value and nil typed value will fail this test now).

Then if they want the old semantics for some reason, they will just write if !err.(nil){...} instead which tests whether the error interface value is empty.
But that's not what people want to check in general so that would be rare.

[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.
For instance, if go.mod says go1.20, err != nil uses the current semantics. Even if I add code, the sematics will remain the same. But that's something to be aware of.

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).
It works here because a go1.20 toolchain cannot compile go1.28 code but a go1.28 toolchain can automigrate/rewrite go.120 code and compile it.

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.

@kurahaupo
Copy link

@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.)

@atdiar
Copy link

atdiar commented Jul 26, 2023

Thinking about it some more, the approach I've suggested above is a substitution + a redefinition.
It still has the drawbacks of redefinitons for code that could exist at large (only libraries are versioned, but code snippets are not).

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 nil is a formalisation/artefact of why the behavior can be surprising.

@ianlancetaylor
Copy link
Contributor

The reason that prints "true true false" is that the name nil means different things each time it is used. That is unfortunate. But the fix is not to make the meaning of v == nil even more subtle and complicated than it already is. See #61489 for more discussion of this point.

@atdiar
Copy link

atdiar commented Jul 28, 2023

@ianlancetaylor

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 == operator (probably correlated)

Comparing a value to its boxed value is always transitive except when the value is a nil.
Hence the entry in the FAQ.

It would be less complex if comparing to nil was always returning true for any nil, whether typed or untyped.
This seems to be people's natural inclination too.

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.

@DeedleFake
Copy link

DeedleFake commented Jul 28, 2023

@atdiar

From what I can tell, the issue solves the problem by eventually making nil incompatible with pointers. As in, p = nil would be an error. Instead, it adds null to indicate a null pointer specifically, and presumably slices, maps, channels, etc., so that if you have an interface v, v == nil would always mean that the interface was nil while v == null would mean that the value inside the interface was null. That way there would be no confusion about which was meant and you could easily pick which one you wanted to test for. It's designed only to fix the problem of confusion around it, not to change how the comparison works.

Edit: Specifically not slices, maps, channels, or functions. Odd.

@ianlancetaylor
Copy link
Contributor

Comparing a value to its boxed value is always transitive except when the value is a nil.

Because nil has multiple meanings. You are attributing that to ==, but I don't think that's an accurate way of describing the issue. In err == nil the nil means the nil interface. Therefore the == operator is comparing two interface values. When comparing two interface values, == is true if the types stored in the interfaces are the same, and if the values stored in the interfaces are the same. When comparing a nil pointer stored in an interface to a nil interface, the types are not the same.

To put it another way, you say that == is transitive except when value is nil. That is not correct. Using your terms, == is transitive except when comparing to another interface value.

@atdiar
Copy link

atdiar commented Jul 28, 2023

@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.
To make it backward-compatible requires some work.
Is it as important and implementable without friction as the change in for-loops? No clue.

@ianlancetaylor
Copy link
Contributor

No change in consensus.

@ianlancetaylor ianlancetaylor closed this as not planned Won't fix, can't repro, duplicate, stale Aug 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LanguageChange Proposal Proposal-FinalCommentPeriod v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

8 participants