And your explanation does not really hold in actuality:
type DoesAThing interface {
Thing()
}
type ThingDoer struct {
}
func (td ThingDoer) Thing() {
}
func WillPanic() {
var thingDoer *ThingDoer
var someThing DoesAThing = thingDoer
someThing.Thing()
}
There is no situation in which calling Thing() on a ThingDoer will break, but Go's author have decided that implementing an interface on a value also implements it on the pointer, I never said to do that and carefully did not do that.This means when you assert that
> the nil is an invalid implementation, not because it is an invalid Go value, but because it is an invalid implementation of the interface.
The design of Go forces that lie on you, you have no way to opt out of it. If you implement interface methods on a value receiver, that lie will be forced onto you with no recourse. If you implement interface methods on a pointer receiver, then that lie will still be forced onto you but give you the option of... defensively ignoring and maintaining garbage state (à la ObjC I guess).
They have different types. This is not splitting hairs.
"The design of Go forces that lie on you, you have no way to opt out of it."
No, it doesn't. I've been programming in Go for about 8 years now, and I do not face this problem, because I do not lie in my code this way. My own experience is constructive proof of my point. That it, it falsifies your point, if this was "forced" on me I'd be encountering this problem. It is not a problem you have to have in Go. I can and have rewritten any code that has this problem when people post their code where they are "forced" to have this problem, and this is the cause every time. It is a result of bad programming decisions. Do not create objects that can not fulfill their contract, and you won't have problems with them propagating through your code.
(I mean, I'm really shocked at how much this angers people. Who is really advocating for "Create whatever garbage you want where ever and get angry at the downstream code when it can't pick up the pieces?" I mean, sure, it's a common style de facto, but who is advocating for this?)
Go is far from the worst case of this, the dynamic typing world affords code at scale that does this so much that it simply becomes a fact of life. It's the biggest reason I've come to loathe writing any code at scale in dynamic languages.
The official golang tutorial explicitly does not say "never use struct receivers at all" even though using them is "lying."
Also that GP was probably sarcastic when they nominated typed nils.
void *outer = 0; // outer can be nil
int val = 5;
int *inner = &val; // inner can also be nil
outer = (void*)(&inner) // can have an inner value, inner nil or outer niltyped nils are a single value which is both nil and non-nil at the same time.
It's no different than `var x **int` (a variable called "x" whose type is a pointer to a pointer to an int). If you want to safely access the underlying integer data, it's not sufficient to check that the outermost reference is not nil--you must also verify that the inner reference is not nil. We run into this in every language with nullable reference types, including C, Java, Python, JavaScript, etc.
EDIT: `*int` -> `**int`. TIL HN converts two asterisks into one.
None of that is correct. The value-part of the fat pointer is the exact same in both situations: nil. It is a single value which is both nil and non-nil.
> It's no different than `var x **int`
I assume you meant that rather than what you actually wrote.
It is in fact quite different.
> If you want to safely access the underlying integer data, it's not sufficient to check that the outermost reference is not nil--you must also verify that the inner reference is not nil.
Something a typed nil does not let you do, unless you know (or reflect) the underlying concrete type in order to downcast the interface back to a regular pointer.
> We run into this in every language with nullable reference types, including C, Java, Python, JavaScript, etc.
We really don't. In "every language" you have to actually create a double indirection for something within the same heavenly realm (but is not at all the same thing) to occur.
Yes, today I learned HN replaces double-asterisks with single asterisks. Edited my original post.
> None of that is correct. The value-part of the fat pointer is the exact same in both situations: nil. It is a single value which is both nil and non-nil.
I repeat, an interface isn't "both nil and non-nil", but rather the nullity of the interface and the nullity of the underlying data are distinct propositions. For example, there is nothing analogous to "both nil and non-nil" when the runtime type is a value type, such as an integer.
An interface behaves just like a reference. When the underlying data is also a reference, then it behaves like a reference to a reference. This means you can have a nil interface or a non-nil interface which references a nil pointer.
Under the covers, Go implements this behavior by representing interfaces as a tuple where the first field contains a pointer to the runtime type information and the second field contains a pointer to the data. Go encodes a nil interface as (nil, nil) while it encodes an interface backed by a nil int pointer as `(<pointer to `int` type>, nil)`.
> We really don't. In "every language" you have to actually create a double indirection for something within the same heavenly realm (but is not at all the same thing) to occur.
Go interfaces to pointer data are functionally "double indirection". The outer pointer can be either nil or a pointer to the data which itself can be nil or valid.
> Something a typed nil does not let you do, unless you know (or reflect) the underlying concrete type in order to downcast the interface back to a regular pointer.
Well, this would be unsafe as the runtime type of the underlying data isn't guaranteed to be a pointer. So by default Go requires runtime type checks in this case, but you can always opt out of these runtime type checks via `unsafe` but this is usually inadvisable.
EDIT: would really like to know why I'm being downvoted. If you disagree with my comment, then best of luck to you in rebutting it. :)
var i *int = nil
var x interface{} = i
if x != nil {
fmt.Printf("%v was not nil", x)
}
<nil> was not nil
By all means, do demonstrate how Java or Python have this problem. class Ptr:
def __init__(self, x):
self.x = x
x = Ptr(None)
if x != None:
# Go's `%v` print directive unpacks interfaces.
# We'll use `x.x` to emulate the same here
print(f"{x.x} was not None") var i *int = nil
var x interface{} = i
if x != nil {
I assigned `nil` to `x` and yet `x` is not nil. Implicit conversions aren't unusual in programming but in Go they stand out a bit. The closest analog I could find was abusing implicit conversion operator in C#. But even that's pretty explicit and you can tell something odd is happening. Thing1 i = null;
Thing2 x = i;
if (x != null)
{
System.Console.WriteLine("x is not null");
}
class Thing1
{
public static implicit operator Thing2(Thing1 c) => new();
}
class Thing2 {}First of all, I’m not asserting that there isn’t a problem with Go. I’m asserting that its interfaces aren’t some three-state beast per the OP.
Secondly, you don’t see the problem with the Python code because you’re not comparing it like for like with Go code. The equivalent Go code would look like this: `var x interface{} = (*int)(nil)` which is at least as conspicuous as the Python example.
> I assigned `nil` to `x` and yet `x` is not nil.
No, you put a nil pointer into an interface, thus the interface is not nil. We have to understand the semantics of the language we’re programming in—it’s not fair to criticize Go because it’s interfaces aren’t my exactly Python variables. I think there’s probably some valid criticism against Go with respect to the error-prone nature of its interfaces, but I don’t think it’s that they lack Python variable semantics.
> Implicit conversions aren't unusual in programming but in Go they stand out a bit.
This isn’t an implicit conversion, we’re explicitly putting an int pointer value (nil in this case) into an explicitly-typed interface{} variable.
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.
> 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).
I don't think I made any such clarification...
> typed nils are a single value which is both nil and non-nil at the same time.
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
Any preferred solution (which will never happen due to compatibility guarantees) would be to eliminate zero types altogether and use Rust-like enums instead.
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).
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.
> 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.
Click through to the playground link and add some 'fmt.Printf("%T\n", err)' lines here and there to see what I mean.
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".
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.