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 view of nil has 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 keypoints 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 mind when you hear about nil in Go? Most of you will say that it’s nothing. ‘Nil’ means nothing. But the meaning and behavior of nil depend on the type in which it appears.
Let me show you how.
Zero Values
When we declare a variable in Go without initializing it, it receives the zero value for its type. We call this ‘zero value’, and it is different for different data types.
A boolean has the zero value false, numeric types have a zero value of 0 for their respective type, and a string has the zero value "". But what is the type of nil?
nil has no type of its own; its meaning depends on the type in which it is used. An interesting detail is that nil is a predeclared identifier rather than a reserved keyword, so it can be shadowed by another declaration. What I mean is that you can do something like this in Go, and it is completely valid and compiles successfully.
1var nil = "a"
Different Personalities of “nil”
Nil behaves differently depending on the type in which it is used. Let me show you.
Nil Pointers
A pointer stores a reference to a value. A nil pointer does not point to a value, so dereferencing it can cause a panic.
Nil Slice
Things change when it comes to slices. The diagram below shows you how a slice is represented internally by Go.
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.
A nil slice has length 0 and capacity 0, and its underlying array reference is nil. Operations such as len, cap, iteration, and appending to the slice are still well-defined.
1var s []int
2len(s)
3cap(s)
Nil Interface
Before understanding nil interfaces, you must understand how an interface is represented internally. So it has two properties:
1(type, value)
The interface value has a dynamic type and a dynamic value. If the interface itself is nil, neither is set. Let’s take an example of fmt.Stringer interface which just has 1 method String().
1var s fmt.Stringer // (nil, nil)
2fmt.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?
1type Person struct{}
2
3func (p Person) String() string {
4 return ""
5}
6
7func main() {
8 p := Person{}
9
10 var s fmt.Stringer = p
11 fmt.Println(s == nil)
12}
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”, the interface contains a dynamic type of Person and a non-nil value. Because the interface itself now contains a concrete value, comparing the interface to nil returns false. That’s why the output of this program 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.
1func NewGet(url string, headers map[string]string) (*http.Request, error) {
2
3 req, err := http.NewRequest(http.MethodGet, url, nil)
4 if err != nil {
5 return nil, err
6 }
7
8 for k, v := range headers {
9 req.Header.Set(k, v)
10 }
11
12 return req, nil
13}
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.
1NewGet("", map[string]string{
2 "Content-Type": "application/json",
3})
But in case you don’t need any headers you will do something like this.
1NewGet("", map[string]string{})
The interesting thing is that this code will also work even if you pass nil to it.
1NewGet("", nil)
A nil map has no initialized map data structure for writes, but read operations such as len, iteration, and lookup are still well-defined. Writing to a nil map, however, panics.