Go takes away type system fidget spinners and leave devs with nothing to do but the actual job, which they hate, and thus they hate Go.
Go takes away type system fidget spinners and leave devs with nothing to do but the actual job, which they hate, and thus they hate Go.
For some reason, most languages do not encourage the users to create types that are similar to Option<Option<T>>.
Have you actually taken this stuff to a group of first-semester CS students and had them understand it and think it was a good design?
In the stdlib there's this function:
// String returns the source text used to compile the regular expression.
func (re *Regexp) String() string {
return re.expr
}
which takes a pointer-to-struct receiver and does not check it for nil before using it. So any program that declares a Stringable interface, diligently checks Stringables to make sure they are non-nil, then calls String() only on the non-nil ones is allowed to panic on this line from the stdlib because re == nil. The wonderful tutorial[0] teaches beginning go programmers to write the same sort of bug in the first lesson about interfaces.That’s not the most straightforward interpretation, since it would invalidate the original criticism that I responded to (that Go more-or-less uniquely requires you to check inner/outer nil).
> For some reason, most languages do not encourage the users to create types that are similar to Option<Option<T>>.
Lots of languages “encourage” references to reference types (including Rust), but many of the ML family including Rust have non-nilable references. Go’s error (and that of many others including Python, Java, C, JS, etc) isn’t that it supports/encourages references to references, but rather that all references are nilable.
If you'd like, I can post about "implicit conversion from nil pointers to non-nil interfaces containing nil pointers that can only be checked for nil using reflection, combined with interfaces that work by duck typing and an existing body of code that pervasively uses struct pointer receivers in methods that panic when passed a nil pointer, even if it was a nil pointer contained in a non-nil interface" but that's a bit of a mouthful.
You have it exactly backwards--you're arguing that Go should implicitly convert a not-nil interface (that is implemented by a nil pointer) to a nil interface so that Go interfaces behave more like Java interfaces, but Go isn't doing any implicit conversions (unless you consider "assigning a concrete value to an interface" to be an "implicit conversion", but Java does this too).
> implicit conversion from nil pointers to non-nil interfaces containing nil pointers
Again, Go doesn't do any such implicit conversion, and you're arguing the position that it should. An interface is just a tuple containing a pointer to the data and a pointer to the type `(pdata, ptype)`. A nil interface is `(nil, nil)`, and an interface that is implemented by a nil pointer is `(nil, <pointer-to-concrete-type>)`. You're arguing the position that the latter should be implicitly converted to the former, but this would break code that uses nil as a valid value.
The root cause of the problem isn't how Go handles nil pointers in interfaces, but rather that Go has nil pointers, interfaces, etc at all. Go gives you no way to express that a reference type can't be nil, but ideally it would have an enum system like Rust's that makes optionality an opt-in property via `Option<T>`. If an interface `Bar` is implemented by `*Foo` and neither `*Foo` nor `Bar` can be nil, then this problem largely goes away. If `Bar` is implemented by `Option<*Foo>`, then `Option<*Foo>`'s methods need to handle both the `Some(*Foo)` and `None` (i.e., nil) cases.
> that can only be checked for nil using reflection
You don't need reflection:
i, ok := (iface).(*int)
if !ok {
fmt.Println("`iface` is not an `*int`")
}
if i == nil {
fmt.Println("`iface` contains a nil pointer")
}
fmt.Printf("`iface` contains a pointer to `%d`", i)**What do you call it when you assign a value of one type to a variable of another type and the language creates a new value, different from the one you were assigning, and stores that new value in the variable? In C++ this is called "implicit conversion." Here's the tutorial again:
var a Abser
f := MyFloat(-math.Sqrt2)
v := Vertex{3, 4}
a = f // a MyFloat implements Abser
a = &v // a *Vertex implements Abser
The last two lines involve constructing a new value for the LHS of a type that differs from the type on the RHS without any function calls, constructors, "make" keywords, etc. What word would you like to use for this?You could just as well have a patch that changes the type of a single variable from *Vertex to Abser or changes the return type of a single function from a type that matches an interface to the interface, and that would also create an assignment that creates a new value of the new type that will panic in contexts the old one would not have panicked, even though the programmer has not added any code saying "please construct an Abser (type, pointer) tuple from my pointer."
> Go should implicitly convert a not-nil interface (that is implemented by a nil pointer) to a nil interface
I am not arguing for that.
> unless you consider "assigning a concrete value to an interface" to be an "implicit conversion", but Java does this too
I guess, but this is much less of a conversion than Go is performing because the runtime value does not change at all. It's the same pointer, not a tuple containing the pointer. Unlike in Go, this assignment may very often be correctly implemented by a sequence of 0 instructions! Importantly, the meaning of "foo == null" is unchanged. This is not because Java performs implicit conversions of interface types to specific pointer types at the null check. It's because in Java interfaces are unable to express having a concrete value of a specific class which is null, because they are just pointers. One might say "Java interfaces do not have inner and outer nil values" as a shorthand for this.
> Go gives you no way to express that a reference type can't be nil
I agree that this would solve the problem.
> You don't need reflection:
You don't need reflection if you statically build an exhaustive list of all pointer types that match the interface in the whole program but don't actually match the interface in the sense that the methods don't handle nil, then generate code to handle each of them. This just doesn't seem like a reasonable option because sometimes people make libraries so they can't necessarily do static analysis of the whole program at the point where they are writing the code. It also sort of defeats the purpose of interfaces, I guess.
Right, I addressed this when I said:
> unless you consider "assigning a concrete value to an interface" to be an "implicit conversion", but Java does this too
So let's just skip to your reply to that bit:
> I guess, but this is much less of a conversion than Go is performing because the runtime value does not change at all. It's the same pointer, not a tuple containing the pointer. Unlike in Go, this assignment may very often be correctly implemented by a sequence of 0 instructions!
Yes, Java and Go have different implementations/conceptions of "interface". I don't see what bearing this has. From the programmer's perspective, Java's interface assignment isn't more explicit than Go's even though the underlying mechanisms differ. In particular, Go needs to create a new "header" value because its interfaces are polymorphic across any type (including primitives and value types!) whereas Java's are polymorphic only across objects and Java attaches a header to every object.
> Importantly, the meaning of "foo == null" is unchanged.
Go's `foo == nil` isn't changed either. It behaves like it does for all reference types. The difference between Java and Go is conceptual, but the `==` operator is coherent within each of them.
> This is not because Java performs implicit conversions of interface types to specific pointer types at the null check.
Agreed, and to be clear, I didn't claim it did. Nor does Go, so I'm not quite sure what you're responding to here.
> It's because in Java interfaces are unable to express having a concrete value of a specific class which is null, because they are just pointers.
> One might say "Java interfaces do not have inner and outer nil values" as a shorthand for this.
Right, but we were debating whether or not Go interfaces have inner or outer nils. Consider `var x interface{} = 0`. What's the outer value? And what's the inner value? The whole point of my comment is that you're conceiving of Go interfaces as having inner and outer values, but Go interfaces are just a fat pointer. As such, you can have pointers to pointers with Go interfaces. And while you can't have pointers to pointers with Java interfaces, you can elsewhere in Java so a Java programmer should be able to reason about Go interfaces because they can understand the concept of a nullable pointer to a nullable pointer.
Ultimately, it seems that you're criticizing Go for having a different interface mechanism than Java, and you get bugs when you treat Go interfaces like Java interfaces. And Go made the right choice here, because it can express polymorphism over a wider range of types than just objects (contra Java interfaces) and indeed it doesn't have any sort of object/primitive bifurcation--from the perspective of Go interfaces, all concrete types are just data.
This is incorrect. The value of "foo == nil" in function A can be changed by a patch that only changes the return type of function B from *Vertex to Abser. If you would like to say that this is not because of implicit conversion from pointer to interface and it is not because of any semantic difference between comparing the vertex pointer to nil and comparing the Abser interface to the nil interface value, I would like to know what you think causes it.
> Right, but we were debating whether or not Go interfaces have inner or outer nils
I haven't been debating this since it's obviously true. You seem to agree that an Option<Option<T>> may take on the values None and Some(None) separately. You can debate about what to call things, but I'm still not sure what you would like me to call the difference that exists between Java and Go which causes code that does not throw NullPointerExceptions in Java to panic in Go because the inner value is nil and the user has failed to use reflection (or generate an exhaustive list of all possible pointer types that could match the interface but whose method does not handle nil and check each of them individually).
> Consider `var x interface{} = 0`. What's the outer value? And what's the inner value?
What do you mean? That's a (int, 0) tuple. The type tag is int. The inner value is 0. interface{} expresses outer-nil-ness using a special value of the type tag, so that's (nil, nil) which a different value from (*Vertex, nil).
> And while you can't have pointers to pointers with Java interfaces, you can elsewhere in Java so a Java programmer should be able to reason about Go interfaces because they can understand the concept of a nullable pointer to a nullable pointer.
Please demonstrate by declaring a pointer to a pointer in Java. Ideally, you could also show how in Java to convert a pointer to a pointer to that pointer using a single assignment, without calling any constructors or functions or making any explicit casts.
> Ultimately, it seems that you're criticizing Go for having a different interface mechanism than Java, and you get bugs when you treat Go interfaces like Java interfaces. And Go made the right choice here, because it can express polymorphism over a wider range of types than just objects (contra Java interfaces) and indeed it doesn't have any sort of object/primitive bifurcation--from the perspective of Go interfaces, all concrete types are just data.
No, I'm criticizing Go for being fundamentally broken in a novel way that no language had come up with previously. It should seriously not be possible to cause a panic in a method call that is guarded by a nil check by changing the return type of some other function from a pointer-to-struct to an interface and making 0 other changes.
If you ask the language designers on the mailing list why it's like this, they'll tell you that it's important to make the zero values of types useful, including nil pointers of specific types. They also decided to ship a stdlib in which like half of the methods with pointer receivers immediately panic when passed nil, and they would like to tell you that the right way to avoid panicking is to not store such pointers in such interfaces. The language will do nothing to help you to avoid storing these pointers in these interfaces. Even if you successfully avoid storing these pointers in these interfaces in your code, if you store them in any interface at all, other code may use "interface upgrades" to find out that the methods that panic exist and call them. Wow!
The majority of developers I work with are only aware of the success paths of their code. That means we’re knee deep in exception corpses because they avoided doing part of their job.
Go makes them deal with it.
However, repetition can introduce bugs, too, for example a very common mistake is to write this:
if err != nil {
return nil
}
instead of this: if err != nil {
return err
}
which is pretty serious because you essentially ignore the error.This particular pattern is so common your brain often ends up completely skipping it and it's pretty hard to detect during code review.
1) not properly checking return values from standard library functions
2) not checking correct last error level function after use of standard library function, e.g. json_last_error(), curl_errno() etc
3) different handling of old style errors vs exceptions
This is an unfortunate legacy of PHP, it is messy and error prone.
I'm not a golang programmer but to my understanding is that the golang compiler does not enforce strict error handling, if a function changes its signature to start returning an error when it previously didn't is not something the compiler will warn about. This is unfortunate too.
I never really been a huge fan of exceptions either, but the good thing about exceptions is that they are not silent by default whereas return values can be ignored and therefore errors can missed.
What I'm trying to say is that we need stricter compilers.
My personal style of coding in PHP to avoid hidden bugs is to code in defensive style, where the unhappy path is as important as the happy path, combined with strict type handling where I try to use the type system to detect bugs.
This is why we use linters, they can catch this kind of problem.
Rust, Haskell, Kotlin, yes. They force you to consider the error path.
Go makes it all too easy to just ignore errors and produce crashy code.
As golang has no instanceof or typeof keyword, you practically you cannot use a method that uses multiple types as arguments without having to use the reflect API, just to have an if/else branch on what to do with that argument.
The things that I mentioned are from another point of view where language designers and implementers decided to implement the interactions with their data types in a different way, by providing overloadable methods with different function signatures for the same function name whereas in golang everything must be "namespaced" in a sub sub sub package.
Good luck finding the right methodology where to sort in a method that can handle two different structs. If they're nested it's not even possible cause golang's dependency resolver doesn't allow cyclic dependencies.
Parsing html5 in golang is a nightmare, and I've realized that this language is not made for such a use case.
You don't have to use reflection, there are typecasts:
"value, ok := arg.(MyType)"
You can also switch on a type. How is it different from instanceof in practice?
If item instanceof HouseStruct {}
Elseif item instanceof ContainerStruct {}
With your suggested solution you end up with OOP fatigue and GenericBuildingImplementationStruct which is probably not what golang wanted to embrace with their type system.
switch val := item.(type) {
case HouseStruct:
case ContainerStruct:
}