Golang is π˜Όπ™‘π™’π™€π™¨π™© Perfect-image

Golang is π˜Όπ™‘π™’π™€π™¨π™© Perfect

Date published: 31-Dec-2022
7 min read / 1744 words
Author: Luciano Remes
Golang
Distributed Systems
Concurrency
Programming Languages
Parallel

Disclaimer: I'm not a Go expert, I might get some things wrong.

Background

Golang's been a player in cloud-native software since it's inception in 2012, when Google decided to Open Source it. While I had written "Hello, World" equivalent programs in Go, it seemed like it was time for me to properly check it out. But it wasn't until this year that I delved deep into the bowels of the ever so coveted Gopher language. I first had exposure to production Golang during my internship this last summer at AWS EC2 where my team was transitioning part of our core services to Golang. Then, I took a Distributed Systems class taught by Ryan Stutsman where I implemented Map-Reduce, Raft, and a KV store in Go. I was so inspired by how easy it was to build correct distributed systems, that I also began building a distributed Fuzzer in Go (blog coming soon!) for my undergrad Thesis, as well as a few other small projects here and there. As someone who mostly uses C and Python for Systems and Security research, if I could only sum-up my experience using Go in one word, it'd be: enamoured. I've probably written ~10,000 lines of Go (including iterations) in just the past year.

I've had to write parallel and concurrent systems before, although admittedly not at the scale or complexity of MapReduce or Raft. And one thing I found to always be a problem was thread synchronization. How best to do this? A chunk of shared memory? An explicit middle man to communicate between nodes? "No, lets just build language primitives that completely abstracts all that away and let you synchronize and easily exchange data between threads"β„’, said Golang, and I'm all onboard. With the relatively recent 1.18 patch which introduces generics and fuzz test support, the speed improvements in 1.19. I thought it'd be a great time to talk about my love-hate relationship with Go.

While there is an element of absurdism in the title, I like to think of it as literary clickbait. If I can succeed in at least convincing the "Golang is literally perfect" and the "Gophers are dirty land animals" crowds to agree on a single point, then it will have fulfilled its purpose. If it wasn't already clear, I'm in neither group. But if I had create my own crowd, then it'd be "Gophers are cute, in an ugly sort of way"

Writing Go

Ok, let's talk about writing Go, from the perspective of a student/researcher/debatably competent dev.

I was immediately greeted with an incredible developer toolchain that made everything exceptionally easy to work with. Compile times, Docs (including go doc), and Go modules. Not to mention the fact that Go seems to like fuzzers, having a builtin fuzzer in the testing toolchain, a huge plus for me. And of course, who could forget that cuddly gopher.

The language paradigm that Go encourages almost feels like an ontology to develop concurrent systems. I wrote an ASCII visualizer for Advent of Code recently and although the code could have been completely synchronous I ended up writing it "the Go way" and I observed a substantial speed improvement, purely as a function of its native concurrent operations. The world naturally works concurrently and it's one of Go's greatest accomplishments to have recognized this, allowing concurrency and parallelism to be such a first class part of the language. The fast compile times are also something not to scoff about, apparently something Rob insisted on while designing the language, which has persisted in the ethos of the community. A great testament to the team.

Other enumerated features of the language that were really appealing when first picking it up:

  • Go thread concurrency model with go routines. Go threads aren't actual kernel threads, they're concurrent green threads managed by the runtime and are process scheduled among a shared thread pool of real kernel threads.
  • Git integration into the module systems.
  • Export from namespace by capitalization. I usually don't like Pascal-case, but I love this feature.

It's genius! I almost want to give Rob Pike a big wet kiss on his big Googley forehead. So then, what am I complaining about?

"Almost"

There's mainly 3 things that I believe can substantially improve the experience of using the language and would encourage me to use it over something like Python or Rust for certain non-concurrent applications. And for Go to be a fully general purpose programming language, some of these things definitely need to be improved. These are all features that could be reasonably added or changed by the maintainers right now, and wouldn't be paradigm breaking changes to the language: Generics, Error handling, and Functional operators.

Generics:

A common pattern I find myself using when handling certain kinds of data is creating objects with multiple properties, and say I have a bunch of these. I might find myself needing to do a custom rank ordinal sort based on multiple properties of the objects to find the largest item or even k-largest items, this is simple enough in Python. I just create a class Thing, operator overload the < operator, and then the builtin sort function can easily handle doing my custom sort:

class Thing:
def __init__(self, a: int, b: int, c: int, dat: str):
self.a = a
self.b = b
self.c = c
self.dat = dat
def __lt__(self, other):
if self.a < other.a:
return True
if self.a > other.a:
return False
if self.b < other.b:
return True
if self.b > other.b:
return False
if self.c < other.c:
return True
if self.c > other.c:
return False
return False
def main():
things = [Thing(i, i-2, i+3, f'thing{i}') for i in range(10)]
# Sorted Things
sorted_things = [t.dat for t in sorted(things)]
print(sorted_things)
# Largest Thing
print(max(things).dat)
if __name__ == "__main__":
raise SystemExit(main())

First of all, apart from not having a generic builtin max or min function. Golang is completely unable to to define a sortable struct such that I can just use a Sort generic. I have to use sort.Slice with a custom comparator function, or I have to make another type which wraps the slice of Things and then specify a Less, Swap, Len functions. At least the custom comparator is not as cumbersome, but then it just exists as a function and isn't really attached to the Thing type. There's no intrinsic way to specify that these 2 things have a relationship other than naming conventions:

package main
import (
"fmt"
"sort"
)
type Thing struct {
a int
b int
c int
dat string
}
func comp_thing(t1 Thing, t2 Thing) int{
if t1.a < t2.a{
return -1
}
if t1.a > t2.a{
return 1
}
if t1.b < t2.b{
return -1
}
if t1.b > t2.b{
return 1
}
if t1.c < t2.c{
return -1
}
if t1.c > t2.c{
return 1
}
return 0
}
func max_thing(things []Thing) Thing{
max_thing := things[0]
for _, t := range things{
if comp_thing(t, max_thing) > 0{
max_thing = t
}
}
return max_thing
}
func main(){
things := []Thing{}
for i:=0; i<10; i++ {
things = append(things, Thing{
a: i,
b: i-2,
c: i+3,
dat: fmt.Sprintf("thing%v", i),
})
}
// Sorted Things
sorted_things := append([]Thing{}, things...)
sort.Slice(sorted_things, func(t1, t2 int) bool{
return comp_thing(sorted_things[t1], sorted_things[t2]) < 0
})
fmt.Printf("%+v\n", sorted_things)
// Largest Thing
largest_thing := max_thing(things)
fmt.Printf("%+v\n", largest_thing)
}

Having quality of life builtin generic primitives like min and max functions that I can use instead of having to implement my own every time would be amazing. On top of this, being able to define a struct Type as sortable by just adding a function field as part of the Thing struct that allows it to be compared with < or >. Which the sort.Slice std lib can use such that it's function header looks something like this:

func Slice[T sortable](s []T)

This is originally what I thought the generic comparable keyword would be, but I get it if we want to make the distinction between sortable and comparable. But right now, there's no sortable! This also brings up the question of operator overloading, which I'd appreciate as a feature, although not fundamentally necessary. The only thing I'll say about that is that if there's no operator overloading I get .Equals PTSD from Java, which if I never write again will still be too soon.

Error Handling:

Do I need to say it?

Who wants to write yet another: if err != nil

I understand the freedom it gives developers, allowing them to handle errors on their own accord. And I understand how expensive exception handling can be for compile times and keeping a clean runtime. But there's gotta be better language semantics implemented to give users better error handling. Even if it's just syntactic sugar, so I don't have to stare at those ugly err != nil code. Maybe some way to specify error return types that should be handled differently if returned. I don't have a clear solution to this, but it might be useful to theory-craft something better than the bare-bones implementation that currently exists.

Functional operators:

What do I mean by functional operators?

I mean generic functions that act on data. First, as alluded to earlier:

  • min and max: It sort of goes hand-in-hand with generics, but having language primitives that do this for you would be amazing. However, I'll settle for a generic version of Min and Max in the math module. Because right now it's only implemented for float64, and it only takes 2 values instead of any number of values. Let me do: math.Max(nums...)

  • map, filter, reduce: Please stop making me write explicit for loops... I've written more for loops in go than I've written for loops in Rust or Python combined in the past year (that may or may not be an exaggeration). I know the strings package implements a Map function. But it should be a generic primitive that can act on slices, arrays, and maps.

  • set: Sets are such powerful data-structures that it's an essential part of a language to either have them as language primitives like maps and slices are right now, or including them in the std lib. Neither of which are a thing right now. Currently, I use this library that implements set operations whenever I need set operations. Just so I don't have to write 3 for loops to calculate an intersection between 2 slices.

This also brings me to another point, the container std lib is miserably small, and unnecessarily hard to use. So much so, that heap for example, has a whole 100 line example on how to use it to implement a Priority Queue. Which is what I'd want a heap for 80% of the time. And then the other 20% when I actually just want a heap, I still need all this boilerplate code for it to work properly like defining a Pop, Push, Len, Less, Swap functions. Currently, there's only three containers: heap, list, and ring. This can't be the language of the future with 3 containers in the std library. If Sets should be anywhere in the std lib, it's probably here.

Conclusion

I hope the maintainers will address some of these concerns in future releases/patches, these few things will take Go from a somewhat niche language to a truly general purpose language. We're already seeing it replace Java in many cloud-native production environments, this was definitely my experience during my time at AWS (EC2 intern 2022). The changes around functional operators and generics will make the greatest difference, and will have a huge effect in enticing a lot of developers and companies to switch over to Go. Making it easier to adopt for people coming from higher level more functional languages like JS, Python, Rust. As for me, I'll continue to use Golang for all concurrent/parallel workloads and the occasional term-based project. While I'm not holding my breath, I look forward to seeing the language grow, mature, and make us rethink the way we write concurrent software.

PS: If there's any Security Researchers, I'm still developing my distributed fuzzer written in Go. Would love to get feedback on usability and efficacy! Follow my twitter or mastodon