Thirteen Years of Go
go.dev
go.dev
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.
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.
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.
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.
It's perfect for middleware, APIs and CLI tools.
I do 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
Perhaps one day someone will come up with an implementation of Go that doesn’t use a GC.
It’s just such an incredible language, and it deserves the type of Rust fanaticism that Rust has, but I suspect Go users are just a different type of person and we really are fanatics, we just don’t publicize it.[1]
[1]: https://survey.stackoverflow.co/2022/#most-popular-technolog...
Either one is nicer than c++ which I have a quarter century of experience at.
Edit: but that said with sich constrained resources do people really use gc or even dynamic memory allocation often in embedded? I work embedded and it is usually standard practice to preallocate everything to avoid fragmentation. Unless it is a more performant device in which case I'll add the quotes to "embedded". But for those you could just use normal go, or python for that matter.
"import C, import unsafe".
You absolutely can, at least for performance critical sections where you cannot afford garbage collection.
https://dgraph.io/blog/post/manual-memory-management-golang-...
This is very insightful. Would you mind sharing more resources regarding that? I'd be interested in a list of such features.
The GC can also be used manually by setting GOGC=off, then be triggered manually at the most convenient time.
It is of course possible to use Tinygo-compiled programs for your production use case as well. It has a focus on WebAssembly and microcontrollers, but produces perfectly-serviceable amd64 binaries as well.
1) It's performant. The language itself is very fast, the GC is very fast and go routines make concurrency fast.
2) The tooling is great. Once you have the Go CLI installed, everything else "just works." Cross-compilation is super easy. Install dependencies is easy. Generating code is easy. Embedding files is easy. The ecosystem is really mature. Modules are easy to create, export, and import. Compilation time is fast. The list goes on...
3) The generics system is amazing. It beats any system that implements generics with type erasure.
4) The language itself is very easy to read and write.
5) The language is relatively new, which means less outdated cruft. The language also puts an emphasis on having only one way to do things.
So comparing it to other languages:
- Python: Terrible tooling. Not performant
- C# Bad tooling (do I need .NET, .NET Core, Mono? How do I create a project? What is this crazy project xml file? How do I export my project to be consumed by others? How do I import a dependency?). Also, as an older language, it has a bit of cruft, e.g. it has optional types, but they also aren't required? Maybe if I knew more about C#, I would find it better. But using it with Unity didn't leave a great taste in my mouth.
- JavaScript: not performant, especially in multi-core environments.
- Rust: Rust is too hard to write. It's not worth the overhead if you can afford a GC.
- C++: Same as rust, + more foot guns and unsafe memory management.
- Zig: Don't have much experience with Zig, but I'd like to.
- Ruby: Not performant, not typed.
- Swift: Tooling is bad. Compilation is slow. Cross-platform is not a priority. Swift Package Manager is buggy. Using Xcode sucks.
- JVM languages: bad tooling (maven, sbt, gradle are all a pain), generics have type erasure.
Of course, there are reasons to use other languages. If you’re writing an iOS app, the tooling for Swift is best-in-class. Or if you’re doing ML, the ML-specific ecosystem in Python outweighs all other considerations.
addresses := persons.map(func(p Person) Address {p.address})
Actually what I really want is
addresses := persons.map(p => p.address)
But I understand Go doesn't allow that level of readability.
Say you have a list of classes and want to pull out certain fields. With immutability as default and easy map functions many people write something like this:
a = my list.map(e => e.foo)
b = mylist.map(e => e.bar)
This may or may not matter performance wise but I think Go has a strong culture of of making something like this easy vs writing a for loop that does everything in one go.
3) Besides C#, which languages doesn’t have typed erases generics? Most languages implement it through erasure. Also, Go’s generics are basic as hell..
4) Not sure about write, read.. deeply nested for loops, error handling taking up place everywhere, literally more verbose than even Java
5) it has plenty of hard-coded shit already, due to missing generics for a huge time (e.g. built-in data types)
Javascript is quite performant, but I have to agree on concurrency.
JVM languages: I don’t think it has bad tooling at all, and it is fucking performant (with much better GCs).
It makes some of the best tradeoffs I've ever seen, which is maybe the highest praise I can give as an engineer.
It's a somewhat uneasy feeling to see how the "solution/projects" (in Visual Studio terms) hierarchy keeps getting reinvented in every. single. bloody. ecosystem, even in non-PL ones. Surely there must be a better way to organize things?
The modules in a workspace may have entirely different ownership. For instance, let's say you're using a WebRTC library to run a selective forwarding unit as part of your video conferencing app. Maybe you notice a bug in the WebRTC library. You can check out the code for the WebRTC library and add it to your workspace so you can test your application against your local WebRTC library changes.
E.g. if your workspace looks like this:
workspace/
mod1/
mod2/
Then both mod1 and mod2 will pull their dependencies remotely.But if you add a file to workspace/go.work, then local versions of mod1 and mod2 can be used without changing either mod1 or mod2.
As you can see it's quite complicated. The contents of go.work is literally
mod1/
mod2/
Also, the go.work file is meant to be your local workspace. If mod1 and mod2 have dependencies between each other, they should declare them explicitly and pull them in from the remote in most cases. The go.work file is only when you want to work on both modules locally. Meaning you don't check in go.work to your github repository in most cases.I wasn’t aware about “not checking in go.work”. What if you want separate modules to be aware of each other but keep them in the same repo?
Yes and no. It depends. You can use modules. With modules, each sub project lives in a directory under the root project. Maven reactor figures out the build order and all that crazy stuff. It’s the same with sbt and gradle.
Has anyone had good success with any particular resources?
Maybe also frp and hugo.
https://github.com/abiosoft/colima
And many others, really.
Neither sound like "applications I really enjoy". More like applications you suffer.
> And many others, really.
How about examples?
Sorry, I'm not here to tease or please you.
I'm pretty sure you can use google or github search to look for popular Go based projects and try them out.
So you replied to a comment asking for examples, without examples, because... your life passion is to be an ass?
And I'm not eager to bruteforce the "right answer".
As a .NET guy, I've actually started looking for tools built in Go since I know they'll be easy to run across the platforms I use, without extra dependencies.
https://learn.microsoft.com/en-us/dotnet/core/deploying/sing...
I also exclusively host dotnet applications on Linux (and also develop them there), and it works just fine.
But if I'm looking for a tool someone else built, .NET options are usually pretty slim.
I found it a little bit redundant coming from already knowing common programming constructs and C, but it was still a good introduction to the syntax and especially for the more unique features like Goroutines.
It's somewhat akin to what "The C Programming Language" (K&R) is to C. It's a fairly practical and on-point, and has a lot of (somewhat tricky) exercises, while still providing a complete overview of the language.
I think error handling is the thing about Go that people get most wrong. I believe that what's happened is that Go expected its users to be people fleeing C++ (they definitely got that one wrong), and instead they got a flood of Python and Java developers. Systems programming is error programming. Hiding and abstracting errors in systems code isn't a win; it's a handicap. Fiddly decisions about errors is the whole ballgame. But that's not the case in application code (EAFP!) and Go has been beset by that countervailing sentiment ever since.
That's not to say Go's got error handling perfect; it would be better with matching. But no mainstream language gets errors perfect. What unifies the strong systems languages is that they enable the overt programming of errors.
The second problem is nil, use of which is common and doesn't enforce error handling.
Are you saying that you’ve had 5 bugs caused by unnecessary imports?
It did so 5 times, versus how many times it gave a false positive where you just had to delete the import or underscore the variable?
Warnings are intended precisely for this type of "hey, you probably made a mistake here". Errors should imo be reserved for cases where the compiler is more or less sure you are wrong.
Rust has probably the biggest possible focus on "making sure only correct programs compile", and rustc does not error on an unused variable or import; it gives a warning. Precisely because it does not know if it is a bug, or just, I don't know, "commenting out the code that uses it to try something".
Also, in the case of an unused variable, you CAN blow it off, by using _. In fact, this makes it far easier to forget about it later compared to a warning that can be silenced in the same way, because if it's a warning you'll only do it if you're actually sure, while with an error you might do it even if you're less certain, because it's absolutely necessary.
Although, I guess this is the difference between being 13 years old and 7. For Rust easily accessible CI has always existed, for Go at the time of launch it took effort setting it up. Therefore best practices were pushed into the compiler to the detriment of the users, giving the same benefit trading effort writing the code.
There are other footguns like the unpredictable by-reference behaviour of slices and the printf functions corrupting your output if you make a type error but they are much less severe than the misguided idea that implicit default values are useful.
I don't like Go personally, but I have advocated for Go to be used in many situations depending on context. It's unproductive to start a language war here since it's generally situation-dependent.
Pascal had exceptions for error handling and got generics before Go
I was tired of the dogma of wrong in PHP. Everyone was an armchair expert trying to tell me how wrong everything I was doing for the last 11 years as a professional was.
Go was a relief. At first I didn't get the hype, why were variable definitions the other way around? But then I took the tour and bam. Instant love and i learned so much about everything using it.
It's not perfect but good enough for my needs.
I feel like it's a language that is the product of a higher language, like swift. Because it's so primitive. I also find it easy to translate thought to code with Go.
1. wide range of OS's you can target
2. no dependency on libc, you can build and copy over a binary to a machine and it will work
3. fast compiles
4. amazing stdlib, makes writing no to low dependency code very attainable
(imo) there's very little reason to start a new backend project in ruby/python/js when you can just use go and get better performance
> doesn't seem to be making much traction
Its one of the most in demand languages, at least according to linkedin in my area.
What do you mean by this? By what metrics and what would qualify as making traction? Many companies use Go and many prominent and widely used open source projects are written in Go.
> it's Bell Labs heritage
I have never met a person (in person, offline) who has cared about this at all.
> pretty syntax
One of the most common things you hear as a knock against Go are things that explicitly do not lead to "pretty" syntax, such as its error handling.
Overall, your comment confuses me.
> I have never met a person (in person, offline) who has cared about this at all.
People care about it because the language has the Bell Labs "feel". They don't care about it the way a dog breeder would care about a dog's ancestry.
(And of those who care about the "feel", some view it as a positive, others as a negative...)
I'm not attacking, I am honestly asking: What does that mean? That it feels C-like? Or something different? A lot of languages are C-like in syntax, so Go is not exactly unique in that aspect.
Contrast with e.g. Haskell which is based on the belief you can always improve some code by making it more general, or Rust on the belief you can improve it by making it safer, or Java that you can improve it by breaking it down into smaller chunks.
Languages carry aesthetic values. Most are aren't committed to them above literally all else - we all want our code to be somewhat simple and general and safe and isolated etc - but other than the most kitchen sink-y of them (C++, JavaScript) they have priorities among those which show through. (And even in those you can find some, muddled as they are.)
The difference was that C was written by people who were trying to write an OS, and they had to create a language that was able to handle all the odd things involved, like writing directly to hardware. Pascal really didn't want you to do that.
I'm not saying that Go lets you talk directly to hardware - I don't know whether it does or not. But I think that Go has the "get out of the programmers way and let them write code" approach more than, say, Haskell, or even Rust. That (plus the C-like syntax) probably is the Bell Labs feel.
Since the sibling comments already addressed everything else, I'll talk about this one:
> For a compiled language it's not very fast
This is essentially true, _but_ a lot of Go users are coming from Python, JavaScript, and maybe Ruby and Go is much faster. Additionally, it comes _close enough_ to Java while typically having a much smaller memory footprint that it really performs quite well in a cloud environment.
But the memory footprint is amazing. I've got three servers plus up to 10 test environments running on one small VPS (Virtual Private Server), and everything runs as smooth as can be; memory usage stays below 1GB, leaving enough space for the db server. In a previous job, we had a Java/Tomcat server, and it was filling up memory without doing much. I've inherited a Python server in my current job, and if you don't take care, it slowly grows to 4GB. Go is really a blessing for the back-end.
But, yeah, my experience with Go matches yours. It's really amazingly light on resource usage.
The problem is, that wasn't really well advertised or understood, so people would see a Java program using lots of memory and assume that this is how much it really required. The elasticity wasn't apparent. These days the JVM can kick off background collections from time to time to reduce memory usage if the app is idle. However it still won't aggressively collect if the app is running because, again, if you haven't capped it then it figures that it's better to serve requests than do lots of "pointless" GC work.
Fundamentally with GC there's a throughput vs memory usage tradeoff. Go faces the same issue which is why GOGC exists:
…really? Go seems blazing fast to me…
1.17 saw a major performance boost because of the switch from stack-based to register-based ABI for go function calls [1]. The release notes claimed a 5% average improvement, but benhoyt (in that discussion) has benchmarks showing much greater improvement for GoAWK.
Comment police/gatekeeping discussion (outside of dang actually enforcing the rules) is probably the most annoying behavior on this site.