There's a reason it's sliding into the "boring and effective" (that's a good thing) camp of languages, but it's still got some cool factor.
If the timeline was flipped and Java was the new language on the block while Go had been around for 25 years, I bet you'd see the quality of Java code was vastly superior simply due to Java attracting passionate developers interested in learning new technology.
I still see Java devs with 15-20 years "experience" coding like 1st year CS students. It really wouldn't matter what language they were coding in.
I'd like to add that it just tends to not punch you in the balls. I originally picked it up trying to solve a simple web hook integration problem. I had a Python Flask app running which received a webhook, did some data transformation and did a POST to an API. After spending a whole morning pissing around trying to get uWSGI, systemd via ansible etc working I went for lunch. Came back, learned enough Go and had it working in a single binary with systemd unit before the end of the day without any 3rd party packages, server mechanisms, dependency management, futzing. It just worked. The same code has been moved into docker, then into kubernetes over the last few years with no change, no bugs and no headaches.
IMHO, the biggest win of Go over anything Python is the single distributable.
You write once. Compile (or cross-compile) once. Distribute the binary. Job done.
Python you've got the hell of dependencies. Write your script once, but then users have to endure pip hell for the hundreds of libraries you've depended on. And then the potential different dependencies between different Python stuff.
Go is awesome for that and the Go stdlib has 80+% of what most people need.
You got me there. That is one of the things I keep asking Santa to bring me for Christmas. ;-)
I wish someone would come along with a competitive Go ML lib. That would be awesome with a capital A !
There are one or two niche libraries around for specific tasks (e.g. anomaly detection) but there's no broad-scope general ML library.
Sure, it's not as nice or compact as a single binary, but "you'll be lucky if you can run on any non-Ubuntu/RHEL system" is massively overblown.
It's perfect for middleware, APIs and CLI tools.
I also love 'import "embed"' in Go to embed web files (templates) inside a binary and then deploy/manage that one binary with systemd with zero downtime updates with systemd port activation. Slick.
Random blog post with a few examples:
https://blog.carlmjohnson.net/post/2021/how-to-use-go-embed/
Java runs nice and quick when it's ready, but it's slow to get there and quite a memory hog.
Memory usage is a tradeoff — a good GC works best with a bit more overhead.
My experience with Go has been that errors get "handled" by propagating them up the stack manually and then logging them, without keeping proper track of how the error got there. So there is error handling but only in theory, in practice what you get is a sort of hand-written stack unwinding logic with far worse functionality and consistency than exceptions, at 100x the verbosity.
Goroutines in themselves won’t give you actually correct concurrency.
I really hope that Go doesn’t replace Java.
This is exactly the approach used in Go code, so I'm not sure what you're referring to. If you're talking about the fact that you can technically ignore an error result like
res, _ := doWork()
then that's a fair point. But we still have https://staticcheck.io/docs/checks#SA4006 for that.Empirically, java code has had terrible error handling, because people just stick one catch statement at the root of their program. Go atleast confronts you with the error at every step.
In go you would just verbosely (and error pronely!!) have to manually bubble it up, while this is automatically happening in Java with an informative stack trace, with no option to forget about it. Java is the one that handles error cases properly (and that’s why you might see exceptions more there — because they weren’t silenced somewhere wrongly!) here.
It could've definitely be better (just Rust-like Result<T> type would make it so much clearer to handle) but Go's insistence of dealing with it here and now means you have to think about what is exactly happening when it errors out, and are encouraged to add context to the error message instead of "here is stack trace, fuck you user, you're not worth knowing what is wrong".
But it's verboseness in common cases is inexcusable, if I have 3 steps to establish a connection, having 3 lines per error to handle is inexcusably bad. And it will be fucking 3 lines even if you write it in one line, because go fmt will expand it back to 3 lines
> Goroutines in themselves won’t give you actually correct concurrency.
Main advantage is they are cheap enough that doing goroutine per request won't bite you in most cases so you can have near-perfect parallelization of common apps by just writing essentially serial code.
The concurrency primitives are built into language and stdlibs which means when you do need synchronization, everyone does it same way.
As long as you after refactor N still are staying in the happy path avoiding the numerous footguns and implicit behavior Go supplies you with.
I would say if you stray outside a single thread or shared nothing fire up a Goroutine per response architecture deeply reconsider the choice of using Go.
haha, so true. I almost always wrap errors at every level with addition context. If it was worth doing, then it's worth explaining.
People that prefer try/catching large sections of functionality or want to skip verbose handling of errors often have not thought through the user or tester experience. Heck, even trying to recreate the issue/state from production.
if err != nil { return err }
vs
if err != nil { return fmt.Errorf("context here: %s: %w", action_type, err) } (or wrapping some custom app error with context for the trace)
Of course you don't want to leak private details, but often there is a public component that is worth adding. This also brings up the concept of dual-error chains where what you log and what you report to the client are different.
Go makes you deal with the error every single time something could go wrong, so you have a chance to do the right thing and append context at every single failure path.
Also, checked exceptions are very much not like that, and being able to handle errors of a logical unit is also beneficiary (e.g. transactions). You are free to use the scope-size that makes sense for the given use case.
Can you explain that? Touching every context of an error propagation seems opposite of forgetting about it.
I read it as "let's stop pretending we are wizardy wizards and can do things right way in 100% cases and let the language to enforce us to handle errors, producing better result for average Joe SR SWE".
Am I right with my readings (i'm not a dev guy at all)?
I think it is. I will gladly trade a couple of hours of extra error handling code every week in exchange for no pager duty alarms on the weekend.
1. Immediately when the error occurs, send it to logging with all required data to find the exact point of failure + data to understand the "why".
2. Send the error up the stack.
If you do these two things, your systems written in golang will be resilient.
There's a ton of this kind of stuff that amounts to a whole lot of productivity that gets overlooked in all of the pennywise-pound-foolish type-system navel-gazing that programmers fixate on.
Why even perpetuate this red herring comparison—maybe over a decade old?—by making unprompted mentions of Rust though.
e.g., tooling is good because the language is easy to parse
It's hard to explain, but the language is one you can throw into a team of random developers and come out with benchmarks, tests, CI/CD pipelines, unified code formatting, and good parallel work models (via goroutines) almost every time. Builds are instant. Built artifacts are a single binary - not 1,200 files that must be moved. Every single package that has any traction is both easy to read and has documentation inline or via published go.dev docs.
It's easy enough the frontend JS guy can write it. It's complex enough you can use goa.design or go-kit to auto generate openapi specs and complex microservice designs that support multiple transports (like gRPC). Errors always get handled because the linter points them out and the devs don't cheat by setting up multi-function try/catch blocks.
I've had less issues with production Go applications than any other language. I don't want it to win over Rust though, Rust is awesome. I want Go to replace Java.