Revisiting Golang

Golang and Simplicity

A come-around from someone who used to hate Go.

Created: Aug 30, 2022 // Updated: Sep 22, 2022

By

~12 min read


Update 2022-09-22: Here you will see me compliment Go. Infact, I call it pretty alright. Objectively - I do believe it is. Subjectively however, my opinions have shifted wildly and I’ll be doing a writeup about all my grievances. In the meantime, I’ll tell you that the straw which broke the camel’s back was trying to release a major version upgrade. Needless to say, I’ve been burnt bad. Anyways, back to the article.


If you’ve ever heard me talk about Go, it was probably in a bad light.

Let me spoil it right off the bat. I still have problems and grievances, but the language is actually pretty alright.

Yeah, it’s not a secret that I hated working with the language. Didn’t help that my introduction to it was through work, and was the winning alternative to a favourite language of mine - Rust. That started us off on a bitter foot. Maybe I’ll talk about Rust one day - I’m no evangelist but I’d still call it one of my favourite languages out of the lot.

Go, I feel, is a language that can cause quite a lot of friction. It can also be pretty polarizing. Reasoning isn’t some deep quality issues, but because of a strongly opinionated design and system that kind of locks your capabilities. This is not strictly negative - said system results in a significant pro infact - simplicity.

A quick tour

The language is one of the simplest to deal and work with. With regards to simple languages, I’d say it wins the contest for my favourite hands down. Python and Ruby are cool - however I’m someone who’ll never really like dynamic typing. It can make writing code simpler, but more often than not what it does is just cause numerous bugs that would’ve been avoidable in a static typed system. Not to mention, of course, the worse autocomplete.

This is why if someone asked me to recommend a language to start with, I’d say Golang. It’s simple, easy to setup and play with, and implements some pretty important concepts quite well. To prove this, how about I show an example?

type Dog struct {
    Name string
}

type Cat struct {
    Name string
}

Here’s some structs. A struct is short for structure - and it encapsulates a group of data. So Dog here has a Name, and so does Cat. if I want to create a dog named Fido, that could be done like so:

fido := Dog {
    Name: "Fido"
}

Now, let’s say I want to be able to call a dog home. In our case, we do this by printing whatever we’d want to say on the terminal - which is where the code is run.

fmt.Println("Come home, " + fido.Name)

This is fine, but it can get tedious to type all this out whenever we just want to call fido home. So, instead we can define a method:

func (dog *Dog) CallHome() {
    fmt.Println("Come home, " + dog.Name)
}

fido.CallHome()

Now we can just use CallHome! Infact, we might want to do the same for cats:

func (cat *Cat) CallHome() {
    fmt.Println("Come home, " + cat.Name)
}

I know this is getting long, but there’s one more thing to show.

Now, say there’s someone who’s got a lot of pets - both cats and dogs. You wanna represent them in the code, so you use a struct right?

type Person struct {
    Name string
    Cats []Cat
    Dogs []Dog
}

..and if this person wants to call all of their pets home - the best thing to use would be a method…

func (person *Person) CallPetsHome() {
    for _, cat := range person.Cats {
        cat.CallHome()
    }
    for _, dog := range person.Dogs {
        dog.CallHome()
    }
}

Static vs. Dynamic Typing

I want to pause here - because this is a good point for comparing static typing and dynamic typing. Go is statically-typed, like I said - and here you can see that we have to declare what type things are. In this case, we’re storing cats and dogs - but a cleaner way would be to just have pets, and in dynamically typed languages this is easy. Here’s an example in python:

class Person:
    def __init__(self, name, pets):
        self.name = name
        self.pets = pets

    def call_pets_home(self):
        for pet in self.pets:
            pet.call_home()

person = Person("Enbyss", [Dog("Fido"), Cat("Skimbleshanks")])
person.call_pets_home()

You can see that we don’t need to separate these two, the code just works as is. This is a pro of dynamic typing, it’s very easy to prototype things and go fast. However, this comes at a pretty steep negative in my opinion, in addition to a smaller one. See, what if I defined a person as this?

person = Person("Enbyss", Dog("Fido"))

Well, the editor won’t complain. It will see nothing wrong here. You might have even missed the problem - but when this code is run, what’s returned is this:

Traceback (most recent call last):
File "<string>", line 26, in <module>
File "<string>", line 21, in call_pets_home
TypeError: 'Dog' object is not iterable

I emphasize - when this code is run. Code isn’t constantly being run - and people can easily see that there’s no problems reported by the IDE and go, yeah things are good. The example above is very obvious and simple - however this problem becomes significantly worse when you’re dealing with large codebases that have nested logic and use external libraries. Suddenly, while you may think things are fine, when you run the code there could be some error about iterability - and it would be pretty hard to find out the problem.

Hell, what if you have something like this?

def do_thing(obj):
    do_this(obj)
    do_that(obj)

What is obj? What type do I need to pass here? Well, I need to go through the documentation or trudge through code to find out - and this confusion can end up complicating the learning process.

I’m not done - there’s a second issue infact. do you remember this method?

def call_pets_home(self):
    for pet in self.pets:
        pet.call_home()

You lose all autocomplete functionality here. What’s autocomplete? In a nutshell, it’s when you type something like pet. and the editor shows you examples of what could follow - for example a Pet has call_home(), so the autocomplete window will show call_home() as an option.

Here’s the problem - how is the editor supposed to know that self.pets is a list of pets? There only reason we know is because we designed the function - but if someone else looked at Person, they could easily wonder what pets is supposed to be set as. The answer here is that there isn’t a type. You just need to pass something that has a call_home() function.

note: This is called _duck typing_, so called because of the following phrase:

“If it acts like a duck, and quacks like a duck, then it’s a duck.”

Compared to static languages, where you know what something is because it’s defined - dynamic languages can’t use this information because it doesn’t exist. Instead of saying “this function accepts a Duck”, the only thing you can say is “this function accepts something that quacks” - with no way to specify this in the actual function signature (aka def call_pets_home(self) - the name, parameters, and in some cases - return value)

Interfaces

So that’s all fine and dandy Enbyss, great that you hate dynamic typing or whatever, but so what? The code ends up looking cleaner - I don’t need to do this splitting into []Cat and []Dog. Well, that’s where I have to stop you. See, you won’t need to do that in statically typed languages either.

type Person struct {
    Name string
    Pets []Pet
}

type Pet interface {
    CallHome()
}

func (person *Person) CallPetsHome() {
    for _, pet := range person.Pets {
        pet.CallHome()
    }
}

Say hello to interfaces. They’re a special type that serve as an abstraction. There’s no logic inherent to an interface, but instead it represents anything that can CallHome(). In most other languages you need to implement this explicitly - so for example in rust…

trait Pet {
    fn call_home(&self);
}

impl Pet for Cat {
    fn call_home(&self) {
        println!("Come home, {}", self.name);
    }
}

In go however, this happens automatically. For beginners, this serves as a seamless way to learn interfaces and their utility - for professionals, this is still pretty cool because you can now define interfaces that external types represent. Of course this is also possible in Rust, but that’s a story for another time.

With all this, I’ve shown interfaces, structs, methods, variables, and types - pretty quickly compared to other languages. I love Rust, but that language is stuck at hard mode. In a way, it feels like Golang and Rust complement each other. They’re both pretty cool and unique languages, but one strives for simplicity and uniformity, and the other goes for power and potential.

Higher Order Functions

That sounds pretty scary. Like one of those things you’d get taught in courses. Just as a thought experiment - I’ve shown you methods and types now, right? Look at this code, and try to understand what it’s doing.

func HelloWorld() {
    fmt.Println("Hello world!")
}

func DoTwice(function func ()) {
    function()
    function()
}

DoTwice(HelloWorld)

No shame in being confused - what this code does is run HelloWorld() twice. We’re passing the function into DoTwice, and then DoTwice well… calls it twice. If you’re not confused, and you’re not too familiar with code, that’s the power of Go. In static languages, doing something like this can be a pain in the ass. Dynamic languages of course get it for free:

def hello_world():
    print("Hello world!")

def do_twice(fn):
    fn()
    fn()

do_twice(hello_world)

This is why dynamic typing isn’t terrible, things like this come easily - however the same problems still apply, you know nothing about fn, it might require a parameter or return a type - or better yet I could just do this.

do_twice(2 + 2)
Traceback (most recent call last):
  File "<string>", line 5, in <module>
File "<string>", line 2, in do_twice
TypeError: 'int' object is not callable

Golang gives you this functionality easily - the type is easy to define, and once it is, you can just pass in the function no problem. Plus, static typing means you know exactly what the function needs and what it will return - and any mismatching function will show you an error in the editor immediately without needing to run.

Generics

One last thing. What if, say, you wanted to sort a list? We won’t get into logic here - but what would such a function look like? In python, it’s easy.

def sort_list(unsorted)

In the function you’d then just add logic to sort the list. If the elements can’t compare, you’re outta luck - but ignoring that, it feels simple and clean. However, we’ll run into a problem when it comes to statically typed languages…

func SortList(list ?[]) ?[]

What do we set the type of the parameter and output to? Well, you could set individual types like int64, but then you’d end up with a million functions all doing the same thing on different types. This is where Generics come in.

func SortList[T any](list T[]) T[] 

so list is T[]…? What’s that? Well, T is a generic - and as we’re seeing in [T any], it can be any type. Suddenly, our SortList function works with every single type of list that we can pass in - or it would if it compiled. Remember, not every type can be compared like that. Like, what if I passed a custom struct of mine, say Cat? So, instead of any…

func SortList[T comparable](list T[]) T[]

…we use comparable, which means anything that can be compared with their own type. For example, a number is comparable because I can do 2 > 5. Cat isn’t because I can’t do myCat > yourCat.

Like this, one function can be used generically, but at the same time if you pass in stuff that isn’t supported, you’ll get an error showin up in your editor or at compile time.

End

I know I hated on dynamic typed languages… and yeah I don’t like them - but at the same time they do have at least one pro, being that some things come really easy. Instead of needing to futz around types, classes, interfaces, generics, whatever - you can just let the language handle all of that for you. Certain projects really benefit from that - small ones, prototypes, and AI all use this dynamic nature to their advantage, since rather than focusing on defining stuff you can simply define logic instead.

But as I’ve grown, I learned that static typing has helped me a lot. It’s tougher to learn, and by definition can be more strict which feels limiting - but the limits are well worth it, and they’re there for a reason. Golang, to me, feels like the language that meshes the simplicity of python with the utility of static typing. You can learn it quickly, and stuff can be done quickly as well. I didn’t even go into how easy it makes stuff like concurrency - though to be fair I haven’t played with that myself.

I remember when I hated the limits Golang set up - but similarly to static typing, they serve a purpose. Code is standardized, looking similar which means that it can be easy to look at other Go code for the first time and understand what’s happening. This standard is then set to the goal of simplicity so that people can get into it easily - and I think they did a great job. Although pointers are still a thing which uh… They’re a lot. Maybe I’ll write on them too.

In the end, Golang is very opinionated - and its opinions tend to clash with my own. But I respect its approach, because it has undoubtedly led to many people picking up programming, learning, and going deeper into the field. Its ease of use, combined with ease of debugging and a dash of power, has left it a damn good language to pick up and use for whatever project you want to use.

Of course… if you ever fall deep into the rabbithole and start to get a taste for power and bloodshed - when the surface of the ocean proves too calm for your needs - when you wish to fall into the chasm, deep into the trenches, destined to meet whatever is at the centre of the earth… then, I’ll be waiting in the water, with a crab in my hands.

And I’ll tell you about Rust.

To be continued...?