Nil Notions You Should Know in Golang

Demystifying nil: Unveiling the Hidden Concepts of Null Values in Go

Think how hard physics would be if particles could think.
— Murray Gell-Mann

Medium Link: Nil Notions that You Should Know in Golang | by Wesley Wei | Jun, 2024 | Programmer’s Career

1.1 Nil in Go

Understanding the meaning of nil in Go and its common use cases in real-world programming.

1
var nil Type // Type must be a pointer, channel, function, interface, map, or slice type

In Go, nil is a predeclared identifier representing the zero value for a pointer, channel, function, interface, map, or slice type.

and nil is used in two significant ways:

  1. To represent an uninitialized object: As explained above, in Go, nil is the zero value for many types.
  2. To check for errors: Using nil as a condition is a common way to check for errors in Go. (However, it should be noted that nil is an essential part of the language, and you should be able to understand why it’s said so after reading this article.)
1
2
3
4
file, err := os.Open("file.txt")
if err != nil {
// handle error
}

1.2 Nil’s Characteristics

Exploring some characteristics of nil

Nil is not a type with a default type

First, let’s understand the definition of an identifier:

Identifiers: Identifiers name program entities such as variables and types. An identifier is a sequence of one or more letters and digits. The first character in an identifier must be a letter.
from: The Go Programming Language Specification - The Go Programming Language

In Go, other predeclared identifiers have their default types, such as:

  • The predeclared identifiers true and false have their default types as the built-in type bool.
  • The predeclared identifier iota has its default type as the built-in type int.

However, nil is not predefined with a default type. We must provide enough information in the code to let the compiler infer the expected type of the nil value.

Here’s an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
p := nil
fmt.Println(p)
}
// run
use of untyped nil in assignment

func main() {
var a = (*struct{})(nil)
var b = []int(nil)
var c = map[int]bool(nil)
var d = chan string(nil)
var e = (func())(nil)
var f = interface{}(nil)
fmt.Println(a, b, c, d, e, f)
}
// run
<nil> [] map[] <nil> <nil> <nil>

Nil is not a keyword

First, let’s understand the definition of a keyword:

Keywords: Keywords are reserved and may not be used as identifiers.
from The Go Programming Language Specification - The Go Programming Language

Here’s an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Case 1
func main() {
nil := 345
fmt.Println(nil)
true := 567
fmt.Println(true)
}
// run
345
567

// Case 2
func main() {
nil := 345
fmt.Println(nil)

var slice []string = nil
fmt.Println(slice)
}
// run
cannot use nil (variable of type int) as []string value in variable declaration

From the example, we can see that nil is not a keyword in Go, and we can define a variable named nil (however, it’s not recommended).

We can also confirm this from the official documentation:

1.3 Comparison of nil

How nil behaves in comparison operators, and how to compare nil correctly.

Comparison of nil values of same types

  1. When an uninitialized identifier (predeclared identifier nil) does not have a default type, it can be compared with any type of variable. (This can be observed in the case of err != nil.)
1
2
3
4
5
6
7
8
var p *int
fmt.Println(p == nil)

var q *int32
fmt.Println(q == nil)
// Output:
// true
// true
  1. When the compiler can infer the expected type of nil, partial comparison is possible, but partial comparison is not possible in some cases.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Case 1
func main() {
fmt.Println((*int64)(nil) == (*int64)(nil))
fmt.Println((chan int)(nil) == (chan int)(nil))
fmt.Println((interface{})(nil) == (interface{})(nil))
}
// Output:
// true
// true
// true

// Case 2
func main() {
fmt.Println((func())(nil) == (func())(nil))
fmt.Println((map[string]int)(nil) == (map[string]int)(nil))
fmt.Println(([]int)(nil) == ([]int)(nil))
}
// Output:
// ./prog.go:8:14: invalid operation: (func())(nil) == (func())(nil) (func can only be compared to nil)
// ./prog.go:9:14: invalid operation: (map[string]int)(nil) == (map[string]int)(nil) (map can only be compared to nil)
// ./prog.go:10:14: invalid operation: ([]int)(nil) == ([]int)(nil) (slice can only be compared to nil)

In this case, pointers, channels, and interface types can be compared with each other, but function types, map types, and slice types can only be compared with nil. Comparing two types with each other is not allowed.

In the The Go Programming Language Specification - The Go Programming Language, it is mentioned that “Slice, map, and function types are not comparable.” But why is this the case?

The reason is primarily due to the following points:

  • Slices: Slices are dynamic, ordered collections of elements. Their equality depends on the equality of all their elements, in the same order. Since elements themselves can be slices, maps, or functions, the comparison process could become infinitely recursive, leading to potential stack overflows or unpredictable behavior.
  • Maps: Maps are unordered collections that associate unique keys with values. As keys themselves, maps would introduce circular references during comparison. Imagine comparing two maps, where one map’s key might reference the other map, creating an endless loop.
  • Functions: Functions are essentially blocks of code that can be assigned to variables or passed around. Their equality is a complex concept. Should function equality depend solely on the function’s name, or should it delve into the function’s body and analyze its logic for exact duplication? This ambiguity makes defining a clear comparison for functions impractical.

Golang’s decision to restrict the comparability of slices, maps, and functions promotes efficient map operations and avoids potential issues with circular references or ambiguous function comparisons.

Comparison of nil values of different types

In Go, comparing different types of nil values is generally not allowed, even though they both have a memory representation of zero, as this will result in a compile-time error. The primary reason for this is that the memory layouts of different types are usually not the same. For example:

1
2
3
4
5
6
7
8
9
10
11
var p *struct{} = nil
fmt.Println(unsafe.Sizeof(p)) // 8

var s []int = nil
fmt.Println(unsafe.Sizeof(s)) // 24

var p *int
var q *float64
fmt.Println(p == q)
// run
invalid operation: p == q (mismatched types *int and *float64)

Additionally, Go itself imposes certain constraints that make comparison between different types of nil values invalid. For instance:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
var ptr *int64 = nil
var cha chan int64 = nil
var ma map[string]string = nil
var slice []int64 = nil
var inter interface{} = nil

fmt.Println(cha == ptr)
fmt.Println(inter == ma)
fmt.Println(inter == slice)
}
// run
./prog.go:14:21: invalid operation: cha == ptr (mismatched types chan int64 and *int64)
./prog.go:15:23: invalid operation: inter == ma (map can only be compared to nil)
./prog.go:16:23: invalid operation: inter == slice (slice can only be compared to nil))

Here, the constraints are already provided, and the reasons for them have been explained in the previous text. The reason for the constraint that a slice or map can only be compared to nil is also given.

Next, we will move on to our main focus, which is examples of different types of nil that can be compared to each other:

1
2
3
4
5
6
7
8
9
10
11
func main() {
var ptr *int64 = nil
var cha chan int64 = nil
var inter interface{} = nil

fmt.Println(ptr == inter)
fmt.Println(cha == inter)
}
// run
false
false

Through this example, we can observe that pointer-type nil and channel-type nil can be compared to interface-type nil. Why is this?

Firstly, we need to understand the source code definition of interfaces in Go. An interface-type variable (whether it’s iface or eface) consists of two parts: the type and the value.

1
2
3
4
5
6
7
8
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}

iface and eface are both underlying structures used to describe interfaces in Go, with the difference being that iface describes an interface with methods, while eface describes an empty interface: interface{}.

It can accept any type of value. When an interface-type variable is not assigned a value (i.e., its type and value are both nil), or when its value is explicitly set to nil, we say that the interface’s value is nil.

When you try to compare fmt.Println(ptr == inter), Go compares the type (*int64) and value (nil) of ptr with the type and value (both nil) of inter.

Although both ptr and inter have a value of nil, they have different types, so the result of the comparison is false. Mechanically speaking, Go handles it this way to maintain type safety and avoid any potential unexpected situations.

1.4 Nil in Go: Points to Remember During Usage

Based on the previous discussion, we can draw a preliminary conclusion that in Go, nil is not just a representation of zero or nothing, but rather has significant meaning and utility in various contexts.
In the following, we will explore some noteworthy points regarding the usage of nil in Go for different types.

Interfaces and nil

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

import "fmt"

type animal interface {
eat()
}

type dog struct{}

func (d *dog) eat() {
fmt.Println("dog is eating bones")
}

func GetNilConcreteAnimal() animal {
var dog *dog
return dog
}

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

The output of this program is false, which may be surprising if you’re expecting true. This is because for an interface to be nil, both the type and the value must be nil. In this case, the type is still animal, so the comparison with nil returns false.

Functions and nil

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
43
44
45
46
47
package main

import "fmt"

type Animal interface {
eat()
}

type Dog struct {
name string
}

func (d *Dog) eat() {
fmt.Println("Dog is eating bones")
fmt.Printf("Dog %s is eating bones\n", d.name)
}

func GetNilConcreteAnimal() Animal {
var dog *Dog
return dog
}

func GetConcreteAnimal() Animal {
dog := &Dog{}
return dog
}

func main() {
concreteAnimal := GetConcreteAnimal()
concreteAnimal.eat()

nilConcreteAnimal := GetNilConcreteAnimal()
nilConcreteAnimal.eat()
}

// Output:
// Dog is eating bones
// Dog is eating bones
// Dog is eating bones
// panic: runtime error: invalid memory address or nil pointer dereference
// [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x481e20]

// Goroutine 1 [running]:
// main.(*Dog).eat(0x0)
// /tmp/sandbox2244011392/prog.go:17 +0x60
// main.main()
// /tmp/sandbox2244011392/prog.go:35 +0x2c

From the output, we can learn the following point:

  1. An instance of a nil pointer can still access its methods, but you cannot access the nil pointer itself (this is a common low-level error that is easy to make when you are a rookie).

Slices, Maps and nil

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
func main() {
var slice []int64 = nil
fmt.Println(len(slice))
fmt.Println(cap(slice))
for range slice {

}
fmt.Println(slice[0])
}
// Output:
// 0
// 0
// panic: runtime error: index out of range [0] with length 0
// [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x481e20]

// Goroutine 1 [running]:
// main.main()
// /tmp/sandbox2244011392/prog.go:22 +0x2c

func main() {
var m map[string]string = nil
fmt.Println(len(m))
fmt.Println(m["tfrain"])
m["tfrain"] = "wesleywei"
}

// Output:
// 0
//
// panic: assignment to entry in nil map
// [signal SIGABRT: Abort signal from trace/profile received]

// Goroutine 1 [running]:
// main.main()
// /tmp/sandbox2244011392/prog.go:27 +0x2c

From the output, we can learn the following points:

  1. Accessing a nil slice will cause a panic
  2. A nil map can be read from, but it cannot be written to, or it will cause a panic

In short, you can simply remember “don’t access nil pointers”.

Channels and nil

1
2
3
4
5
6
7
8
9
10
11
func main() {
var cha chan int = nil
close(cha)
}
// Output:
// panic: close of nil channel
// [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x481e20]

// Goroutine 1 [running]:
// main.main()
// /tmp/sandbox2244011392/prog.go:22 +0x2c

From the output, we can learn the following point:

  1. Closing a nil channel will cause a panic

1.5 More

I recommend watching this video to gain a new understanding of nil.

GopherCon 2016: Francesc Campoy - Understanding nil - YouTube

generate by AI :

This video is a deep dive into the ‘nil’ concept in the Go programming language. The speaker provides a detailed explanation on the understanding and usage of ‘nil’ in Go programming, emphasizing the importance of ‘nil’ in programming. Here are some main points from the video:

  1. ‘Nil’ is often seen as an error case in Go, but it should really be considered an important part of the language.
  2. ‘Nil’ is ubiquitous in the basic structures of Go, such as slices, maps, channels, and functions.
  3. ‘Nil’ interfaces can represent default behaviors, such as the default operations of the HTTP package.
  4. ‘Nil’ interfaces can also represent a condition with no errors.
  5. ‘Nil’ is necessary in concurrent programming.
  6. The speaker hopes the audience will stop seeing ‘nil’ as an error case and instead understand and embrace it as an essential element of Go.

In essence, the video advocates the recognition and understanding of the importance of ‘nil’, knowing how it’s used in Go, and realizing that avoiding ‘nil’ might not be the best strategy.

1.6 Reference

Go中的nil -Go语言101
GopherCon 2016: Francesc Campoy - Understanding nil - YouTube

中文文章: https://programmerscareer.com/zh-cn/golang-nil/
Author: Wesley Wei – Twitter Wesley Wei – Medium
Note: If you choose to repost or use this article, please cite the original source.

Context You Should Know in Golang Flag Library You Should Know in Golang

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×