Understanding Zero Values in Go

Understanding Zero Values in Go

April 20, 2023

In Go, variables of primitive types such as int, float64, bool, string, and others, will have a default value that is assigned by the compiler or runtime when no value is explicitly assigned to it (they are declared but not initialized). This is called a “zero value”.

The concept of zero values is important because it allows programs to work with variables even if they have not been assigned a value yet. This is useful in situations where a default value is sufficient or when a value will be assigned later in the program.

Let’s look at some examples of zero values for different primitive types in Go:

  • int: A zero value for an int variable is 0.
  • float64: A zero value for a float64 variable is 0.0.
  • bool: A zero value for a bool variable is false.
  • string: A zero value for a string variable is an empty string "".
// Zero value for int
var x int
fmt.Println(x) // Output: 0

// Zero value for float64
var y float64
fmt.Println(y) // Output: 0

// Zero value for bool
var z bool
fmt.Println(z) // Output: false

// Zero value for string
var s string
fmt.Println(s) // Output: ""

nil as a Zero Value

In Go’s documentation, nil is often referred to as the zero value for certain types. However, I find this terminology somewhat misleading, as nil suggests the absence of a value—its Latin origin literally means “nothing” or “void.” A more intuitive term might be default value. I want to emphasize that this distinction is my personal interpretation and not the official terminology used in Go’s documentation. I’ll refer to nil as the zero value in this article to align with the common terminology.

The types for which nil serves as the zero value include channel, map, slice, pointer, function, and interface. These types may behave differently when uninitialized, potentially leading to runtime errors. For instance, attempting to access an uninitialized map variable will result in a runtime panic.

⚠️
If you have an uninitialized slice of a certain type, like []string, and you use the fmt package to print it, the output will be []. This is because the fmt package uses reflection to determine the type of the slice and what to print. When the slice is uninitialized, the type is still known, so the fmt package is able to print an empty slice of that type. However, the underlying value of the slice is still nil, which can be misleading.

How to Avoid Errors Related to Nil Values

To avoid errors related to uninitialized variables in Go, initialize variables when you declare them, or assign a value to them before using them. This ensures that the variable has a valid value and can be used safely in the program.

Let’s look at an example of how uninitialized variables can cause runtime errors in Go:

// uninitialized map
var myMap map[string]int

// This will cause a runtime panic
myMap["foo"] = 1
panic: assignment to entry in nil map
Program exited.

In the example above, we declare a variable myMap of type map[string]int but we do not initialize it. When we try to assign a value to the key "foo" in the map, Go throws a runtime panic because myMap is nil and cannot be used in this way. To avoid this error, we can initialize the variable before using it, with the make() function:

// initialized map
myMap := make(map[string]int)

// This works correctly
myMap["foo"] = 1

In this example, we use the make function to create a new map and assign it to the variable myMap. Now we can safely use the myMap map without causing any runtime errors.

Similarly, an uninitialized slice will have a nil value, until a value is appended to it.

// Uninitialized slice
var names []string
fmt.Println(names) // Output: [] - see note above about the fmt package.

if names == nil {
  // at this point, names is indeed nil
  fmt.Println("Names is an empty slice")
}

// Append a value to the uninitialized slice
// now that a value is appended, names is no longer nil.
names = append(names, "John")
if names != nil {
  fmt.Println("Names is no longer empty")
  fmt.Println(names) // Output: [John]
}

While the zero value for non-primitive types is typically nil, it is not always the case. Consider the bytes.Buffer type in the Go standard library. Its zero value is an empty buffer, which means that a new bytes.Buffer variable is automatically initialized with an empty buffer when it is declared without any explicit initialization. This is a convenient feature because it allows developers to start using the bytes.Buffer variable immediately without having to worry about initializing it first.

Embrace the zero value

One of the Go Proverb from Rob Pike advises us to “Make the zero value useful”1.

By knowing the zero values of a types such as int, float64, bool, and string, you can use them in situations where a default value is sufficient or when a value will be assigned later in the program. It is also crucial to understand that variables of other types such as channel, struct, map, slice, array, and interface will have a nil value, and using uninitialized variables of these types can cause runtime errors in Go programs. With this knowledge, you will be able to write more robust and error-free programs in Go.