Golang features I (used to) miss
29 Oct 2024About 7 years ago, I was faced with some projects that implemented APIs that had strict performance requirements-not so strict that a language with a garbage collector was out of the question, but strict enough that I couldn’t use Ruby or any other language that is orders of magnitude slower than C. JVM languages were also not a good fit because I needed new instances of the service to spin up quickly. So I chose Golang as the implementation language because of its speed, simplicity and the fact that it was a modern and mature language with a good ecosystem. I also considered Rust, which would fit most of my requirements, but it wasn’t as mature as Golang at the time so I went with the safe bet.
Over that time, I’ve grown accustomed to the language even though a few things felt off. Coming to it from Ruby, Scala, Clojure, Python and other languages, I can’t say I immediately fell in love with it the way I’ve seen other developers do and put a gopher sticker on my laptop. I’ve never shaken the feeling that the way the language forces the developer to write much more verbose code than I was used to writing in other languages is a problem. Mind you, this is not about refusing to adjust to language idioms (e.g. the way it forces the developer to do error handling). I’m talking about features that help write much less verbose code while still maintaining readability like generics, collection primitives (e.g. map, filter, collect) and immutable data structures.
On the other hand, I appreciate the philosophy of the language of keeping it straightforward and avoiding adding features just because there is demand for them from people like me. The maintainers want to take their time to keep the language simple, minimal only adding features if they aren’t sacrificing these traits. I can respect that, but in the real world we need to get things done and there is a limit to how much repetitive code I’m willing to write. Thankfully they are listening and already implemented generics and built a slices package on top of them so that we don’t have to keep writing a Max function for every type a collection could hold. I’m still left with my biggest missing feature: powerful collection primitives like map, filter, reduce, etc.
Things are changing on that front as well. The iter
package is a wonderful first step and I’m sure more will come. Similar to how
everyone was writing a custom Max
function (and now they don’t have to) in
their projects, it unlocks ways of writing code that that were previously not
possible. Even if it is “just” syntactic
sugar.
Let’s look at one recent example and how I
was implementing it in my codebase which I can now thankfully stop doing.
EachSlice
Coming from Ruby, I was used to having a rich tool-set to work with collections. One example is being able to iterate a collection in chunks of arbitrary length. This is useful if the collection data that you’re working with contains batches you want to process together. The answer in Ruby is each_slice. What would it take to implement something like that in Golang? Well, just like the Max function without generics, we can implement something like this
func EachSliceInt(slice []int, step int, f func(int, []int)) {
for i := 0; i < len(slice); i += step {
end := i + min(step, len(slice[i:]))
f(i, slice[i:end:end])
}
}
We can use that implementation with code like this
EachSliceInt(
[]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
4,
func(i int, s []int) { fmt.Println(i, s) },
)
To get output like this
0 [1 2 3 4]
4 [5 6 7 8]
8 [9 10 11 12]
12 [13 14 15]
The problem with that is that for each type a slice could hold, we’d have have a separate implementation. Let’s make the implementation generic:
func EachSlice[S ~[]E, E any](slice S, step int, f func(int, S)) {
for i := 0; i < len(slice); i += step {
end := i + min(step, len(slice[i:]))
f(i, slice[i:end:end])
}
}
Now we can use it with any type of slice
EachSlice(
[]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
4,
func(i int, s []int) { fmt.Println(i, s) },
)
EachSlice(
[]string{
"one", "two", "three", "four", "five",
"six", "seven", "eight", "nine", "ten",
"eleven", "twelve", "thirteen", "fourteen", "fifteen",
},
4,
func(i int, s []string) { fmt.Println(i, s) },
)
To get output like this
0 [1 2 3 4]
4 [5 6 7 8]
8 [9 10 11 12]
12 [13 14 15]
0 [one two three four]
4 [five six seven eight]
8 [nine ten eleven twelve]
12 [thirteen fourteen fifteen]
Now for the final touch let’s make it possible to use the function in a for-range loop.
func EachSlice[S ~[]E, E any](slice S, step int) iter.Seq2[int, S] {
return func(yield func(index int, slice S) bool) {
for i := 0; i < len(slice); i += step {
end := i + min(step, len(slice[i:]))
if !yield(i, slice[i:end:end]) {
return
}
}
}
}
This last implementation allows us to use code like this
for c := range EachSlice(
[]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
4,
) {
fmt.Println(i, c)
}
Which works the same as before
0 [1 2 3 4]
4 [5 6 7 8]
8 [9 10 11 12]
12 [13 14 15]
What we’ve done in the last example is that we’ve (re)implemented the slices.Chunk function. We added an index to make is possible for the client code to know which chunk index is currently being processed, but other than that we could use the builtin function as is. I always wanted rich tools to deal with collections to appear in the standard library sometime and (just like with the Max function) they slowly are with the advent of the slices package and the maps package.
There are even more interesting things
happening to make working with
collections in Golang a more pleasant experience. The generics implementation
is still limited in that it doesn’t support method type
parameters so code like
myList.Backward().Filter(func(i int) bool { return i % 3 == 0}).Contains(42)
will still not be possible, but even with the limitation, the code with these recent
changes will be much less verbose and contain a lot less boilerplate while still
having having same Golang feel of simplicity over complexity. That’s a good
direction to be heading in and I’m looking forward to seeing how the language
evolves.