nil in Go Is Not What You Think

Recently I was watching a GopherCon talk on YouTube that was published in 2016. The speaker spoke about "nil". After watching that 30 minutes talk, my views about Nil are completely changed. In this post we will discuss exactly the same things that he talked about. This post is not going to be a very long post, so it will be easy for you to get the key points from that talk. But I will highly recommend you invest your 30 minutes in watching that. Here is the video.

Let me ask you any question. What comes to your mind when we talk about nil in Golang? Most of you will say that it's nothing. 'Nil' means nothing. But what if I tell you the meaning of 'nil changes' in the context we are using it?

Let me show you how.

Zero Values

When we declare a variable in Go but do not initialise it, then it will automatically get a default value. We call this 'zero value', and it is different for different data types.

GohperCon Talk

So we can see that boolean has a zero value of "false", which has a data type of boolean; numbers have a zero value of 0, which has a data type of integer; and string has a zero value of "", which is a string itself. But what is the data type of nil?

Nil doesn't have a data type, and even more surprising is that it is not even keyword in the Go programming language. What I mean is that you can do something like this in Golang, and it is completely valid and compiles successfully.

var nil = "a"

Different Personalities of "nil"

Nil behaves differently with different data types let me show you.

Nil Pointers

Pointers are variables that point to something in memory. So a nil pointer will point to nothing. This is simple and straightforward.

Nil Slice

But the things change when it comes to slice. The diagram below shows you how a slice is represented internally by Golang.

It has three properties: a pointer to the backing array that stores actual data, an integer variable representing length and another integer variable representing capacity.

When you declare a nil slice, the length and capacity properties are still there; only the pointer to the backing array is nil. This means that even if your slice is nil, you can still use the following methods on it, and it will completely work.

var s []int
len(s)
cap(s)

Nil Interface

Before understanding nil interfaces, you must understand how an interface is represented internally. So it has two properties:

(type, value)

type tells the data type of the interface implementation (if provided) and value tells the actual value. Let's take an example of fmt.Stringer interface which just has 1 method String().

var s fmt.Stringer      // (nil, nil)
fmt.Println(s == nil)   // true

This is true because nil == nil.

But what if I do some modification with this interface and make it something like this? What do you think the output of this program will be?

type Person struct{}

func (p Person) String() string {
	return ""
}

func main() {
	p := Person{}

	var s fmt.Stringer = p
	fmt.Println(s == nil)
}

If you guessed true, then I'm sorry, but you are wrong. Because when I assigned an object of the "Person" structure to our interface "s", its internal layout becomes (Person, nil), and because of this, we are actually comparing Person == nil, which is not true. That's why the output of this programme will be "false".

Nil Maps

Nil maps are also similar to nil slices. You can use len() on it. You can also iterate on nil maps, and you can also get values from nil maps, but they will give you the default value. The only thing that you can't do on a nil map is assignment. Here is a simple piece of code to demonstrate this.

func NewGet(url string, headers map[string]string) (*http.Request, error) {

	req, err := http.NewRequest(http.MethodGet, url, nil)
	if err != nil {
		return nil, err
	}

	for k, v := range headers {
		req.Header.Set(k, v)
	}

	return req, nil
}

Let's say you have a function that generates an HTTP request. If you need to add headers to your request, you will do something like this.

NewGet("", map[string]string{
	"Content-Type": "application/json",
})

But in case you don't need any headers you will do something like this.

NewGet("", map[string]string{})

The interesting thing is that this code will also work even if you pass nil to it.

NewGet("", nil)

Because what I said earlier is that a nil map does not mean nothing. It means that there is not the backing storage system, but there are still other properties that allow us to perform some other operation, such as iterating.

Nil Channels

For nil channels, I'll highly recommend you watch this 10-minute section of the video on which this post is based. Trust me, you won't regret it. The speaker has explained it in such a simple way that I won't be able to explain it in this post.

Nil Channels Video Section

Let's Connect

I keep sharing my learnings via this blog. You can subscribe to my newsletter on Substack, and I can send you weekly newsletters. Here is my Substack.

Also we can connect Twitter & LinkedIn.

Share: X LinkedIn