Nil and zero value in Golang

It is well known in golang that when allocating storage for a new variable and no explicit initialization is provided, the variable is given to a default value, either zero value for primitive types or nil for nullable types such as pointers, functions, maps etc. More details can be found in go sepc. Some of the nullable types can cause panic when accessing them without initialization. Some examples are as follows.

Access a nil pointer

1
2
var p *int
*p = 2 // panic: runtime error: invalid memory address or nil pointer dereference

Assignment to entry in nil map

1
2
var m map[string]string
m["foo"] = "bar" // panic: assignment to entry in nil map

Access to a nil interface

1
2
var p error
error.Error() // panic: runtime error: invalid memory address or nil pointer dereference

Golang developers have to pay attention to check whether a nullable structure has been initialized before using it. This has been discussed in many articles, such as Go: the Good, the Bad and the Ugly and Nillability and zero-values in go. Since this check is a lot of burden to developers, is there any way to check the potential risk, such as compile time check or static analysis tool.

Try to check nil pointer before panic

Compiler checkers

In Go1 the compiler can’t detect nil pointer since it is legal to pass a nil variable when a pointer, interface, function or anything nullable types is needed. This article proposes a solution to check nil pointer during compile time by borrowing a pointer type that can never be nil, which is like the idea used in Rust and C++. Code sample is as follows.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type myNumber struct {
n int
}

func TestNil() {
var number *myNumber
plusOne(number) // compile error: cannot use *myNumber as &myNumber, *myNumber can be nil
}

func TestPointer() {
var number *myNumber = &myNumber{n: 5}
plusOne(number) // compile error: cannot use *myNumber as &myNumber, *myNumber can be nil
}

func TestNonNilablePointer() {
var number &myNumber = &myNumber{n: 5}
plusOne(number)
fmt.Println(number.n) // output: 6
}

Static analysis tools

golangci-lint is a Go linters aggregator, it includes many linters, some useful static analysis tools are also integrated, is it possible to find the nil pointer problem via it? Let’s run linters with the following code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"fmt"
)

func funcRecover(f string) {
if err := recover(); err != nil {
fmt.Printf("func: %s, panic: %s\n", f, err)
}
}

func test1(m1 map[string]string) {
defer funcRecover("test1")
m1["foo"] = "bar"
}

func test2() {
defer funcRecover("test2")
var m2 map[string]string
m2["foo"] = "bar"
}

func test3() {
defer funcRecover("test3")
var err error
fmt.Println(err.Error())
}

func test4() {
defer funcRecover("test4")
var i *int
*i = 12
}

func main() {
var m map[string]string
test1(m)
test2()
test3()
test4()
}

From the result of lint, we can see only one possible nil variable access is detected. What’s more, the SA5000 detector is only concerned with local variables that don’t escape, global variables or function call with nil map access also doesn’t work. The static check logic can be found in source code of staticcheck tool.

1
2
3
4
➜  ./golangci-lint-1.36.0-linux-amd64/golangci-lint run niltype.go
niltype.go:21:2: SA5000: assignment to nil map (staticcheck)
m2["foo"] = "bar"
^
1
2
3
4
5
➜  go run niltype.go
func: test1, panic: assignment to entry in nil map
func: test2, panic: assignment to entry in nil map
func: test3, panic: runtime error: invalid memory address or nil pointer dereference
func: test4, panic: runtime error: invalid memory address or nil pointer dereference

As far as I know there doesn’t exist a tool that can detect nil pointer risk with every single nullable type just by static analysis. The best way to avoid nil pointer panic is to check the nullable type is not nil before using it.

Be careful when checking nil interface

Interface in Go contains both type and value, when checking whether nil with an interface, there are two cases

  1. Interface has nullable type(like pointer, map, etc) and value is nil
  2. Interface itself has a nil type.

Let’s use the following code as an example to explain the above two cases.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"reflect"
)

type MyError struct {
Err error
}

func (me MyError) Error() string {
return me.Err.Error()
}

func check(err error) {
fmt.Printf("type: %v value: %v\n", reflect.TypeOf(err), reflect.ValueOf(err))
}

func main() {
var e *MyError
check(e)
check(nil)
}
1
2
3
➜  go run nil_check.go
type: *main.MyError value: <nil>
type: <nil> value: <invalid reflect.Value>
  1. When passing a nil pointer of MyError to function check, the err contains type *MyError and nil value.
  2. When passing a nil to function check directly, the err is a nil type itself.

So when we check the nil of an interface passed from some other place, either returned by a function or passed in from the function parameter, we must be aware there exist two cases. If we don’t want to check whether it is a nil type or only nil value, we can put the check logic before golang type inference, in the above example, we can check whether it is a nil *MyError type before calling function check.

Besides, if we have to check the nil of an interface, the article Go: Check Nil interface the right way has a detailed introduction about it.

Summary

To summarize, in every case that we don’t want a nil structure, we must check it manually, there is no easy way to detect nil pointers automatically in golang, and we must be carefull when checking nil with an interface because ther interface could be nil type or not-nil type but nil value.

Reference