Understanding Zero Values in Go
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 anint
variable is0
.float64
: A zero value for afloat64
variable is0.0
.bool
: A zero value for abool
variable isfalse
.string
: A zero value for astring
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.
[]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.