Saturday, September 9, 2023

Why Go is actually easier than Python

A gopher

After a number of years developing in Python, I have seen the light and fallen for Go. This isn't the first time I have tried out Go, but it is the first where I have felt it to be easier, cleaner and overall more sane than Python. I do admit a part of this is the euphoria of seeing something new and shiny, and not having used it enough to see its flaws, but from my experience I feel I have come to discover the beauty of Go for myself.

I think this is a really fundamental point because I have read about the wonders of Go a number of times, and nodded along. But until you actually use something in anger, you won't really get it. Nothing here is going be truly original, but hopefully this gives a convergent perspective to the Go lovers from someone who had no intention to love Go.

As I mentioned, this was not my first foray into Go. I have tried learning it a couple of times and even spent a couple of weeks in a Go codebase. But until I tried porting over an existing Python program to Go, I found it frustrating. My previous attempts were frustrating because I jumped to the novel things: Goroutines and channels (which are awesome but I don't recommend starting there). But after playing around with the more familiar, I think I have finally worked out for myself what makes Go so wonderful: stacked simplicity. A few key rules and ideas add up to make a comprehensive grammar. Go's strength is from what it does not include as much as from what it does.

Error handling

Error handling in Go is contentious. Littering if err != nil everywhere feels like a ton of boilerplate that other languages avoid with exceptions. But it has two very important implications:

  1. It is clear where errors occur. This is such a huge improvement over Python (or JavaScript or any other exception-based language) where you have no clue what exceptions a function might throw. This is important because it makes you think about errors up-front, rather than debugging them later.
  2. No default behaviour. The idea of crashing as the default behaviour on error has to be up there with NULL in terms of billion dollar mistakes. Of course, failing fast is better than failing silently, but how many times have you seen a program crash because of a simple mistake that could easily have been caught in development? Go simply doesn't let you ignore errors. If you have a function func foo() (int, error) and you want to use the return value, you are forced to handle the error. Sure, you could just do x, _ := foo(), but that _ is a clear indicator that you are ignoring the error. The compiler erroring on unused variables is another simple rule that enforces this. 

Note: the compiler doesn't error on ignored return value so func subroutine() error doesn't complain if called as foo(). This has been a known issue since 2017 but is actually more complicated than it seems on first thought (e.g. _, _ := fmt.Println("Hello world") – not exactly pretty to a newcomer).

I also love how simple the error type is, it is just an interface with Error() string, no magic here!

No decorators

A lot of Python web frameworks use decorators for routing. But you do not need decorators when you have anonymous functions. The codes looks almost the same, but the lack of the @ makes it a lot clearer what is going on.

Compare:

@app.get("/post")
def post():
...

and

r.GET("/post", func(c *gin.Context) {
...
})

Not only do you lose the @, you also do not need to write out a pointless function name.

No classes

This can at first look like a semantic difference, but structs are really not the same as classes. You do not have inheritance, just interfaces. Again, the language provides you with just enough to avoid spaghetti code. Interfaces are such an elegant way to provide static duck typing. The type system as a whole feels like an aid rather than ceremony.

Summary

Go is not perfect of course and I have noticed through browsing Go codebases that there is often a lot of repetition. For example you often see parameter structs passed into another struct so you end up with duplicated fields. 

So what about Rust? Go is often compared to Rust because of their relatively short histories (2009 and 2015 respectively) and fairly recent usage explosions, but I personally don't find the comparison particularly useful. They are very different languages with completely different goals and philosophies. Put simply, Rust is not trying to be a simple language and Go is not looking to replace C.

Ultimately, for any new project for which you might consider Java, Python or JavaScript and which does not have a specific language requirement, I honestly think Go would be my first choice.