BlogProjectsTravelAbout MeContact

My first impressions of Go

June 3, 2026

For a long time I've had a fascination with programming languages. Before the software industry entered this exhausting and lamentable era of never-ending AI hype and drama, I used to entertain myself by reading good old-fashioned programming language debates. One of the most contentious languages which always received a lot of crossfire was Go.

For one reason or another I had never given Go a try until recently when I spent the last half year reverse engineering a PS2 game (Rumble Racing, 2001), and writing a CLI tool in Go to extract and convert the assets into modern formats. I mostly chose Go because it just felt appropriate and that I was doing the universe justice to use a language called "Go" to quickly parse a racing game to extract audio such as these:

I will soon write a blog post specifically about that project, which was an amazing rabbit hole in its own right, but for this post I wanted to focus on my experience with Go specifically.

Things I Like

Simple Syntax

The minimal syntax, limited ways of expressing ideas, and consistent formatting have the nice side effect that pretty much any Go codebase feels immediately comfortable and easy to visually parse.

Pleasant and Fast Tooling

The tooling is fast. As someone who is used to rustc and rust-analyzer, it's a refreshing change of pace to work with tooling that is so immediately responsive and snappy. Obviously Rust tooling needs to do much more, but the difference in the time it takes to test and iterate on my code is night and day.

Helpful Standard Library

Go surprised me with the amount of genuinely comprehensive and useful functions available in its standard library. For my project in particular, which was reverse engineering a PS2 game, it had a lot of very convenient functions. For example, hex.Dump() to pretty-print a byte array was super convenient when working with slices of binary data. Concurrency was super easy to implement, simply having to create a sync.WaitGroup var, Add(1), kick off the Go routine, defer wg.Done(), and call wg.Wait() after kicking off all tasks. It helped me parallelize my CLI data processing tool with almost zero effort and gave me huge speed-ups. Having an entire PNG encoder available in the standard library was really convenient too, and made exporting my raw texture data a breeze without needing a third party library.

Prevents Footguns

I was pleasantly surprised at some of the cool things that Go does at runtime level to protect the programmer against accidentally writing dodgy code. For example, take this Go snippet, where we initialize a struct on the function's local stack and return its address to it to the caller:

func (s *Store) GetCar() *Car {
    return &Car{Store: s}
}

This would be a pretty bad thing to do in a language like C or C++, resulting in a dangling pointer. I was surprised to stumble across this and learn that Go auto-magically moves the variable to the heap when returning a reference to something that is about to go out of scope.

Things I Don't Like

Holes in the Standard Library

Despite my praise for the standard library, I found it odd that there are no functions for some simple things, like reversing a string or reversing a byte array.

Refusal to Compile Unused Variables

Refusing to compile unused variables is probably the language decision that most actively annoys me about Go and slows down my productivity. I get that it hints at bugs and affects compilation time, and sure, that's great in theory, but in practice it is highly annoying.

Type Switching is Painful and Leaky

Go provides type switching, which enables us to switch on the underlying type of something that implements an interface. In practice I've found this switch thing := i.(type) syntax to be easy to forget. The tooling also doesn't work well with this feature. If I have various struct types spread across multiple packages that implement an interface, the tooling will only generate cases for those structs that exist in the specific package I'm working in, despite it being totally possible that the value is a type from another package. I'm probably way too used to Rust's enums/sum types, and the wonderful tooling which exhaustively checks that all cases are covered. Go's equivalent which bolts onto switch statements that are not exhaustively checked just feels like a crappy and error-prone Dollar Store alternative.

No Enums/Sum types

As alluded to above, I think Rust's pattern matching has spoiled me. Any time I write a switch statement in an older imperative language like Go or C/C++, it just feels so hacky, error-prone, and ugly.

Syntax Rules Taken Too Far

The fact that the below code is a compiler error is kind of ridiculous. If I wanted to have a comment above the else statement, I just can't in Go. The comment must be inline after the curly brace for it to compile.

if 1 < 2 {
}
else {
}

if 1 < 2 {
} else {
}

This is totally goofy in my opinion. There were a few times where I forgot this requirement and began to question my sanity when I was failing at writing a simple if statement.

A Hundred Different ways to Print

Don't you dare make the mistake that you're writing Python and use print() or println(), because they write to stderr, so if you try to log the console output of your program, (i.e. go run . > out.txt) it curiously won't be there.

So instead, you must select your weapon and choose from: fmt.Print, fmt.Println, fmt.Printf, fmt.Sprint, fmt.Sprintln, fmt.Sprintf, fmt.Fprint, fmt.Fprintln, fmt.Fprintf, log.Print, log.Println, log.Printf, log.Fatal, log.Fatalf, log.Fatalln, log.Panic, log.Panicf, log.Panicln.

Now that you've picked your favorite function, it's time to LARP as a C programmer and recall the appropriate sequence from the forty different character sequences so that you can be sure that your float actually gets written to the string in a way that you would obviously expect it to look.

I just want to print something man. I'm not trying to impress people with the amount of random bullshit formatting incantations I can recall off of the top of my head.

I think this is another area where Rust has just spoiled me. In Rust I can just write this line and it will work for anything that implements fmt::Display

println!("{}", thing);

Uninitialized Values are Allowed

For a language that has a mental breakdown and refuses to compile over unused variables, it seems highly questionable for that same language to allow default/uninitialized struct values. I have a hunch that allowing uninitialized struct values is way more likely to introduce bugs than unused variables. In my experience, the fact that the compiler doesn't warn me when I don't initialize a struct value is annoying. Consider the below example:

type Foo struct {
    A int
    B int
}

x := Foo{
    A: 1,
}

If I add the B property to Foo, I now have to waste a lot of my own time having to hunt down all of the places in my codebase where Foo is initialized so that I can make sure to initialize B as well. If I actually remember to do this, it is a waste of my time at best, manually searching for something the compiler could have pointed me to. At worst, it's likely going to be a bug.

Overall Thoughts

I find Go to be a pleasant language to use once you get used to its quirks and understand where it tries to help you and where it won't. There are a ton of libraries and a strong community, and it is the closest thing to a statically typed Python that I've tried, speaking in terms of a language that gives me the ability to rapidly prototype something, iterate relatively quickly, and play with ideas. Overall I enjoy it, and it will probably be a language I add to my regular wheelhouse in situations where I want native code with better performance than Python, static types, and the ability to whip something up quicker than say, Rust.