First Impressions on Go

Last fall, I started learning Go seriously. Since then, I've used it exclusively for personal projects, so here are my takeaways after a few months.

Background: I come from Python. I used it for years, both at work and for side projects. My projects are mostly the same kind of thing: web services, APIs, automation scripts. That's what I've been writing in Go.

Concurrency

Go sells itself as a language built for concurrency. Goroutines and channels are part of the language. The standard library adds useful primitives: sync.WaitGroup, atomic operations, mutexes. This didn't disappoint me. The primitives are raw but versatile. You can pick the mental model that fits your situation: classic threads, worker pools, sequential promises (without the function coloring problem of async/await), callbacks, or pipelines.

That said, concurrent programming in Go is not easy. It's hard, and the usual traps are still there. I've seen people criticize Go for being difficult when writing concurrent code. I don't fully agree. The difficulties I've faced in Go are the same I've always faced, regardless of language. Concurrency is hard by nature, and sure Go makes it more ergonomic and pragmatic, but it won't magically protect you from race conditions, deadlocks, or resource management errors, like writing to a closed channel or leaking goroutines.

Simple and Explicit

Go feels like a language that wants to be explicit. It avoids hiding control flow behind complex constructs. I'll give Go this: its constructs are simple and boring and code reads well. I sometimes read the standard library source, and so far nothing has tripped me up. Indirection levels are kept under control and even code wirtten by those smart Go contributors is explicit and easy to read.

Let's take a recent example:

func IPv6PrefixToUint(ipV6Addr net.IP) uint64 {
    if len(ipV6Addr) == 16 {
        return binary.BigEndian.Uint64(ipV6Addr[0:8])
    }
    return 0
}

No hidden details here. Eight bytes are read in big-endian order to create a uint64-integer. The function name says exactly what it does: BigEndian.Uint64.

Quality Culture

Testing tools are well integrated. You'd expect this from any post-2000 language, but Go's tooling left me with a good impression. Setting up tests is easy. Examples double as tests and end up in generated documentation. Benchmarks let you measure progress or catch regressions.

Recently, Go started working on native fuzzing support. This kind of attention signals a language (and community) that genuinely tries to solve real engineering problems.

Performance

Go's performance hasn't disappointed me. I've inspected the generated assembly a few times (Go 1.15 and 1.16). The compiler still misses optimizations that veterans like GCC or LLVM would happily catch, but the compiler improves with each release, and performance clearly matters to the contributors.

The Go contributor also maintains hand-written assembly for performance-critical functions in the standard library. The crypto packages are full of it (e.g., AES, SHA-256, elliptic curves, etc., all with architecture-specific implementations leveraging hardware instructions). Same for CRC32 and other maths-related code. Like I said: performance matters to the contributors, and they seem to take it seriously where it matters.

For me, this is a real win. I increasingly use managed cloud services, even for personal scripts. Cloud billing is often based on resource consumption, so I have an incentive to keep it low. Go might not match C, C++, or Rust, but coming from Python, the improvement is pretty huge, and I am more than happy.

Other Things I Like

A few more points, in no particular order:

Conclusion

Go hasn't revolutionized how I think about programming. It just gets out of the way and lets me ship things. Coming from Python, that's exactly what I needed.