Since you sort of ask: Go pointers themselves are not that special, because in a GC'ed language with no pointer arithmetic they're really just a map/territory-type distinction, but there are some syntax affordances that can make them briefly more confusing than they appear.
For method calls and field accesses, Go fuzzes whether you have an object or a pointer by making the "." operator work on both, rather than C's "." vs "->" distinction. Since it's never ambiguous, it's just pointless overhead to make the programmer worry about that. It can also be slightly confusing that the method itself can control whether it receives a pointer, so you can call object.Method on a concrete object, but Method will receive &object. As a professional programmer I appreciate not being bothered with this unambiguous detail that the language can easily handle for me, but it can confuse people in the early going, which is a legitimate criticism. (I still come down in favor of doing what it is doing, but it is a legitimate drawback.)
The other thing I see that fools people with experience from other languages is that the "nil" pointer is not the same thing as a NULL pointer in C. In C, the NULL pointer is simply a zero with no type information connected to it. In Go, pointers are actually (type, address) tuples, so a nil pointer to a custom struct is actually (pointer CustomStruct [1], nil), which means that the runtime is capable of correctly resolving methods on nil pointers and that you can therefore write methods that work on nil pointers. Nil pointers are still bad because they are a value added to all pointer types by the act of taking a pointer that you can't control, you get it whether you like it or not, but they are not bad in the C sense that any attempt to touch one is a segfault.[2]
That's about all that matters in normal Go programming.
[1]: Asterisk is confusing HN's italicization there.
[2]: Which is I think an important aspect of understanding the "billion dollar mistake", by the way; it is easy to deconstruct that mistake as being two mistakes rolled in to one, which may perhaps explain why it was such a big mistake.
> The other thing I see that fools people with experience from other languages is that the "nil" pointer is not the same thing as a NULL pointer in C. In C, the NULL pointer is simply a zero with no type information connected to it. In Go, pointers are actually (type, address) tuples, so a nil pointer to a custom struct is actually (pointer CustomStruct [1], nil), which means that the runtime is capable of correctly resolving methods on nil pointers and that you can therefore write methods that work on nil pointers.
This paragraph seems, at best, misleading. C NULLs are not untyped either (using the same asterisk-avoidance as you): `T pointer x = NULL` means x is a NULL pointer with type T. Similarly, a Go `var x pointer T = nil` is a plain pointer-sized object with all bytes zero, just like C.
If you're talking about interfaces, then, there's actually two sorts of nil interfaces: interfaces where the data pointer is nil, and interfaces that are completely nil (both data and type/interface-info pointers).
func main() {
var x *int
var y interface{} = x
var z interface{}
fmt.Printf("%v, %v, %T, %v, %T, %v", x, y, y, z, z, y == z)
// <nil>, <nil>, *int, <nil>, <nil>, false
}
In any case, for a comparison to C, Go is fairly similar: you can call functions with nil pointers in either, just fine, and dereferencing one is bad (the badness is far more controlled in Go, but see below). For a more reasonable comparison about methods, C++ could make it legal to call methods on nil/NULL pointers, and, like a completely-nil interface, crash when calling a virtual method (i.e. one that requires dereferencing a nil pointer to get the function pointer), but optimisations win out.
> they are not bad in the C sense that any attempt to touch one is a segfault
Note that this has nothing to do with nil pointers storing runtime types (even if they stored them) or anything like that: the Go language just (sensibly) decided to not have undefined behaviour with nil pointers, requiring that implementations handle them in a reliable/reproducible way.
> This paragraph seems, at best, misleading. C NULLs are not untyped either (using the same asterisk-avoidance as you): `T pointer x = NULL` means x is a NULL pointer with type T.
The point, I think, is that this type information is carried only at compile time on the binding, not at runtime on the value itself. If you have multiple aliases to the same null value (e.g. via references or pointers to pointers), the behavior will change depending on which alias is used. Not so in Go.
Of course, this is also true for non-null values in C++, when methods are non-virtual, so...
> In any case, pointers to pointers with different types seem likely to be a strict aliasing violation in C.
Surprisingly, no, at least not in similar context. For example, it's perfectly legal to cast a pointer to a struct to a pointer to its first member - it's specifically guaranteed that this works as you'd expect. This is often used when emulating single-inheritance OOP in C - your "base class" is then the first member of struct type, and this lets you upcast and downcast with impunity.
Casting pointer-to-struct to pointer-to-field is fine, yes, but is it legal to cast pointer-to-pointer-to-struct to pointer-to-pointer-to-field? (I genuinely don't know the answer to this.) The former results in a new (temporary or otherwise) value with a new static static type, independent of the original, while the latter does not, and so is the interesting one. In any case, note that Go also has something a little similar, with its anonymous fields approach to composition.
However, that's not still at all the main point: there's actually not that much difference between how C and Go behave with compiletime/runtime types.
> Which is I think an important aspect of understanding the "billion dollar mistake", by the way; it is easy to deconstruct that mistake as being two mistakes rolled in to one, which may perhaps explain why it was such a big mistake.
If it's two mistakes, it's one big one (having null pointers at all) and one tiny one (the fact that calling methods on null pointers is undefined behavior in C++). The latter is yet another C++ gotcha, but not nearly as pernicious as the former, which causes nearly all the null pointer-related bugs in the wild.
Yes, I agree that adding values that you can not avoid having into the set of valid values for a type is the worse one by quite a bit.
However, I'm still not a big fan of languages where values exist that are automatically crashes if you try to touch them in some bad way. Why are they there, then? My mental image is one of the anthropomorphized language just tossing caltrops around willy-nilly and blaming people who get stabbed for not being careful where they walk. I think it's important to call this idea out separately because programmers should learn this principle as they learn how to use strong typing systems: Do your best to exclude meaningless states for which your only recourse will be to crash if you see one from your system at the type level. The C-style NULL value is a degenerate case of this general principle.
Yes, this absolutely includes Go. I consider it a mistake in any language. In particular I'd highlight the distinction between reading a nil map (legal, gets the zero value of the value type for the map) and writing to one ("kablooie") as particularly bothersome and asymmetric, especially in light of the way nil slices can often be "written" to (via append which is in practice much more common than using index access to write) legally. Lots of asymmetries around what actually blows up vs. what "works" (but quite likely isn't doing what you want) in Go here. I'm not really in love with getting zero values out of a map if the key doesn't exist anyhow and I tend to pretend that only the check for both the value and existence exists in my own code (because I almost always care about existence too), but I especially don't like that behavior out of nil maps.
Undefined behavior is what makes C efficient. And NULL pointers are just a consequence of the ability of pointers to take numerical values. Naturally, this behavior was kept in C++, which is just a superset of C (with a few minor exceptions).
NULL pointers and undefined behavior make a lot of sense in C++. The mistake is replicating this behavior on languages where it doesn't.
> And NULL pointers are just a consequence of the ability of pointers to take numerical values.
Not at all. The C standard doesn't allow pointers to take arbitrary numerical values; while you can cast back and forth, the only guarantee is that casting a pointer to a numeric value and back to a pointer still points to the original object (and even then only if the numeric type used is large enough, which couldn't be portably ensured prior to C99, and is only conditionally supported even now).
In particular, one thing that the standard does not guarantee - and there have been implementations that did not do so - is that casting a null pointer to int will produce zero, or that casting int zero to a pointer will produce null. Nor does it guarantee that a null pointer is an all-bits-zero value.
The fact that you can write "p = 0" in C is not because pointers can take arbitrary numerical values, but because the language syntax and semantics allow you to do so, with 0 being treated specially when assigned to an lvalue of a pointer type. But you can't write "p = 1", for example, because there's no such special treatment for any other integer literal.
> In Go, pointers are actually (type, address) tuples, so a nil pointer to a custom struct is actually (pointer CustomStruct [1], nil)
I could be wrong but I think that's not right. The key difference is dynamic vs static dispatch. What you're describing (type, address) is how go represents interfaces. So go can resolve a.Read() on an io.Reader if the value is (os.File, nil). A go empty interface is not like a c void pointer for that reason. Type info is preserved and can be safely recovered.
But go also has non-virtual method calls unlike Java (afaik), it doesn't implicitly dynamic dispatch. This is a consequence of the fact that it doesn't have inheritance. If you write var foo os.File; foo.Close(); there's no runtime (type,address) tuple. That .Close() call is essentially compiled directly to something like CALL os.File.Close(nil). This can potentially be inlined even. If your method treats nil specially that's up to you.
"it's just pointless overhead to make the programmer worry about that"
You are incorrect, removing this distinction actually adds overhead of keeping a type in mind when reading and working with code. And overall selector operator in Go is pretty bad, no need to defend it, it forces you to always keep in mind some things, like not to collide with a namespace, because there is no :: operator for namespaces and always look around where the thing is defined, because it's not obvious whether it's a namespace, an interface, a struct or a pointer receiver.
For method calls and field accesses, Go fuzzes whether you have an object or a pointer by making the "." operator work on both, rather than C's "." vs "->" distinction. Since it's never ambiguous, it's just pointless overhead to make the programmer worry about that. It can also be slightly confusing that the method itself can control whether it receives a pointer, so you can call object.Method on a concrete object, but Method will receive &object. As a professional programmer I appreciate not being bothered with this unambiguous detail that the language can easily handle for me, but it can confuse people in the early going, which is a legitimate criticism. (I still come down in favor of doing what it is doing, but it is a legitimate drawback.)
The other thing I see that fools people with experience from other languages is that the "nil" pointer is not the same thing as a NULL pointer in C. In C, the NULL pointer is simply a zero with no type information connected to it. In Go, pointers are actually (type, address) tuples, so a nil pointer to a custom struct is actually (pointer CustomStruct [1], nil), which means that the runtime is capable of correctly resolving methods on nil pointers and that you can therefore write methods that work on nil pointers. Nil pointers are still bad because they are a value added to all pointer types by the act of taking a pointer that you can't control, you get it whether you like it or not, but they are not bad in the C sense that any attempt to touch one is a segfault.[2]
That's about all that matters in normal Go programming.
[1]: Asterisk is confusing HN's italicization there.
[2]: Which is I think an important aspect of understanding the "billion dollar mistake", by the way; it is easy to deconstruct that mistake as being two mistakes rolled in to one, which may perhaps explain why it was such a big mistake.