April 25th 2026
Recently a friend asked me what I thought about Google's Go language. Below are some scattered thoughts on the language as someone who hasn't really interacted with go directly but has experience with many of the affordances of the languages.
Memory Safety has become a hot topic for modern languages and downright a default requirement for some large organizations including Google, Apple, Microsoft Linux and the NSA. Google's research states that an overwhelming majority (70%) of bugs are due to memory safety violations that would've been solved by using languages that enforce memory safety. This is real money and real risk that can be mitigated by having memory safety guarantees.
Yes but no.
Go is obviously a garbage collected language making it "mostly" memory safe. Go does protect against things like use-after-frees or out of bounds memory access, however the definition of memory safety is somewhat debated and is now often implied to also mean thread safety. When accessing data across threads there are particular rules required in terms of what thread can access the data and when. Failure to adhere to these rules can lead to a data race. A data race is when two or more threads access data at the same memory location, with one of the actions being a write. This leads to undefined behavior meaning there is no clear definition on what the code will do after. In the usual case it will result an in correct value and in the world ending case it can end up as memory corruption.
This absolutely occurs in the real world and is such a problem that Go provides a tool to check for this after compilation. While not perfect it can often catch many cases. Despite this tool you can not guarantee that a Go binary is data race free.
@alilleybrinker.com on bluesky was nice enough to reach out to explain some more of the details here as well.
This is in fact well known to the Go team and ultimately chosen as a trade off of the language itself. More specifically Go does not include full "thread safety" meaning while a write can occur, which takes some time, a read occurs from a different thread. The read now how has partial data. Likely not what the programmer intended.
This most clearly exhibits itself in Go's interfaces and slices implementations as they're both multi word types making this behavior an easy foot gun.
Another blog about Go's memory safety
Go data races in popular software
The state of the art error handling in Go is to return both the value and and err. The programmer must check if the err contains anything, usually with if (err). This not only litters the code but makes it a very easy way to miss an error. This forgetfulness is a common error and is similar to the bug that took out Google's GCP software a couple months ago (that software was in C++). This failure mode also has echos of the infamous billion dollar mistake. I believe errors as values, as done in Rust and many others, is a superior paradigm as it forces the programmer to deal with or ignore the error as soon as the function is called and at compile time.
Garbage collection is not inherently a bad thing but is something that I personally dislike. Often when optimizing it becomes a battle against the garbage collector on how to get it to NOT collect at a critical hot path. Often it's easier to just manage it myself at this point. This lack of control can be deadly in certain situations and famously led Discord to switching to Rust for a particular service. I personally write mostly systems software making it hard to have garbage collected software in real time systems or in places like the kernel prohibitively difficult.
While this is mostly changed these days and not really an issue I did think it was an interesting artifact of Go's history. For a simple tldr; Go directly uses sys calls instead of going through the libc as most other things do. If I had to guess this was probably for 2 reasons: 1. On linux the libc is shipped separate from the kernel making it easier to not depend on a libc and just write sys calls for portability. 2. Is ocasionally faster to call the direct sys call. Personally I prefer my binaries to go though libc unless I explicitly want otherwise. OpenBSD does this as a security boundary forcing Go to change to accomidate this.
https://lwn.net/Articles/806776/
This part is a little over my header in understanding, but my basic read is that in the case of Mullvad's VPN app: is that a go function reads a whole page of memory to determine the length of a string. This triggers MTE on android, a protection mechanism that limits process memory access to ensure that a vulnerability in an app can't lead to arbitrary memory access for the entire device. Go's opaqueness into how memory allocates makes this difficult to deal with in low level situations.
https://github.com/mullvad/mullvadvpn-app/pull/6727
Despite everything I said I don't want to make it seem like Go is a bad language. It has excellent usability and people have made amazing artifacts in it: Kubernetes, docker, CockroachDB, most of Hashicorp. Go's standard library is very fat and has excellent high quality packages. It's often easy to write a full production ready server using only the standard library. In 2026 this is becoming more of a feature due to the ongoing supply chain attacks. Go itself also has some great technologists working on the project who are extremely responsive and care very deeply.
I want the software I write professionally to be secure, performant, and correct. This requires a complete understanding of the software I'm writing and the ability to control and create visibility into the areas I need. Rust aligns with me in these areas and is better fit for me.