Good evening

Many languages have some way to denote a null-pointer which essentially points to nowhere. Some kind od an initial dummy value. In C it is NULL, in Python it is None which is its own type (NoneType, a type only None belongs to).

Go language has it as well. It is nil, and you’re probably familiar with it already. But one thing you might not think of is that it sometimes has a type, and sometimes doesn’t. Confusing already? Let’s explore further.

Infer? Nope!

Let’s take a look at a simple program.

package main

import "fmt"

func main() {
	a := nil
	fmt.Println(a)
}

Will it compile? No, it won’t. The compiler will print use of untyped nil error. Here we are trying to assign a nil to a variable without providing its type; we expect the compiler to infer it. It can’t. What is it? A pointer to an integer? A string? An array? A structure? No way to tell in advance. And Go requires knowledge of types to compile a program; it’s not C language that lets you declare pretty much anything in a block of memory and then treat it later as something else (something that I love and hate about the C language).

Tell me more!

The following program, on the other hand, is perfectly valid.

package main

import "fmt"

func main() {
	var a *int64 = nil
	fmt.Println(a)
}

The compiler knows the type now, and it is a pointer. Assigning a null value to it is fine.

If you create another variable, initialize it to nil and then try to compare the two

package main

import "fmt"

func main() {
	var a *int64 = nil
	var b *int64 = nil
	fmt.Println(a==b)
}

it will print true. Sounds reasonable.

Pointers? Not only addresses.

Now try something like this

package main

import "fmt"

func main() {
	var a *int64 = nil
	var b *string = nil
	fmt.Println(a==b)
}

Won’t compile. invalid operation: a == b (mismatched types *int64 and *string). But they are both nil, you might say. Doesn’t matter! Unlike C, where pointers simply hold addresses, in Go they have types, and variables of different types cannot be compared. Therefore we’re looking at a slightly different nil.

Most of the time it won’t bother you; why compare different types anyway? Yet there is one example, that I’m going to show you now, that made me think about nil having a type in the first place.

Nils with… methods?

So here’s a simplified interpretation of the code that gave me something to think about.

package main

import (
	"fmt"
)

type A struct {
	alpha int8
}

type B struct {
	a *A
}

type C struct {
	b *B
}

func (b *B) GetA() *A {
	if b != nil {
		return b.a
	}
	return nil
}

func (c *C) GetB() *B {
	if c != nil {
		return c.b
	}
	return nil
}

func main() {
	beta := C{}
	fmt.Println(beta.GetB())
	fmt.Println(beta.GetB().GetA())
}

Let’s abstract ourselves away from what’s going on in the main() for a moment. We declared a simple structure A, then declared a structure B containing a pointer to an instance of the structure A, and finally declared a structure C containing a pointer to an instance of the structure B. And we created getter methods for B and C that return the pointers they contain. If the structure itself is nil, it will return nil (it could do whatever we wanted, to be honest, like logging or returning some default pointer, etc.).

Wait! The structure itself is nil? How can a nil have a method? Well, in Go, it can!

In the main(), we have declared a C structure that contains a nil pointer. The following statement should not cause any confusion. It just returns nil, right? Yes, it’s all going smoothly.

Now the next statement is interesting. GetB() returns a nil here, right? If we remember how we make a program panic every time we forget to check whether an error is nil or not and accidentally call Error() method on a nil (guilty as charged), we might expect that GetA() called on a nil would make it panic.

And yet, it does not! The nil returned by GetB() is not some generic null pointer value, it is actually typed. It has a type *B, and that type has a method GetA(). And that method’s implementation checks whether the pointer to B itself is nil or not.

When I first saw this code, that was my biggest confusion. I expected GetA() to panic in this case. And it would, but for the fact that nil in Go has much more to it than pointing into the void of nothingness.

In conclusion.

There are several StackOverflow questions like this and this that helped me get to the bottom of all this. Maybe not even the bottom yet, since Go, just like pretty much every programming language, has so many tidbits many of us might not be aware of. Or we might use them every day without even noticing that we would instantly panic, had we given it some thorough thought. Pun intended.

Thanks for tuning in!