Right, absolutely correct, Java is a great choice, so why does this post keep going on about Go?
I used to love Java but the complexity merchants showed up and ruined the party. 1.5 was just coming out when I stopped doing Java dev. Kotlin might pull me back into the fold though for when I can't use Go.
https://en.wikipedia.org/wiki/Java_version_history#Java_SE_8...
To be clear, all the annotation and Java EE stuff is still going if you want it!
Java's warts are far worse than Go. Everything is nullable. There's no module system to speak of. It's so IDE-dependant.
I agree with the spirit of "use boring technology." So thank god Go is boring enough that I don't have to write Java anymore.
Only reference types, the same as in golang (which also has nullable pointers, and its interfaces interact weirdly with null). Java is getting value types, which can be declared as non-nullable.
> There's no module system to speak of.
https://dev.java/learn/modules/
> It's so IDE-dependant.
Modern Java has been simplified that you can run a hello world program as follows:
$ cat Hello.java
void main() {
IO.println("Hello");
}
$ java Hello.java
Hello
Furthermore, any non-trivial project is better served with an IDE, regardless of language.But overall Java, like Go, IS a boring language. And that's a compliment.
No it really doesn't. It litters your code with if statements that are all just about the same, except that one that needs to be different, and you go blind looking at them all and can't spot the difference. And these days people probably just type "tab" and their LLM assistant fills out the entire block incorrectly in one keypress copying the pattern from everything else.
But LLMs didn't create that problem. Having to type something never meant you had to think about it, or thousands of "sudo shutdown -r now" commands would never have been run on production databases, because typing "sudo" would have magically made someone think about it, rather than just being keyboard memory.
And the problem of reviewing the code and spotting the one error handling block that should be different from all the others is always going to be there for human reviewers.
Rust converts the common case boilerplate down into one character: ? which lets you focus on any exceptional error handling rather than a wall of if statements that almost all look alike. And the compiler can see that you're ignoring a Result from a function call and force you to explicitly do something about. Plus you can then use a monad without knowing a single thing about monoids, endofuctors or category theory, and impress your friends.
Wrong thing to be bothered with. This allows static analysis of every possible path the code can take. Try to do that with throw/catch. There is a reason many industry guidelines like misra, jsf, etc just outright ban hidden paths. They have been literally catastrophic. There is a reason why many modern languages like go, rust, etc, want errors to be explicitly handled.
Go documentation specifically reasons why they treat errors-as-values and why is it like that - https://go.dev/doc/faq#exceptions
> Rust converts the common case boilerplate down into one character: ? which lets you focus on any exceptional error handling rather than a wall of if statements that almost all look alike
Again, wrong way to look at errors. Errors are not a problem for a compiler to resolve. Errors are part of your business logic. They need to be explicit and handled. The syntax of one language vs another is not the point of error handling. Errors are not a distraction. It is your job to review each path the code can take.
Code may be written once, but code is always reviewed or audited or referenced more than once. No one is impressed if you write some meta magic one-liner. I would rather be more impressed if I can read your code and form a mental image on how many paths your code takes to achieve an outcome in a single sitting, instead of pinging you for a quick call to explain yourself.
> you go blind looking at them all and can't spot the difference
> And these days people probably just type "tab" and their LLM assistant fills out the entire block incorrectly in one keypress copying the pattern from everything else
Now, if someone is 'going blind' looking at error checks, its because their function is doing too much and has too many failure points. Perhaps one may need to focus on honing their software engineering skills on how to structure their code, instead of hitting that 'tab' more so often.
The overwhelmingly common case is for an error in a nested call to be bubbled up to be handled by a parent call. If you make this common case look similar to the distinct case of actually handling an error, you just obscure where the real error handling happens.
Writing good error handling is Go is really hindered by two other issues mixing together: 1. There’s no Result type, so while it tries to treat errors as values, it’s missing out on a lot of the benefit of that idea. 2. Multiple function return values are implemented as a special case, and aren’t themselves a value
Most languages that support multiple return values, do it via some notion of tuples: returning a single aggregate value that you can pass around as a whole, but that also have some nice syntax for destructuring them into local variables. Go implements as a syntactic special case.
You can’t assign the multiple return values of a function call into a single variable. You’re forced to take all the parts, and pack them into a struct yourself. This means that you can’t factor your result-handling logic into testable functions, without needing to do this dance at every call site.
I am very impressed if they can express the entire algorithm succinctly with a one liner. That's the kind of abstraction every piece of code should strive for and be shamed to the ground if they haven't.
> I would rather be more impressed if I can read your code and form a mental image on how many paths your code takes to achieve an outcome in a single sitting, instead of pinging you for a quick call to explain yourself.
Does this mean you're impressed by the ability to read go's if spaghetti?
I mean I'm explicitly supporting how rust does it over go, so I don't understand how this is any kind of rebuttal. In your last sentence you admit that rust enforces that errors are explicitly handled, and I think it does it much better than go does, and does it without littering the codebase with boilerplate if statements. The fact that you're launching into an explanation of the insufficiencies of throw/catch exception handling when that has absolutely nothing to do with what I was arguing... Well, I don't feel the need to argue with you any further. Rust also has errors-as-values and doesn't throw (at least not the common case, just like go), so the go FAQ on exceptions isn't relevant to the discussion.
And I'm just going to end the argument with that, since you seem to be a waste of time to deal with.
If most of your error handling is just bubbling the error up, you are doing something wrong.
if err != nil { return err; }
is an antipattern. Not because it's verbose, but because you are supposed to handle the error, not pass it right through, in most cases.But it's also a pig to write and comes with a lot of foot guns. Especially the Null handling. Somehow they made it worse than every other language.
Well, they'll call it a design decision, not a bug. I guess I'm being charitable.
To get true nullable fields you need to use pointers. That's a whole topic in itself but they're awkward.
It's much worse than true nullable objects that your compiler can check for NPEs. It throws fewer NPEs but at the expense of data integrity where you don't know if your 0 is actually a 0 from the user or a null.
.NET has almost all these upsides, but with a concurrency model (async/await) that is (now) more transferable to other languages.
The only thing I can think of: I dont think c# can compile as easy to a single executable binary, like Go (or even rust)?
Otherwise Java, .NET, Typescript (with possible C++ addons).
For me go is just above c# and both of those are not super high on my list.
If it's "Large standard library", I think that's a selling point. Having anything you need available ( although these days, via optional microsoft.* packages ) helps keep projects consistent between different places.
If it means "Different ways to do the same thing", I can understand that criticism better, and some of that comes with 20+ years of legacy, where the wrong thing was done, and now there's a better way, but a ruthless adherence to backward compatibility means that the old way isn't dropped.
Things I dislike:
- if err != nil. Just give me some syntactic sugar instead of letting me write the same thing a bajillion time.
- no way to bind a struct to an interface. I'd like my IDE to tell me when I accidentally stopped implementing an interface
- some stdlib parts are too bare bones. Unpacking an archive requires me to handle all files, directories, links, etc. myself. There is no move command that can move a file or directory across fs boundaries. The little things.
var _ MyInterface = &MyStruct{}
Now your compiler will tell you you stopped implementing the interface. Pretty? No. But it works. And gopls will even offer to implement stubs for missing methods. var _ MyInterface = (*MyStruct)(nil)
In my mind as I read this, I am reminded that it is the receiver type *MyStruct that implements MyInterface. YMMV.I don't know about others, but Goland's analyser is pretty powerful and can navigate from interface to implementation(s) and vice versa.
For non agentic coding Go has terrible error handling. It does not have exceptions or monadic error handling. Some call that a feature but many avoid Go or that specific reason. This will not change because that debate has been settled so if you can live with if err != nil after each function call (almost) then you are fine.
Things that is beautiful with Go are: * Its simplicity * Superb cross compilation support and excellent support for many different OS/arch combos. Not sure if anything comes close to this level of easy way of compiling to Many targets and target support.
The KEEP for Result<T> goes into details, but basically there are a few ways of handling return values + errors:
1) throw exception if something goes wrong
2) return null if something goes wrong (stdlib XXXorNull)
3) Use Result<T> in some cases, its error type is not paramerized and catches CancellationException
4) Use Arrow if Result<T> doesn't meet your needs
5) Return Sealed Classes / Interfaces with all the possibilities.
Right now, I am going the Sealed Class interface route, but its such a verbose pain in the ass, so I only use it at certain levels of abstraction (like from a Repository, or library API, etc).
The code I needed to write was calling into okio, and it was not straightforward to figure out what kinds of exceptions would be thrown by the JVM layer underneath (docs just say IOException, but sublasses can be thrown).
Its sad to see they still haven't figured this out. Rich Errors was mentioned a year ago but its not even in preview yet. Its also not clear how it will work with Java interop.
Cross-compilation has been a standard feature of every compiled language toolchain for the past decade at least.
Go is handicapped in significant ways which were specifically designed so that it was easier for humans to write. People pushing for Go code in this age either don’t understand the future or the past
Like, enums. I get a lot out of the box when I use an enum in Java or Kotlin. Converting to/from a String is trivial. Type safety ... exists.
I can do that in Go, but I have to hack it in, for every single enum type I want to represent. Enums are not a thing in the language, which means its easier to keep the language in your brain all at once, but at the expense of making it harder to keep the software I'm writing in my head. Is this "enum" the same as that "enum"? I have to go read the code to figure it out.
But Go is excellent at a lot of things. Compile times, static binaries, resources compiled right into that binary, execution speed ... there is a lot to love.
For example, to build a full production web application with database in Go, there is no great out of the box migration tool. There are some good 3rd party libraries of course but compared to something like EFCore in .NET, they don't come as close.
For me, it is now .NET and then Go. Of course, I use Go when just doing a lot of non web stuff as well.
"compiles to a binary" is not a useful criterion. The criterion Go is winning on is "compiles to a single, completely self-contained binary," meaning it does not depend on libc or any external runtime. You can't say that about .NET. You can't say that about damn near any other programming language. It's extremely rare. The fact that .NET uses a binary packaging format is, like... well ok, so what?
But I wonder how well it can cover similar use cases? Go is great for devops and web backends. But what about AI and data science?
The Go ecosystem for data is very limited. There are no widely supported dataframe libraries (like the og pandas and the newer polars written in rust and also available as a crate). Very few data science libs, a few decent gen AI libraries, but not as popular as their Python cousins.
Most of the work I do now is streaming data and very small batches. For that Go is amazing. I don't need dataframes to transform a json, combine it with a bunch of other data and write to a database. I just need to write that logic and make it go fast. Very easy in Go.
But I’m using the slower language because it still integrates with more things
For example, one reason AI is all in Python is because CUDA is basically part of the C ecosystem (ie build system)
You can just embed the C library into the binary of the Go app call it directly in Go. Most of the time I have found calling C from Go faster or on par with calling C from Python.
I know the OP said dont use external libraries, but I love bubble tea (And their related libs), they are a great reason to use Go for TUI
that said, I only use Go for hobby projects, I dont know how good it feels if you have to use it for work 40 hours a week
"This is getting really fucking irritating. Every 3rd comment on every HN post is "This is LLM", which has become a proxy for "I dont like it so it must be llm"
It's one of the dullest, most mediocre languages out there and despite a nice toolchain and the fact it's undoubtedly a "safe" choice, I just have zero interest.
Haven't really used Go, but can't someone just `result, _ := foo()` and go on using `result`, not checking any errors?
The way Rust does it seems closer to forcing you to handle any errors in order to obtain the result (though it is still easy to just `.unwrap()` without properly thinking about it).
Time to update all code references to Gitlab across the globe, in every single Go project.
Or spend the time configuring redirects between URL mappings, across everything that depends on it.
Not to mention that except for lacking garbage collection, even Turbo Pascal 7 for MS-DOS was more modern in language features, with faster compile times, on a 20 MHz powered computer.
The import path is disconnected from where the build system looks for the files. Of course, the default is to use the import path as an URL, right, but nothing forces you to do that. And if you do, *you* lock yourself to the hosting you use. But that can be hardly blamed on Go.
Documentation here: https://pkg.go.dev/cmd/go#hdr-Remote_import_paths
Full example: https://github.com/rsc/swtch/blob/master/app/rsc-io/main.go
tl;dr: Serve this at your domain, update the repo-root when changing host platform for your code: <meta name="go-import" content="import-prefix vcs repo-root">. No one else has to do anything.
Additional work that is superfluous in other mature programming ecosystems, that don't have the dumb idea to use SCM urls as import paths.
In the parallel HTTP fetcher, the error is discarded. This will likely result in a panic when the response is nil. Also, what if it a server locks up? Or the underlying socket never connects and never times out?
I know it’s a toy example, but one must consider all these things in a real system. Go does have good pathways for these concerns, but it’s also easy to do it wrong. I still have to manually reason about access to variables/struct fields from multiple go routines.
The language that actually solves concurrency is Rust, but they will never acknowledge that.
so Go + Templ + HTMX (aka the GOTH stack)
Or, if you prefer more of a power tools feel, then HTMX and Raku in functional style (https://harcstack.org) maybe to your liking. Which I call the Crotch Rocket of the programming world.
- https://raku.org (dog-fooding)
- https://harcstack.org
- https://cragcli.info
It's fairly early days ... but a couple of other Raku community sites are taking it up.Goroutines? Meh. Software transactional memory and green threads? Heck yeah.
An actual type system? Chef's kiss.
Scott Wlaschin from the F# world has written and talked extensively about F# for "boring" software. It works equally well in Haskell. You don't need to use type-level meta programming to spit out a basic service.
Monads are a great honking idea, btw.
Really hate we don't have proper type hints for Django.
...who invented this letter-casing convention?
import (
"embed"
"html/template"
"net/http"
)
//go:embed templates/*.html
var files embed.FS
var tmpl = template.Must(template.ParseFS(files, "templates/*.html"))
why is the name of a module lower-casedbut the names of functions accessed via its namespace upper-cased?
how does this make sense?
The syntax changes a lot from the C one, and I can't see any reason for it. To me, it looks unstructured, with the lack of colons for example. It ignores memory safety, it feels like it ignored all of the typing system research since C, no discriminated union, and structures and types in general are heavy to write. It encourages bad patterns, errors out on mundane things like an unused variable, forces you to handle errors with a lot of code while not catching much more than C in terms of bug-prone practices. The package/module system is a nightmare for contributing to open source projects. Modifying a dependency to find a bug is very hard, even swapping a dependency (version) is annoying.
And what do you get from all of this compared to C? A garbage collector, tuples, and goroutines. No metaprogramming (aside from generics, and that was a whole story), interop with C is limited. To me, it looks like it does not focus on the algorithms, but on the code implementation, which is imo what leads us into poor programming and missing critical logic flaws, because the logic is buried. I may have forgotten other gripes I got while working with Go, but honestly, if I wanted all of that, I would pick D, at least it interops well with C and has metaprogramming (and has been made earlier, which excuses a little the lack of certain things).
But really, I am open to someone explaining me how they enjoy Go. Because I feel like I should be wrong as I see most people (which, for some of them, I know are clever) praise Go.
Edit: I added modal expressions to make it clear that it is my opinion.
Go has less flexibility, no pointer arithmetic, a healthy package system, and a smaller domain. Mostly consuming or providing network services. My favorite feature is channels. For me they make levering the performance of multi-core CPUs straight forward, and dramatically nicer than the C approaches I've tried like pthreads and mutexes.
I wouldn't rate go as secure as rust, but has a pleasingly developer friendly approach. Seems way more secure than C.
Making a pipeline where each stage is 1 to N threads is pleasingly easy, reliable, and performant.
I'm using CC+Opus 4.7 max effort, and it's produced a working apt cacher from the first phase of development, so far there have only been a few things I've had to ask it to fix. This is over ~52KLOC (counted by "wc -l"), going on day 3 of it working on it. This includes: caching proxy, garbage collection, "http://HTTPS///" kludge (apt-cacher-ng semantics), MITM https proxy, admin website + metrics, deep validation of metadata and rejecting invalid updates, snapshots of upstream state and delayed metadata update until "hot packages" are available after metadata update...
10/10, would go again.
FYI: My agent loop is: "Work on next step, have codex review it, compact", and then a couple rounds at the end of a phase to review the code against the spec, and a couple rounds at the beginning of a phase to create the spec.
Error handling also seems pretty dumb in comparison to Rust. Admittedly Rust is a much more complicated language, but I felt like I could just go learn more Rust instead of bothering with Go and have more fun.
Like, if the gripe is that Go should be better about following XDG conventions, fair enough. But pretty much every language other than C, C++, and Node tends to use system paths for transitives/compilation artifacts/registries/downloads.
Also: why can't we vouch for flagged stories now? This post is actually good, and funny, and the conversations are worth having.
I do think a lot of projects would be better served having been written in go instead of java, or whatever else.
I don't think it's a panacea for anything. It's pretty easy to shoot yourself in the foot with. The easy stuff is easy the hard stuff is really hard.
I like rust a little more, and I don't rewrite things with it. I choose it first. That's my preference but go ahead and gopher on.
Like, what if you want some styling or javascript along with your HTML? Either write everything by hand like it's 2003 again, or start inventing a complex pipeline from scratch in Go. Countless of people have spent thousands of man-hours on solving this problem, but you use Go, so now you do this yourself from scratch.
Or maybe your app needs some relatively complex database access? Writing SQL-spaghetti only gets you so far until you need to do conditional joins and unions and parameterized subqueries etc. God forbid you want migrations too. Go has no answer for this and you either have to hand roll all those things as well, or spend tons of time comparing countless libraries that do some of things you want, but not quite everything, or don't fit into your pipeline for whatever reason.
I'd take an opinionated framework with fat dependency stack that has way more features than I'd ever need over trying to reinvent migrations or CSS preprocessing from scratch in Go, thank you very much.
Oh boy, the author has clearly not seen some of the Go codebases I’ve seen.
It truly does not.
In the end, the language doesn't really matter and extremely rarely is the bottleneck.
Python, Java, Go, C#, PHP, Node, Ruby or Scala, for backend stuff, all these languages can do the work 99% of the time. They all have well developed ecosystems, and are sufficiently mainstream to easily find developers.
In the end, these languages are just variants of the same tool, and I find more interesting to just pick one and build something with it, rather than bickering endlessly about which one is better.
And of course there is Rust, which compared to Go has better performance, lower memory usage, smaller binaries, safe concurrency, safe resource handling, and a real type system with tagged unions.
The Go team built good tooling around a mediocre language. Now most other languages have caught up, and Gophers are left writing mind numbing `if err != nil` checks with no benefits left to show for it.
You need a lot of linters e.g. to make sure errors are being handled, and the lack of algebraic data types make expressing data difficult.
I do think it has merits, but I’ll take type safety over simplicity
[0]: https://git.sr.ht/~blainsmith/blog/tree/main/item/.build.yml
I would not consider anything else without a good reason and especially never going for Javascript or TypeScript for anything server related.
You cose something and if you don't use some weird 3rd party packages (Go stdlib is quite complete) you can check that code again in 3 years and will still work.
In seriousness, Go is a good choice. Or at least it’s not a bad choice, I’d definitely pick go over many other languages. If go had a better type system, it would be damn near perfect.
Though TypeScript's type system is maybe still more powerful - and therefore might have the edge for agents writing code? (Not to mention there's probably more TypeScript in the training data for LLMs, though perhaps there's _better quality_ Go - I'm not a Go dev though so I couldn't comment further on this.)
Having backwards compatibility with 1.0 just makes it easier to maintain software.
The big plus in the modern era is that the simplicity of the language lends to having agents write Go without much fuss. That and the standard library being batteries include lets you direct the agent to use little or no third party dependencies.
Just fucking use the language your colleagues can understand and support for the next few years.
Just fucking use the language with the framework and tooling you need to get your job done efficiently and effectively, and one at the appropriate level of abstraction for the project.
Just fucking use the language which AI agents can read and write well, because we're in the End Times and this stuff matters.
Just fucking use the language with great testing and CI/CD support because you'll be spending longer supporting your code than writing it.
The skill is choosing well, and a key realisation is that it's never a one-size-fits-all thing. That's why an article like this is less than helpful, gosh darn it.
That's a DoS attack in the python world.
Citation needed. Go's fat binaries and big stdlib cause most enterprise-mandated CVE scanners to light up with zillions of false positives constantly, because too much shit is present in the binary.
Logger package technically could speak protobuf over gopher, even though you use it to write text to syslog? Congrats, gopher and protobuf ecosystems are compiled in, with their vulnerabilities! Multiply that by every single golang binary anywhere in your system (seriously, I was getting CVE alerts for un-hardened stdlib cryptography in a 50loc file copying backup tool that could have been a shell script, and audio format conversion buffer underflow CVEs for Traefik, and many many more for months) and it adds up to a pain in the ass.
And heaven help you if you do actually have a vuln in third-party software that needs to be patched without an upstream fix (usually because "upstream doesn't make distributions on golang-$LATEST or $TRANSITIVE-$LATEST and they have a roadmap item to do that next millenium"). You can't install an updated transitive and fix it, you have to recompile and somehow distribute the whole thing. Doing that is never as simple as "go build" for big projects: the remaining 20% of build/toolchain needs that Go itself doesn't cover are inevitably fulfilled by the same pile of it-works-on-the-maintainer's-machine rickety Makefile bullshit we always had, but without even the sanity and conventions of autoconf and friends--and yeah, building others' Go projects in anger/in a hurry is enough of a pain in the ass that it makes me miss fucking autoconf.
On balance, I like Go. And there's a lot to hate about dynamic linking and package manager hell. But Go's approach is definitely not without its drawbacks in the CVE/security space.
OCaml?
The big idea with LLMs is consistent references in the training corpus produced cheddar output by the language model during inference.
Go is an amazing language for language models because it's actually quite boring predictable while packing a lot of powerful distractions with a world class tool chain supported by Google and strong std library as well.
As a programmer I actually hated writing Go... and wanted to write Rust; but using coding agents makes me appreciate writing Go more.
I can get consistent results out while having concurrency cross compilation and predictability.
https://jry.io/writing/ai-makes-golang-one-of-the-best-langu...
lol
> if err != nil {
lmao
> defer rows.Close()
Oh dear
...
I'm only poking fun, I'll take a go backend any day over most of the alternatives. Same goes for CLI tools.
Maybe I develop games. Maybe I develop IoT devices. I might even be working in a high-stakes environment where formal verification is needed, who knows.
Whatever the case may be, we all have our reasons for choosing certain technologies. Not everyone is building run-of-the-mill 'backends' after all.
So please, let's stuff that neckbeardy arrogance away. It serves no purpose and distracts from the discussion.
Thanks.
P. s. I develop my backends in go.
For non-trivial golang apps you're still gonna find npm in the mix. I recently packaged forgejo, yopass, and a few others, and if you don't have `npm` on the build machine, the resulting daemon won't serve the front end.