The cost of syntactic sugar in Go
In Go, you can express a lot with a little code. You can usually look at small bits of code and get a clear picture of what the program does. This is known amongst the Go community as idiomatic Go, and is an ongoing effort to maintain the consistency of the language across projects.
When I come across parts of Go that appear as exceptions to “idiomatic Go,” there’s usually a reason for it. Recently, I noticed a quirk in the way that interface slices (or abstract arrays) in Go work. This quirk helps explain how there’s a cost associated with using complex types in Go— and that syntactic sugar isn’t always free. Breaking down the behavior I encountered gives insight into why the issue came up, as well as helps clarify some design principles of Go.
Starting with an example
We’ll create a small program that defines a list of animals (dogs, for example) and calls a function that prints out each animal’s noise to the console.
animals := []Animal{Dog{}}
PrintNoises(animals)
The above program compiles successfully and outputs “Woof!” to the console. Here’s a similar version of the program:
dogs := []Dog{Dog{}}
PrintNoises(dogs)
Instead of outputting “Woof!”, the above program fails to compile and prints out the following error to the console:
cannot use dogs (type []Dog) as type []Animal in argument to PrintNoises
If you’re familiar with Go, you may think that I should check that Dog
implements Animal
, right? As it turns out, if it was an implementation error it would output something more like this:
cannot use dogs (type []Dog) as type []Animal in argument to PrintNoises: []Dog does not implement []Animal (missing Noise method)
Why did the first program compile and run with Dog
used as an Animal
, but the second did not even though they both appear idiomatic and correct?
Here’s the rest of the code that was used in this example for reference. It compiles and shows the internals of the above usage:
type Animal interface {
Noise() string
}type Dog struct{}func (Dog) Noise() string {
return "Woof!"
}func PrintNoises(as []Animal) {
for _, a := range as {
fmt.Println(a.Noise())
}
}
Simplifying the problem further
Let’s try getting this issue to occur in a simpler way to get a better understanding of it. Static type checks are a useful Go pattern to assert that a type implements an interface. Let’s first check that Dog
implements Animal
:
var _ Animal = Dog{}
This compiles successfully. Next, let’s try to do a static check on slices since our program uses them:
var _ []Animal = []Dog{}
Instead of compiling, the above gives us this compiler error:
cannot use []Dog literal (type []Dog) as type []Animal in assignment
Now, we’ve produced a similar (but not exactly the same) error as we saw in our failing program. Using these different clues, I did some research to find out how to fix the issue and why it happens in the first place.
Looking at the fix
After doing some research, I found two things: a fix and a rationale. Let’s start with the fix, since it helps illustrate the rationale.
Here’s the second program that originally failed to compile with a valid fix in place:
dogs := []Dog{Dog{}}// New logic: convert the slice of dogs to a slice of animals
animals := []Animal{}
for _, d := range dogs {
animals = append(animals, Animal(d))
}PrintNoises(animals)
By converting the slice of Dog
to a slice of Animal
, it now can be passed into PrintNoises
and run successfully. Of course, this looks a little silly since it’s basically a verbose version of the first program that already worked. In a larger program, however, this may not have stood out right away. The cost of the fix was four extra lines of code. Those four extra lines may seem like extra work until you start to think about why you, the developer, had to fix it in the first place.
Looking at the rationale
Now that you’ve seen a fix, let’s talk about the rationale. I found a great one-line answer: Go does not support covariance on slices.
In other words, Go will not perform type conversions which result in a linear O(N) operation (such as is the case with slices), instead delegating the responsibility to the developer. It’s Go’s way of saying that there’s a cost associated with performing that type of conversion. Go doesn’t do this 100% of the time, though. For example, when converting a string
to a []byte
, Go will perform this linear conversion for you for free, likely because this conversion is often convenient. This is just one of many examples of syntactic sugar in the language. In the case of slices (and other non-primitive types), Go opts to not take on the extra cost of performing this operation for you.
This makes sense — in the 3 years I’ve been using Go, this is the first time I’ve found myself encountering this type of scenario. It’s likely because of the “simpler is better” mentality that Go instills in its syntax.
Closing thoughts
The authors of a language typically make tradeoffs with respect to syntactic sugar — sometimes they’ll add functionality even though it makes the language a bit more bloated, and sometimes they’ll pass the cost onto the developer. I think this decision to not perform costly operations implicitly has a net positive result on keeping Go idiomatic, clean and controllable.
The above is example is just one of the many parts of Go where this same lesson applies. This example shows that there are side effects to getting comfortable with the idioms of a language. It’s always a good idea to stay thoughtful about design decisions instead of expecting the language or compiler to help you out.
I encourage you to look for more places in Go where these syntactic tradeoffs were made. It should help you gain a better understanding of the language. I’ll continue to do the same.
References
The following are references that were used throughout this article:
- GitHub Gist of the above example: https://gist.github.com/asilvr/4d4da3cdc8180c5a9740d2890d833923
- Go language website: https://golang.org
- Thread on covariance in Go: https://www.reddit.com/r/golang/comments/3gtg3i/passing_slice_of_values_as_slice_of_interfaces/
- Big-O notation: https://en.wikipedia.org/wiki/Big_O_notation
- Syntactic sugar: https://en.wikipedia.org/wiki/Syntactic_sugar