In this article I make an argument for using the Zero Value in Go over the Nil value, where appropriate.
🔥 Hot Take 🔥
- There is only one situation in which we should use a pointer to scalar type of value nil to mean empty, and that is when the zero value has meaning.
- Avoid pointer to struct types where not necessary (concurrency safe mutations and non LOB)
Zero Values
Zero values are default values for instantiation of declared but unassigned values. See this tour of go example.
When we instantiate a variable in go we have some options:
var mystring string = "mystring"
mystring := "mystring"
var mystring string
In var mystring string = "mystring"
we combine variable declaration with an explicit initalization with value of “mystring”
In mystring := "mystring"
we have a short hand inferred type declaration informed by it’s initialization value of “string”
In var mystring string
we have type declaration… but no initialization? So what therefore, is the initial value? It is the Zero Value
.
Why?
At this point, I was about to go on a lengthy explanation of pointers as counterpoint, but no one wants that. You can instead read this article which has a, in my opinion, fair criticism of the “value not a value”. I believe strongly that is is a bad practice to use different type and value to provide meaning that our types value is empty.
🔥😈 example:
type Base struct {
ID *string
}
type Other struct {
Base *Base // y? because ynot.
}
type Composed struct {
ID string
Base *Base
Other *Other
}
func (c Composed) equalIDs() (bool, error) {
// access some stuff
var composedBaseID *string
if c.Base != nil {
composedBaseID = c.Base.ID
}
var composedOtherBaseID *string
if c.Other != nil {
if c.Other.Base != nil {
idIWant = c.Other.Base.ID
}
}
var emptyStrPtr string
if composedBaseID == &emptyStrPtr || composedOtherBaseID == &emptyStrPtr {
return false, errors.New("either id is empty because the pointer didn't even take care of that for us")
}
}
🎵😌 example:
type Base struct {
ID
}
type Other struct {
Base Base // y? because ynot.
}
type Composed struct {
ID string
Base Base
Other Other
}
func (c Composed) equalIDs() (bool, error) {
if c.Base.ID == "" || c.Other.Base.ID == "" {
return false, errors.New("either ID is empty")
}
return c.BaseID == c.Other.Base.ID, nil
}
Yes, these are contrived examples, but very often seen in the wild.
In a language where type composition is powerful and often used, having composed types with pointers requires checking nil value at each value access depth where the pointer exists to check for empty before checking the value. Furthermore, once we access the pointer, if pointer to struct, we still need to do zero value validation in most cases anyways, as seen in the hell example and here.
Top comments (0)