Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I don't think this is a different (and thus not "worse") case--it seems like a specific instance of the same case we've been discussing. Ultimately it comes down to this:

    error.Error((*CustomErrorType)(nil))
You're putting a nil `*CustomErrorType` into an `error` interface.

This definitely causes some people confusion and results in bugs, but the question under discussion is whether the original claim ("an interface can be simultaneously nil and not nil") is true. Of course, it's not true as this example illustrates--the interface is not nil but the underlying data (of type `*CustomErrorType`) is nil.



I think there is a point to be made here.

Let's say I have the following code:

    func open() (*os.File, *os.PathError) { return os.NewFile(1, "stdout"), nil }

    // in main
    // (some lines omitted)
    fi, err := open()
    if err == nil { fmt.Println("no error")
So, my question is, does that print 'no error' or not?

Normally, you would say "no, it does not, 'err' is a nil '*os.PathError' so it does not.

However, I didn't mention what lines were omitted, and those lines actually matter!

    // main
    _, err := os.Executable()
    
    fi, err := open()
    if err == nil { fmt.Println("no error")
Having that line before it changes the output, and the error is no longer detected as nil (since it's now a non-nil error interface referencing a nil struct).

Why does this matter? Well, it's extremely common in go (idiomatic in fact) to reuse the "err" variable name, and it's also extremely common to use it in multiple assignment with ":="... where despite using the declaration syntax, you can re-assign to existing variables without their types changing.

I think that's what the parent was pointing out: a combination of type inference and automatically "converting" a struct into an interface for you (both in assignment, as I showed above, and when you return a value, as shown in the parents example) means that you can end up with this issue without doing anything as obviously wrong.

In other languages, returning concrete error types means the caller has more type-system information and is good. In go, returning concrete error types gives the caller more information, but opens them up to accidentally implicitly casting it to an interface and blowing their foot off.

As such, we have almost completely untyped error returns, and any time a gopher thinks "this is dumb, why don't I use the type system for this?" they're actually just loading a shotgun and pointing it at their foot.

> I don't think this is a different (and thus not "worse") case

All the above was just to say, I think it's the same case, but it's pointing out that it's "worse" because it interacts poorly with other parts of the go language spec.

Playground: https://go.dev/play/p/4MYEZr8D5Qv


I agree that this is a problem, but the problem is not that interfaces have multiple kinds of nility. The issue here is that you’re putting a pointer type into an interface and it’s easy to forget that the runtime type is effectively a reference to a reference, both of which can be nil.

Any preferred solution (which will never happen due to compatibility guarantees) would be to eliminate zero types altogether and use Rust-like enums instead.


The problem I'm highlighting is not that I, the programmer, am forgetting it, but that it can change depending on context without the specific line changing.

Go's type inference can take:

   x, err := 1, someNilStruct
And have that mean either "err is now a nil struct type" or "err is now a non-nil interface". Which of those things it does depends on the types of existing variables in scope or function signatures.

On the other hand, in rust if I type "let err = rhs", that will _always_ introduce a new variable named err that is the same type no matter what lines of code are above me.

The interaction there in go is due to poor interactions between features other than zero types I think, so I don't see dropping all zero types as solving it.

I also don't think zero types are analogous to rust enums, but rather to rust's default trait (which I do vastly prefer over zero types).


> Go's type inference can take: `x, err := 1, someNilStruct` And have that mean either "err is now a nil struct type" or "err is now a non-nil interface". Which of those things it does depends on the types of existing variables in scope or function signatures.

You’re mistaken here. `err` will always have exactly the same type as `someNilStruct`. Specifically you’re confusing the variable declaration syntax with the variable assignment syntax. `err := foo` is the same as Rust’s `let mut err = foo` in that `err` will have exactly the same type as `foo` no matter the types of other variables. `err = foo` behaves exactly the same in Go as in Rust in that you’re just reassigning the value of `err` but it’s type doesn’t change.


The spec says

> Unlike regular variable declarations, a short variable declaration may redeclare variables provided they were originally declared earlier in the same block (or the parameter lists if the block is the function body) with the same type, and at least one of the non-blank variables is new.

This suggests that a redeclaration as part of a short assignment will not change the type of the previously-defined err. is the spec incorrect?

Edit: Here's a demonstration of this sort of thing https://go.dev/play/p/Po_Uy85ORN_L. You can uncomment line 22 to change the meaning of the assignment on line 23 and cause the program to segfault.


The playground link I have two comments ago shows exactly the behavior I describe, that multiple assignment exhibits exactly the behavior I describe.

Click through to the playground link and add some 'fmt.Printf("%T\n", err)' lines here and there to see what I mean.


Okay, I've accidentally put a pointer type into an interface, now what? How do I check that the inner value is nil/not nil? Mmmmm?

It's a defect in the language, plain and simple: if a pointer-to-type implements interface, then converting a nil pointer to type to the interface should result in a nil interface value (that would compare equal to nil) instead of a non-nil interface value that has nil value pointer inside it — which is a second kind of nil in anything but name. You compare it to nil, it says "not nil", then you try to use it and it blows up with panic "I am actually a nil, gotcha sucker".


> Okay, I've accidentally put a pointer type into an interface, now what? How do I check that the inner value is nil/not nil? Mmmmm?

What does this even mean? If you did it by accident it sounds like a bug in your program, so fix the bug so a nil can’t be passed into the interface. If it’s not a bug but nil is a valid value, then type-assert the interface back to the concrete pointer type and check it against nil.

> It's a defect in the language, plain and simple: if a pointer-to-type implements interface, then converting a nil pointer to type to the interface should result in a nil interface value (that would compare equal to nil) instead of a non-nil interface value that has nil value pointer inside it — which is a second kind of nil in anything but name.

No, this would be a bug because a nil pointer and a nil interface mean different things.

> You compare it to nil, it says "not nil", then you try to use it and it blows up with panic "I am actually a nil, gotcha sucker".

You have the same behavior in any language with nullable references including Java, Python, C, etc. You’re complaining ultimately because you don’t understand references. In this case you have a reference type (the interface) whose value is itself a reference type and you’re upset because checking the outer reference for nil doesn’t also evaluate the inner reference; however, no mainstream language works this way and this would be very surprising behavior to people who actually understand references.


GP may have in mind that most other languages would let you write *ptr == NULL if someone handed you a pointer to a null pointer to a specific implementation of an interface even if you didn't know which specific implementation.


I think the original claim is that it can be inner nil or outer nil and those are two different things and often you should check for both.


The original comment was:

> A great innovation of Go is that it includes a built-in reference type with both inner nils and outer nils.

Which the OP later clarified by saying an interface can be both nil and not nil at the same time. So it’s pretty clear that we’re debating about whether interfaces have three states (nil, valid, and both) i.e., multiple kinds on nullity (inner vs outer).


> Which the OP later clarified by saying an interface can be both nil and not nil at the same time.

I don't think I made any such clarification...


You aren’t the OP. I was referring to this:

> typed nils are a single value which is both nil and non-nil at the same time.


the "original poster" is someone other than the person who made "the original comment"?"




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: