I’ve used Go for a similar amount of time as you. I started with Go 1.0 when I pitched it to my company at the time, and then migrated all of our BE code to Go. It solved the problems we had at the time, and I certainly don’t regret that decision.
However, I ran into a ton of issues that I really don’t think I should have. For example:
dumb bugs stemming from nil and weird interaction with interfaces (e.g. interface{}((*int)(nil)) != nil); honorable mention, functions attached to nil types can still be called, so the source of the nil could be hard to find
map isn’t thread safe; why??
nothing like Arc in Go, either use a channel or DIY with a mutex
And so on. Go strives to be easy to write, but it doesn’t do much to hide the footguns. So it’s like C, but at least you get panics instead of SEGFAULTs.
These days I much prefer Rust. I followed Rust pre-1.0, and I’ve used it a bit for personal projects since 1.0. It has come a long way, and I think it’s finally at a point where the barrier to entry is low enough and the ecosystem is robust enough that it can be a decent alternative to Go. I like that it doesn’t hide the complexity and forces you to deal with design problems at compile time instead of running into them randomly.
If Rust is too much, I prefer Python.
I wish Go would do something about its footguns. I honestly don’t like using it anymore because I get a ton of complexity with goroutines and whatnot, and very little to help manage it. The main thing I miss is pprof, and I find I haven’t needed it with Rust because things just run better.
@sugar_in_your_tea map not thread safe, because multiple threads iterate over one map so you have to use sync.Mutex to lock it for reading, writing or both. I fell into that pit too. Part of the learning process. C++ has mutexes too.
Right, the whole point of Go is to be concurrency friendly, as in, the main reason you’d use it is to do multi-threaded concurrency. That’s the #1 selling point and it’s why goroutines and channels are a thing. Yet there are so many little things you need to keep in mind, such as:
sending mutable types across channels - you either need to ensure it cannot be used by the sender until the receiver is done with it (either by coupling the logic, or by destroying the sender’s copy), or you need to copy it; then you get into whether you need a deep copy, and how to protect any other reference types you send; Rust solves it with the Copy and Send traits, which can only work if everything you depend on implements the Copy or Send trait
no scope guards, so you need to rely on defer for unlocking; in Rust, you get this for free, as soon as you lock something, it’ll unlock once your scope exits
a read from a nil channel blocks instead of panicing (ideally it would fail to compile) - that’s a pretty easy mistake to make
Part of the learning process
Unfortunately, you hit a lot of these cases in production instead of at compile time. In Rust, many of these types of issues are caught before you even get to testing your code, much less actually trying to ship something.
That’s why my decision process is something like this:
Python - if I just want a quick prototype and don’t particularly care about production-level stuff like performance, safety guarantees, etc
Rust - once I need performance, safety guarantees, etc
Go isn’t easy enough to just throw a junior developer on a project, and it’s not robust enough to catch a senior developer when they make mistakes. I thought it would’ve been good enough, but it’s just not as productive for me as Python (generally you hand-wave away the concurrency by using separate processes), and the compiler doesn’t catch nearly enough to rival Rust, so I’m happy to pay the productivity penalty to shift from Python to Rust once I know I need something a bit more serious. And once you’re experienced, the compiler doesn’t really get in the way anymore.
Go is interesting I guess as a microservice tool where you’re making things that are small enough, but imo it really doesn’t scale all that well in terms of reducing bugs as the project gets larger.
I’ve used Go for a similar amount of time as you. I started with Go 1.0 when I pitched it to my company at the time, and then migrated all of our BE code to Go. It solved the problems we had at the time, and I certainly don’t regret that decision.
However, I ran into a ton of issues that I really don’t think I should have. For example:
nil
and weird interaction with interfaces (e.g.interface{}((*int)(nil)) != nil
); honorable mention, functions attached to nil types can still be called, so the source of the nil could be hard to findmap
isn’t thread safe; why??Arc
in Go, either use a channel or DIY with a mutexAnd so on. Go strives to be easy to write, but it doesn’t do much to hide the footguns. So it’s like C, but at least you get panics instead of SEGFAULTs.
These days I much prefer Rust. I followed Rust pre-1.0, and I’ve used it a bit for personal projects since 1.0. It has come a long way, and I think it’s finally at a point where the barrier to entry is low enough and the ecosystem is robust enough that it can be a decent alternative to Go. I like that it doesn’t hide the complexity and forces you to deal with design problems at compile time instead of running into them randomly.
If Rust is too much, I prefer Python.
I wish Go would do something about its footguns. I honestly don’t like using it anymore because I get a ton of complexity with goroutines and whatnot, and very little to help manage it. The main thing I miss is
pprof
, and I find I haven’t needed it with Rust because things just run better.@sugar_in_your_tea map not thread safe, because multiple threads iterate over one map so you have to use sync.Mutex to lock it for reading, writing or both. I fell into that pit too. Part of the learning process. C++ has mutexes too.
Right, the whole point of Go is to be concurrency friendly, as in, the main reason you’d use it is to do multi-threaded concurrency. That’s the #1 selling point and it’s why goroutines and channels are a thing. Yet there are so many little things you need to keep in mind, such as:
Copy
andSend
traits, which can only work if everything you depend on implements theCopy
orSend
traitdefer
for unlocking; in Rust, you get this for free, as soon as you lock something, it’ll unlock once your scope exitsnil
channel blocks instead of panicing (ideally it would fail to compile) - that’s a pretty easy mistake to makeUnfortunately, you hit a lot of these cases in production instead of at compile time. In Rust, many of these types of issues are caught before you even get to testing your code, much less actually trying to ship something.
That’s why my decision process is something like this:
Go isn’t easy enough to just throw a junior developer on a project, and it’s not robust enough to catch a senior developer when they make mistakes. I thought it would’ve been good enough, but it’s just not as productive for me as Python (generally you hand-wave away the concurrency by using separate processes), and the compiler doesn’t catch nearly enough to rival Rust, so I’m happy to pay the productivity penalty to shift from Python to Rust once I know I need something a bit more serious. And once you’re experienced, the compiler doesn’t really get in the way anymore.
Go is interesting I guess as a microservice tool where you’re making things that are small enough, but imo it really doesn’t scale all that well in terms of reducing bugs as the project gets larger.