In the previous blog post we discussed about distinction between nil and empty slices. We have to clear about some notation, what is the idiomatic way to check if a slice contains elements? Not having clear answer can lead to subtle bugs.
Let’s talk about the example, we call a getAmounts
() function which returns a slice of float32. We want to call a handle function only if the slice contains elements. Here’s a first erroneous version:
func handleAmounts(name string) {
amounts := getAmounts(name)
//Checks if the amounts slice is nil
if amounts != nil {
handle(amounts)
}
}
func getAmounts(name string) []float32 {
//Initializes the amounts slice
amounts := make([]float32, 0)
//Returns amounts if the provided name is empty
if name == "" {
return amounts
}
// Add elements to amounts
return amounts
}
func handle(amounts []float32) {
//Do some operations
fmt.Println(amounts)
}
We check by the amounts slice has elements by checking if the amounts slice isn’t nil. But there’s a problem with getAmounts() functions which never returns a nil slice instead it returns an empty slice. Therefore, the amounts != nil check is always be true.
What do we do in this scenario? One option might be to modify getAmounts() function to return nil if slice name is empty.
func getAmounts(name string) []float32 {
//Initializes the amounts slice
amounts := make([]float32, 0)
//Returns amounts if the provided name is empty
if name == "" {
return nil
}
// Add elements to amounts
return amounts
}
Instead of returning amounts if name is empty, we return nil. This way, the check we implement about testing the slice nullity matches. However, this approach doesn’t work in all situations we’re not always in context where we can change the callee. For example, if we use an external library, we won’t create pull request just to change empty into nil slices.
Then, how can we check whether a slice is empty or nil? The solution is to check the length:
How then can we check whether a slice is empty or nil? The solution is to check the
length:
func handlePrices(name string) {
amounts := getAmounts(name)
//Checks if the amounts slice is nil
if len(amounts) != 0 {
handle(amounts)
}
}
We discussed in the previous blog nil-vs-empty-slcie that an empty slice has, by definition a length of zero. Meanwhile, nil slices are always empty. Therfore, by checking the length of the slice, will cover al the scenarios:
- If the slice is nil, len(amounts) != 0 is false.
- If the slice isn’t nil but empty, len(amounts) != 0 is also false.
In summary, checking the length is the best option to follow as we can’t always control the approach taken by the functions we call. Meanwhile, as the Go wiki states, when designing interfaces , we should avoid distinguishing nil and empty slices, which leads to subtle programming errors. When returning slices, it should make neither a semantic nor a technical difference if we return a nil or empty slice. Both should mean the same thing for the callers. This principle is the same with maps. To check if a map is empty, check its length, not whether it’s nil.
Github Link :: https://github.com/Sugaml/golang-best-practice/blob/main/slice-nil-vs-empty/practice-5/main.go
References
- 100 Go Mistakes and how to avoid them, Teiva Harsanyi, Manning Publications Co
- https://freshman.tech/snippets/go/nil-vs-empty-slices