Just Fucking Use Go
111 points by zarldev
111 points by zarldev
Not to shoot the messenger, but my god this style of blogging is tiring and juvenile. It was funny maybe the first time and then became exponentially more annoying with each riff.
That being said, yeah, Go is nice. I recently transitioned to a Go project from a TypeScript project and my mental health and work morale is rapidly improving.
if err != nil is the feature, not the bug
I accept this, but still think it's Go's biggest wart. Including sum types could've made things so ergonomic without forcing devs to resort to runtime type assertions.
I prefer this to the AI slop blog posts that equivocate about everything and take no real position.
A new class of crummy blog posts doesn't mean we can't keep our standards right where they were and critique both.
I find this puritanical pearl-clutching to be very tiresome.
I didn't read "funny the first time, then more annoying" as taking a moral position. I think the writing style just falls flat at its attempt to entertain, and then it's in the way.
You're assigning a moral value that isn't there to dismiss a valid criticism. This is a type of post that trades clarity and credibility to try to entertain by imitating the myriad other posts like this. But I think a lot of us just think this type of post has long exhausted its capacity to entertain.
Once the post gets to "You're out there gluing together fifteen Node packages", I can't tell whether or not this is exaggeration commensurate with the rest of the disparaging post, or if this is for a totally different demographic, because I don't do that. "Stop pretending you need a framework" makes the silly implication that the author is the only serious engineer, while everyone else is pretending. Choosing to use a framework is actually just a decision you can make when understanding the tradeoffs.
Is this the real (and incorrect) viewpoint the author has, or is it something forced by the character they're taking on? It depends on how generously you're willing to interpret someone who is being an asshole on purpose. Said differently, I might say "Just articulate yourself clearly, poopy butt fart face dummy."
This isn't an indictment on the author as a whole. They wrote one blog post I think isn't very good, and a blog post does not make the whole sum of a person. I think most bloggers write middling posts while imitating others they've liked, including myself.
Fifteen node packages isn't that many, I don't think they were trying to be hyperbolic or rambunctious about that part
Ah yeah, I meant to refer to the whole paragraph by that sentence:
You're out there gluing together fifteen Node packages, three TypeScript build tools, and a Kubernetes cluster to serve a fucking form. You hired a Platform Team to babysit your Rails monolith. You convinced your CTO that Rust was necessary for a CRUD app that does maybe forty requests a second. Congratulations, asshole. You played yourself.
There is definitely a lot of over-engineering that goes on but it's not clear to me whether I'd be outside the target demo, or if the character of the post forces a paragraph like this. If it weren't for me wanting to complain about this type of post, I'd not have given it another thought.
From your other reply, I think you are misreading or assuming facts not in evidence. Blog posts like these were fine, maybe even worth a wee titter, when I was in my 20s. Heck, at 12 I would have the thought the author the absolute coolest. After 25 years of reading posts like this, they’re just another trope. Every generation thinks they invented sex. And I, my dear boy, am frankly fucking sick of it.
I’m 40. I am not your boy.
I miss Ted Dziuba’s writing and think “Node.js is cancer” and the even more sublime “Taco Bell Programming” blog posts are classics of the form. Like this blog post, they are connected to a pragmatic sense of no-nonsense craftsmanship that is concerned with the quality, stability, and maintainability of the produced software— software in this case viewed through the lens of being an artifact created by the hands of a human artisan.
Unfortunately I think that the discourse in public programming spaces has become dominated by a sort of Veblen peacocking, where the most rewarded viewpoint is the one that most impressively demonstrates the author’s cleverness or erudition. The more clever or arcane and the less rewarded by the market the better, because it demonstrates that the author can afford to ignore material reward. In that way so much of what passes as technical blogsmanship these days is instead a sort of performance of luxury and an attestation to its author’s position within society.
This blog post is of course contrary to that. It uses the language of a carpenter, a truck driver, a sailor, a construction worker: it communicates to the reader without embarrassment that its author is a worker and that the topic is one of labor and not on of leisure. (You could argue that this post in question is also an attestation to its author's position within society: that of a worker, so therefore contrary not in that it rejects the idea of communicating something about one's position but contrary in that it is a different position)
To me that is fine. It takes all sorts of perspectives and angles to see every facet in something. Nothing about this viewpoint should be dismissed out of hand.
Okay so? Just because one is worse doesn't mean they aren't both bad
I don't think this is bad. I think this is good. I do not recoil in horror when someone writes "hey, dipshit".
Well then defend why it's good rather than just saying something else is bad.
Frankly I find it insulting that you think I'm some puritan or am particularly offended by someone using naughty words simply because I dislike this style of article. The style of this blog post is simply annoying and could have been written in a way that gets the authors point across more succinctly.
Here's my view: it's fucking played out man. It's boring. It's not unique. It's like a thousand em dashes or it's not x it's y. I am simply tired of it because it's been done to death. Get a new shtick
Well then defend why it's good rather than just saying something else is bad.
I think it's clear from my original statement: I enjoy blog posts where the author has a concrete opinion on something that they describe and articulate in full, as this post does. I do not like blog posts that go "whether you like or dislike AI, we can all agree, AI exists...", where the blog post is doctrinal about the appearance of neutrality, where the author's true feeling and intentions are obfuscated. Nothing is obfuscated here. I know how the author feels, and I know why he feels that way. I appreciate blog posts written like that, as I find they are increasingly rare. I do not feel the need to re-state the claims made in the blog post about Go, the blog post already does that.
From your replies it's clear you like grandstanding about the imagined puritanism of people you disagree with on aesthetic grounds
not sure what to do with that, should I apologize for staying on message? "Grandstanding" implies that you think I'm being disingenuous, but really I just notice similar patterns and problems across threads. Do you think I'm being disingenuous? Are you also annoyed that Simon Willison keeps talking about AI? People tend to be, at least to some degree, consistent.
> I enjoy blog posts where the author has a concrete opinion on something that they describe and articulate in full
Thank you for highlighting this. It's exactly the kind of Critical Discourse I like to read on lobsters.
edited: the other comments I referenced in my remarks were removed from this post
ngl this entire blog post reads like it was AI generated
I've worked with the author before. I assure you, it is not.
If I like this writing, why would I recommend the author go to training to change his writing?
I'd look for a course in communications or new media
thanks, I have a masters degree in game design. Here's my thesis on how the mechanics of a game intersect with and inform a game's narrative message, how a game's message can be carried by mechanics and level design alone, and what sort of mechanics and level design would invert the dominant message of theft and self-aggrandizement prevalent in most video games: https://ewr1.vultrobjects.com/jorelli/The-Seal.pdf
Writing is not bad simply because you disagree with it.
For a taste of what critical reading is like, consider: in making their point, how did the author use inaccuracies or misstatements?
That is a claim you are making, not a claim that I am making. The burden of proof lies with you, not with me.
Just wanted to say, I am genuinely impressed at how you took a rather insulting post and managed respond in a way that took it seriously while also totally demolishing the point it was making. That was a demonstration of impressive writing skills.
Come on, it’s not my preferred style either but this is such a passive aggressive way of putting it
I found it entertaining but I have not read many of these type of posts. Though I prefer it when author calls me a "walnut" over "dipshit" because it sounds funnier to me.
So IMO if someone writes like this, at least be clever with the insults :D
Though I prefer it when author calls me a "walnut" over "dipshit" because it sounds funnier to me.
That line was the only quote-unquote insult I found genuinely amusing. The rest is just eye rolling.
I agree… Is there a way to report it? It doesn’t fit into any report category.
If enough people hide the post and don't comment, that's a negative signal that can help a thing fall off the front page more quickly.
It seems an unpopular opinion looking at other comments, and I don't want to sound harsh, but I really dislike Go.
Go has a not-so-bad syntax put on top of an efficient runtime for concurrency and the weight of Google behind it to drive the ecosystem. Apart from that, I find it horrible.
The main issue to me is that generally it seems to be designed with the explicit purpose of ignoring decades of programming language design research, or even programming language design practice. Now it has generics at least, apparently, decades later.
I'm not saying one should always use languages with dependent types, but come on! Go lacks any feature whatsoever to model your data, model your invariants, and structure your code, that any modern language should have. Surely Rust has a steeper learning curve but it does a better job in all those things. But you don't need a type system sophisticated as Rust's to be decent. If you mind compilation time, you can have a sound and expressive type system with simple but decent features very quickly.
And sorry but I find if err != nil to be just the most horrible thing one could imagine to litter the code with error handling noise everywhere. What do Go people have against sum types? Even Java exceptions are better at that. The reality about that is that the language does not have any feature to handle errors better, so people came to like the worst hack possible mistaking it for a feature.
Then of course, I wouldn't have written this comment if the post was not condescending in the first place. "Just use X" is just dumb. I'd say just use whatever tool it fits your use case and makes you comfortable and productive. If it's Go let it be, otherwise choose otherwise.
FWIW, my interpretation of Go’s position in the design space is that it chooses to prioritize simplicity for new developers working in large codebases and large organizations over just about anything else. Because of this, it’s often easy for inexperienced developers to read and make targeted changes to code without needing to build a lot of context. That’s helpful in particular for Google-like organizations where you have literally thousands of developers who may have short tenures on any particular team, or indeed the company.
In this context, and especially with inexperienced developers, the lack of an advanced type system is in some ways an advantage because it means that developers often don’t need to think about types. Like, at all, past very basic concepts like primitive types or structs. It provides very little tooling for modeling your data, but the flip side is that you can write a lot of code without really thinking about it!
This is IMO not really great for correctness at a language level! But in a large-org setting my experience is that teams rely more a large surrounding infrastructure of monorepo analysis, CI/CD, canary testing, observability tools, etc. That infrastructure becomes much more load-bearing than in smaller orgs.
(I am moderately a Go fan for similar low-mental-load reasons, insofar as I only write code on any particular project occasionally and I’m not currently engaged in any long-term projects on a daily basis. So it’s a strong advantage for me to be able drop into a codebase I haven’t looked at for a month, for less than an hour at a time. But if I were more of a full-time developer on a complex project I suspect I’d like it less.)
Because of this, it’s often easy for inexperienced developers to read and make targeted changes to code without needing to build a lot of context.
I disagree here, Go forces you to read a lot of context. The type system is hazy (duck-typed interfaces, frequently-abused return res, err, forced use of default/nil, all-or-nothing symbol visibility, no readonly refs...), making it risky to "just edit this func/file" without checking half the call stack.
In this context, and especially with inexperienced developers, the lack of an advanced type system is in some ways an advantage because it means that developers often don’t need to think about types.
This is nice for small projects, but quickly becomes painful at scale, where unambiguous types are crucial for maintainability.
in a large-org setting my experience is that teams rely more a large surrounding infrastructure of monorepo analysis, CI/CD, canary testing, observability tools, etc. That infrastructure becomes much more load-bearing than in smaller orgs.
That's not specific to Go. Unless you're saying that Go projects are more reliant on these tools, which would be a clear negative.
I've written enough go that this is my main issue. I love the compiler, the concurrency model is great for building applications. And I agree with many if not all of the points in the article.
But this right here is why I don't reach for it more often. The read amplification of go, I think at least a couple abstraction layers above the go code, the volume of code I read to find a bug or where to add a feature is silly.
Unless you're saying that Go projects are more reliant on these tools
Yes, that’s what I’m saying! Much of the correctness validation is pushed to shared test infra rather than at the language level.
The workflow I’ve observed many developers follow at very large orgs, is that they make a change, push to their branch, let CI run tests and iterate. In a monorepo setting there’s often shared test-running infra that also identifies tests of downstream dependencies that should run. So people will more frequently let CI run tests than try to run them locally.
which would be a clear negative.
To be clear, I agree with this! There are a lot things I like about Go — I do find it easier to keep the whole language in my head and reload context for infrequently-edited code. But I do think the lack of language-level correctness tooling is a negative.
In this context, and especially with inexperienced developers, the lack of an advanced type system is in some ways an advantage because it means that developers often don’t need to think about types.
The complaint isn't "Go should have an advanced type system to help with correctness", it's "Go should have basic type system features that have been in use for decades to make it easier to help with correctness". Algebraic data types are from the 70s; we've had error handling mechanisms that aren't 'if' based since at least the 80s; Tony Hoare called null references his "billion dollar mistake" in 2009. Generics are actually kind of complicated, these other things are much simpler.
There are two active proposals for adding union types btw.
Ah, cool. If one of these is accepted it would allow developers to model data a lot more accurately and safely. That would leave "error handling boilerplate" and "everything has a zero value", which I assume will never change because of backwards compatibility.
Some proposals that showed up when I searched (I haven't checked if these are all about ADTs vs. union types vs. something more specialized):
https://github.com/golang/go/issues/19412
https://github.com/golang/go/issues/76920
my interpretation of Go’s position in the design space is that it chooses to prioritize simplicity for new developers working in large codebases and large organizations over just about anything else. [...] That’s helpful in particular for Google-like organizations where you have literally thousands of developers who may have short tenures on any particular team, or indeed the company.
I can see how that's a useful property for a corporation, but also it felt really depressing to me. "Go is designed to make devs replaceable."
(Thanks for posting it, though! It's more detailed than the "Go is designed for new programmers" I've heard before.)
"Go is designed to make devs replaceable."
Yes, this! insofar as it’s also aimed at fitting into a large org that regards devs as replaceable.
I’ll admit that I never worked at Google, but I worked at Facebook during a period where the heritage from ex-Googlers was pretty obvious in a lot of the tooling and processes.
During that period at Facebook, most new developers were hired on in generalist roles and then matched to a team, rather than hired for a specific team. Internal mobility was very strongly encouraged, and you got weird looks if you stayed on a team more than about 3 years. (I gather this has changed in the past several years.)
We didn’t use Go, ironically enough, but the feel of the language certainly seems like it would work well in that environment.
I understand your point of view but a feature of good type systems is exactly that of making it easier to reason confidently about a codebase you do not know, because assumptions are explicit instead of being hidden in documentation or embedded in the team's culture.
And I'm not talking about advanced type systems. You can go very far away with just a basic but modern type system, with no cognitive load whatsoever. Go is a missed opportunity in this respect.
Honestly, I think what really happened at Google is that they wanted to replace C, and they wanted to ship the language quickly, and so they just improved upon C without thinking too much about the design space. Then they built a narrative around the reason for the (lack of) features of the language.
That said, I still think that if somebody is fine with Go, it's fine for them to use it. But "Just use Go"? No, thanks :)
To be clear, I agree with most of what you’re saying! Good type systems are immensely useful.
“Advanced” may have been poor word choice, but I do think Go’s designers saw most modern type system features as “advanced” relative to what they were asking for from their teams.
Then they built a narrative around the reason for the (lack of) features of the language.
This is the bit I disagree with. I do in fact think many of the choices in Go’s design were very intentional. I just think the language designers had very different values than you (or I!) would have.
FWIW, my interpretation of Go’s position in the design space is that it chooses to prioritize simplicity for new developers working in large codebases and large organizations over just about anything else. Because of this, it’s often easy for inexperienced developers to read and make targeted changes to code without needing to build a lot of context.
But it fails spectacularly at this too, thanks to the overuse of structural typing and the directory-based module system that combine to make it damn-near impossible to navigate pretty much any Go codebase.
Have to use find implementations from the lsp.
Which can still only guess, because there is no canonical way to tell whether something is an intended implementation or not. But yeah, it helps.
Assuming that you already have the source code checked out.
And that you have gopls set up already.
And that you trust the codebase enough to actually run LSP servers on it.
And that gopls actually decides to work that day.
(Admittedly, the typical gopls experience might have become less awful in the last few years? I don't know if I'm that keen to find out.)
my interpretation of Go’s position in the design space is that it chooses to prioritize simplicity for new developers working in large codebases and large organizations over just about anything else. Because of this, it’s often easy for inexperienced developers to read and make targeted changes to code without needing to build a lot of context.
I've found this is also an assist for AI agentic coding, where smaller-context models can similarly make targeted changes to code.
Dart is another Google language that doesn't seem to ignore decades of research yet nobody would use it outside of Flutter. Go is fine
I wouldn't have written this comment if the post was not condescending in the first place.
Yeah, this post is copying a meme format which is supposed to be aggressive and condescending. I dislike it because I know it's going to activate people and I think the points the post makes deserve a real conversation rather than a flame war (which is not to imply that I think you're engaging in flame war--only that this format is more likely to yield one).
it seems to be designed with the explicit purpose of ignoring decades of programming language design research
The developers focused on getting the fundamentals right, because every language thus far had ignored the fundamentals (as hand the PLT research community). People get so fixated on having the most comprehensive type system, but the quality returns diminish with increasingly complex/expressive type systems and no amount of effort in type system is going to make up for:
Go lacks any feature whatsoever to model your data, model your invariants, and structure your code, that any modern language should have
I would like sum types, but "lacks any feature for modeling data" is patently untrue. You can model data and invariants in any language, and Go gives you quite a lot in the way of a type system for enforcing said model.
Surely Rust has a steeper learning curve but it does a better job in all those things.
Rust is great, and if you don't care about iteration velocity or you're deploying to bare metal or you have really intense correctness or performance requirements then it's a great choice. But it's not a good default for general purpose application development, especially in a team context. But yeah, you do type if err != nil a lot, but I don't think anyone is bottlenecking on keystrokes/second.
that any modern language should have
No modern language other than Rust has these features (unless you want to be programming in Gleam/Swift but those are so niche we might as well be writing Haskell at that point).
if err != nil is the feature, not the bug. It forces you to look at every place something can go wrong and decide what to do about it.
Except it doesn't force you! It 's easier to ignore the error if you don't manually check it!
Rust remains the shining example of how to handle or cascade errors.
Yeah, exactly. Without something like errcheck, errors are trivial to ignore - which is just foolish. You should at least be required to explicitly discard errors.
Fortunately, every Go project I've worked on in the last few years has used golangci-lint to add additional static checks on top of Go's meager built-in offering. It should be required for all Go projects, frankly.
I prefer swift for this, but that's functionally the same model, the swift one just makes a bunch of "propagating errors from different libraries" easier, because the trade offs and design choices are different (not better or worse, that's why they're trade offs)
i despise this writing style/trend. but i do agree with the point that the post is trying to make.
No node_modules the size of a fucking Volkswagen
sure, its just a global package cache in ~/go, not a project local node_modules.
And it’s polluting my home directory. Not even a leading dot. How that is tolerated, I don’t know.
Leading dots are frowned-upon in the rob-pike-plan-9-o-sphere, due to the fact that the 'hiddenness' of filenames starting with a dot was an unanticipated side effect of the behavior of ls with regard to . and ... Here is Rob Pike's recollection of the process
I do wish they used the XDG specification and put stuff in $XDG_CACHE_HOME or some such, but bloated and unenforceable specifications are also frowned upon in the plan-nine-o-sphere.
I do wish they used the XDG specification and put stuff in $XDG_CACHE_HOME or some such, but bloated and unenforceable specifications are also frowned upon in the plan-nine-o-sphere.
The Go standard library actually respects XDG environment variables in functions like os.UserCacheDir. Go's build cache is stored there, too.
GOPATH originally used be not a cache directory, which is where this comes from.
I am relieved to know that I'm not the only one who finds this irritating.
This motivates a very simple ad hoc use of bwrap: bwrap --dev-bind / / --bind … ~/go go… (or --tmpfs ~ or similar.) This is so convenient of a trick that I use it in cases when I just couldn't be bothered how to learn how create a proper virtual environment (e.g., I once got irritated when env GEM_PATH=… gem … didn't do what I thought it should.)
As a lower post suggests, this choice seems to stem from a refusal to adhere to certain standards that we take for granted. And who am I to really argue with Rob Pike? I certainly wasn't around when they decided to make a leading-dot mean hidden or when XDG published the first base directory specification.
But it, and some other choices in Go, keep leading me in the direction that… the language and ecosystem just aren't good. Or aren't good enough… in ways that seem unnecessarily defiant of the standards, affordances, conveniences, and practices we have come to expect.
The Go flag module is just really bad: even worse than Python's argparse or Zsh's zparseopts. I have theoretical and philosophical disagreements with the latter two but find them mostly tolerable. In the case of Go flag, it seems to lead to software that makes obvious, avoidable mistakes. (Though Docker's CLI uses pflag instead of the standard-library flag, the mistake seems inherent to the design of flag. It is not, for example, a mistake that is made in bwrap!)
In the case of Go flag, it seems to lead to software that makes obvious, avoidable mistakes.
By the way, I don't think I'm being that unfair here. Despite mistakes like this existing in tools like qemu as well, the mistakes seem much shallower: the functionality here is very recent, and the fix seems easy (strchr should be strrchr?)
sure, its just a global package cache in ~/go, not a project local node_modules.
I always want to tell people to do a wc -l go.sum before they complain about the dependency tree sizes from other language ecosystems.
This post runs afoul of the same problem that plague most post that exalt a programming language - it's focused less on how great the current language is and more on how terrible the author's previous language was. The author clearly dealt with some serious pain points in Ruby and Typescript (and maybe Python?) that were solved by Go. However, I found the article underwhelming because I also don't use Ruby or Typescript.
I just feel that I've read a dozen variation of this over the years. Just use Haskell, because it has static types, unlike Python and Javascript. Just use Rust, because you can just deploy a single binary, unlike Perl and Erlang. Just use Elixir, because it has proper concurrency and channels, unlike Ruby and Tcl.
I'm happy for the author that they've found a language that works for them, but I'm will not be taking their advice.
There are a number of posters here who seem to think that their job is to sell Go to lobsters readers. It may have the opposite effect on some of us.
I’ve always felt the zero values in Go were a wart. I would rather have required the user be forced to specify the default value. Besides that the language is pretty good given it’s not OCaml.
I love zero values and I think they’re very clever. But I really miss the ability to set default values. It’s very difficult for example to marshal a json object where a missing bool should take the value of “true”.
The deployment and compiling story is great, but I really dislike writing the language itself. Everytime I've tried writing it it becomes a bad experience. Is there any other language with a great deployment story that isn't as handicapped as Go?
Am I missing out on Go?
I certainly appreciate it after trying to deploy a small rails application recently, it needs so much setup
Is there any other language with a great deployment story
I recently started compiling my Rust projects for x86_64-unknown-linux-musl. This way, I get a static binary that just runs on every 64-bit Linux machine. I then scp them over and run them.
This still leaves the problem of having to assign a port and having to start it manually but I plan to solve both with a bit of systemd magic.
I'm curious about your systemd magic - is there a way that you can pick an unused port in systemd to assign to something you run? Or are you just talking about giving it a port argument in the systemd invocation?
I've been pondering deployment strategies for my homelab and the closest thing I've come up with is running Podman (or Docker, I think) with the -P flag (where it picks a random host port to map to the exposed container port). But then I'm not sure what a nice way would be to get nginx to pick up that port for a reverse proxy.
traefik can do this realtime by looking at container metadata (docker labels) and automagically provisioning a reverse proxy, host name, tls certs, etc.
Traefik is indeed the easiest way if you're running standard Docker, but in some contexts like rootless podman that won't work unless they're running under the same unprivileged user.
Alternatively, with Podman (rootless/rootful), you could run podman events --filter event=health_status after running the container. Once the container is reported healthy, you can grab the container's dynamic port using podman inspect and then change/reload the NGINX config. The only requirement is that you configure health checks on the container, e.g --health-cmd='curl -f http://localhost:8000/ || exit 1'
This motivated me to do a write up on this, will report back if I do :)
Edit: typo
This way, I get a static binary that just runs on every 64-bit Linux machine.
This doesn't work when you're working on a Mac , does it?
In terms of a great deployment story, we've had surprising success with the nix bundler at work. To put things in perspective, we're producing a Qt6 GUI app. With the bundler, I get a single executable file that I can drop onto another Linux machine with a different distro and that does not have Qt installed and the user just runs the executable for the full GUI.
Okay, I do have to caveat that it has trouble with OpenGL drivers. You can still do it, but it does become more complicated than "copy and run".
My biggest issue with go is it claims to be designed for concurrency but then has raw pointers you can trivially share by accident built in.
I don't mind boring, but I think Go does an exceptionally bad job at actually… being boring. For a few cases from the top…
There are no decorators.
But you do have struct tags and reflection. Good luck figuring out how those interact without running it...
interfaces
Structural interfaces and reflection are a terrifying source of action at a distance. Suddenly adding the wrong method to your struct can make libraries change behaviour completely.
It's also weird from a documentation perspective; why wouldn't you want to make it clear what interfaces your types are meant to comply with?
goroutines
Why can't we just call threads what they are?
channels
Why are channels a language feature? (…because you were a decade late to admit generics are useful for more than ~three types.)
Why can't we just call threads what they are?
goroutines aren't threads, they're a lighter abstraction running over a pool of threads. That's what makes it easy to spawn thousands of goroutines.
channels
I assume channels are part of the runtime because the goroutine scheduler wants to know about them, so it can more easily wake up a receiving goroutine when the channel becomes non empty. It was probably just easier this way?
Channels also have dedicated syntax for select, and special type syntax for sender and receiver ends. The nice syntax would not be possible unless the language had macros or syntax overloading (both present in C++, but the original Go authors all seemed to strongly dislike C++).
Also, Alef, the language for Plan 9, had native channels with very similar syntax as Go.
goroutines aren't threads, they're a lighter abstraction running over a pool of threads. That's what makes it easy to spawn thousands of goroutines.
As an API.. yes they are. They happen to (when using the golang/go runtime) not correspond 1:1 to OS threads, but that's just an implementation detail.
I assume channels are part of the runtime because the goroutine scheduler wants to know about them, so it can more easily wake up a receiving goroutine when the channel becomes non empty.
Which is why I said language feature, not runtime.
Why can't we just call threads what they are?
Because goroutines are green threads with a lot of extra tooling attached.
with a lot of extra tooling attached
What tooling are you thinking of ? To me, goroutines seem pretty unremarkable compared to the equivalent in other languages ? Fairly efficient, but minimal features. Not supporting some kind of Erlang-style monitoring is one of Go's biggest missed opportunity.
That style of monitoring is only useful in combination with the strict process isolation that erlang has.
I largely agree with the sentiment: pick boring, simple, dumb technology over flashy, convoluted, clever technology.
However, Go is a bit too simple for many classes of web application. It's great if you want to slap a REST API in front of a Postgres DB, but it doesn't quite give you all the tools you need to build full-featured server-rendered apps.
I personally reach for Django when I need to build apps that are mostly HTML pages + forms, but Rails and Laravel are pretty good too. Django and Rails have been around for more than 20 years, Laravel for 14 years. They're known quantities. I can't speak for all of them, but Django's API surface doesn't change all that much between releases. Most of what I learned about the framework when I was 18 is still relevant today when I'm 35.
Out of the box, Django gives you: a solid ORM with the ability to write raw SQL if you want, database migrations, authentication and authorization, forms, a powerful templating language, a storage API to hook up S3 or equivalent, an admin that lets you visually edit database objects, a large number of security features you will definitely get wrong, internationalization, background tasks, sessions, sitemaps, RSS/Atom feeds, and a lot more I'm probably forgetting.
You can build all of these features with Go, but you'd be wasting your time and you'd probably get it wrong. The Django (or Rails, or Laravel) developers have already done this boring, mind numbing work for you so you can spend most of your time on the fun parts of building webapps!
That's not to say I dislike Go. Go is pretty great! I'm not a Go developer, but it's always a green flag when I work with clients who build their APIs in Go. It means I can understand and contribute to their codebase without having to learn some massive new framework. But Go is not a solution to all web programming problems.
I personally reach for Django
that's a hot take showing up to a thread about Go and saying "but you should use python, ruby, or php!"
Because, sure, if Django or Rails or Laravel were applications, you'd be perhaps justified in your high-brow take on no one else being able to write secure, featureful, performant code but the very idea that one starts with this castle in the sky and then, by adding more code to it, it remains a castle in the sky is some :-/
Say what you like about Go but mention ORMs and you lose me completely.
ORMs solve a (supposed) problem by layering many more problems on top of the original “problem” and then pretending things are better. UGH.
That's what people say who've never used the Django ORM which truly is one of the best in existence.
I have never used Django ORM and honestly after 20 years of ORMs I am never using one again. You guys go nuts tho.
also: because go has one canonical format, LLMs are extremely good at golang codegen.
go also takes a lot of care to never break backwards compat. (there is one tiny exception)
i love go because it fucking ships, and then gets out of your way - for months, for years. i have go programs i wrote 4 years ago that still run, untouched, and i am 100% confident that i could jump in, make a change, compile it, and deploy it in 5 minutes.
very few languages can claim that.
Indeed, it has been so nice using Go for small tools and programs to solve problems that surpass a bash script in complexity. They just work, I can move the binary around if I need to, they're all consistent in their formatting, and it's easy to come back to and modify as needed without churn.
For a guy who doesn't use Go a whole lot (me) and who thinks the novelty of this kind of crude blog post is over-saturated, I completely agree with where the author is coming from.
Heh, I do use Go a whole lot, and while I broadly agree with the “keep it simple / boring”, I do worry that this style of blog post will come across as antagonistic and not result in good discussion. I hope I’m wrong.
I do think Go is the Postgres of programming languages. It’s the boring default. I’ve been using it since 1.0 back in 2012 specifically because the tooling was so much better than anything else that existed—building an application didn’t involve learning an entirely new meta build system with its own stringly typed programming language. Pushing docs to a website (with automatic type documentation and links to third party docs!) was free.
All these things that Go’s critics spent a decade melting down about (e.g., hosting source code on a VCS, using a Plan9-inspired toolchain, etc) turned out to be just fine. Go also forced other languages to adapt (e.g., choosing a standard formatter). Go’s goroutine runtime remains one of the best approaches to asynchronicity on the market.
I hated Go when I first started using it in 2019 or so, but it really grew on me. I don't think I LIKE this post, but that doesn't mean I don't agree with it.
my favorite stack right now for web dev is:
go, sqlite, htmx, picocss
Have you considered adding web components to the mix? I have a very similar stack, except Picocss, and the limited scope of web components is just great.
No. Thankfully they've been trivial enough that I haven't seen the need. But yes, this is exactly the type of standards-based solution I'd reach for if it came down to that.
forty requests a second
Per core? (just joking)
context.Context
Is a monad! (kidding)
simple language … concurrency that doesn't make you cry: … switch <- for range
Can I get my async pills back? (almost serious)
if err != nil is the feature, not the bug
Please, stop here. (no, for reals)
Just use Go
I wish I could, but I have to unsee the last ~20 years of language development (totally honest).
Otherwise legit goat 👍
my biggest gripe with this logic is that languages don't start boring, they become boring. in 2009 Go was an exotic language. Java in 1996 was an exotic language. the boring languages of 2040 are being designed right now and they are all not Go.
I don't think it's true.
Go was that boring language, by deliberately ignoring any kind of PL design. The only remotely interesting part of Go are go routines, that's it.
Are go routines even that interesting? Erlang had green threads for what, two decades before Go was released?
Go really nails the problems they set out to solve. I wish I knew another language that tried to solve a very similar set of problems plus sum types, declarative style, and a bit higher level concurrency. But most languages that do those things don't also go for blinding compilation speed, a single binary result, and a rich, all-in-one standard library. (If you know one, I'd be excited to check it out)
What I'm reading here is an indictment of common programming language implementations.
"The language is boring in purpose." What I read: the language has a fairly small set of constructs, and the local program text has a meaning that can't be altered by distant code. I like this criterion, and I think that there's a space for a language that provides much more support to the programmer while still prioritizing this.
"The standard library is the framework." What I read: Go has a nontrivial standard library with a bunch of stuff in place for the uses its designers targeted (web backends). If you can manage to grow a high quality library API and a good reference implementation, I agree. I like a big, well constructed stdlib.
"half of Go's "magic" turns out to be the same two interfaces showing up everywhere" What I read: Alan Kay was right that late binding is super useful. Getting solid interfaces that you can write polymorphic functions against in place is super valuable.
"Concurrency that doesn't make you cry" What I read: Why are we still using runtimes that run on a single core with an event loop in this day of multicore machines? Why don't we have decent built-ins for concurrency? Goroutines plus channels feels a bit low level at this point, but it's a start.
"The tooling ships with the compiler" What I read: There's more to good language implementation than just a compiler. Yes, I think we all would agree. Formatters, linters, good error messages...all the things that we describe as making a language mature.
"Deployment is a copy command" What I read: I want to produce a static binary because it makes operations so much simpler. Or more generally, the complexity of operations is a real concern and the language implementers punting it on all the users of the language is pretty terrible.
"It forces you to look at every place something can go wrong and decide what to do about it." What I read: see above about behavior of code being entirely represented by the local program text, not distant code. Again, a desirable property.
This is all a pretty reasonable set of requests for an industrial language.
You can read the spec[ification for Go] on a lunch break and be productive that afternoon.
The Go specification takes 112 presses of Page Down to scroll. Firefox's Reader View estimates the reading time at 210-267min. I don't take 4hr lunch breaks. Previously, on Lobsters, I claimed that Go has seventeen numeric types; can you name them from memory? (Was it seventeen or nineteen?) There are languages that are small enough to fit wholly in one's head but Go is not among them.
if err != nilis the feature, not the bug. It forces you to look at every place something can go wrong and decide what to do about it.
On point, this is so wrong that I think it must be a System 3 utterance (discussed previously, on Lobsters): the author was not thinking when they wrote it. The ability to use nil to represent error values combined with non-exhaustive case analysis ensures that the typical Go codebase has at least one unhandled error path. Worse, there is no syntactic indication that the error-handling clause is missing. Worst, recover() is builtin, so we cannot even trust Somebody Else's Code when it indicates a non-error status; panicking and recovering is basically the same machinery as exception handling but without the ergonomics or community blessing.
I claimed that Go has seventeen numeric types; can you name them from memory? (Was it seventeen or nineteen?)
Looking at the spec you linked to, I count 14 (uint, uint8, uint16, uint32, uint64, int, int8, int16, int32, int64, float32, float64, complex32, complex64) - what am I missing? Unless you're including byte (alias), rune (alias), and uintptr? But that feels cheap since they're not specifically numeric types, are they?
(I don't think that 14 is particularly dispositive either since you can easily get away with only using a handful in daily usage.)
On the err != nil thing - I can only assume the author has always had errcheck + staticcheck required by their build system, and hasn't noticed that Go simply does not require that you check errors.
Yes, I too love manual error checking, no simplified flow for that checking, and no language level semantics that cause failure to check to not build, and I keep seeing posts about thread safety issues in a language which is ostensibly designed specifically for multi threading.
The last point is just bizarre to me, but the (subjective issues) awful syntax, the bad error ergonomics(end subjective), and the lack of a language level enforcement of the error model is just stupid.
Why would I adopt a new language just to deal with the same garbage error handling I deal with in C?
I really appreciate "colourless async", "errors are part of the function signature" and "compiling to a statically linked binary", and Go is one of the very few "popular" languages that does all three.
Of TIOBE top 50, 7 support "colourless async": Java (since 21, see https://openjdk.org/jeps/444), Go, Ada, Haskell, OCaml (since 5), Zig, Erlang.
Of the 7, 5 compile to statically linked binaries (modulo libc): Go, Ada, Haskell, OCaml, Zig.
Of the 5, 2 support "errors are part of the function signature"[^errors-others]: Go, Zig.
[^errors-others]: Haskell/OCaml are debatable given sum types, but their standard libraries are full of exceptions.
Guess I need to pick up Zig :)
errors are part of the function signature
I wish. The vast majority of Go funcs only have the error interface in their signature. And many happily return both a result and an error (or neither) at the same time. That's better than error handling via unchecked exceptions, but not by much.
That's better than error handling via unchecked exceptions, but not by much.
Agreed, although I'm of a "beggars can't be choosers" mind here. There are checks notes 4 languages in TIOBE top 50 that consistently declare "errors possible here" in the signature: Go, Rust, Swift and Zig. OCaml/Haskell are again debatable.
Any intersection with that list is necessarily going to be small :(
Go async is colored, it's just colored by passing around a Context.
Depends on your definition of function colouring I guess.
I'm using the one from What Color Is Your Function?, by that definition Go seems pretty colourless to me, in that there is no special function annotation necessary (or available) for the sync/async distinction.
Not sure by which definition Go's IO functions are coloured via Context argument, as Context is not a required part of Go's IO. It wasn't even added to the standard library until Go 1.7, see https://github.com/golang/go/issues/14660 .
Zig's functions can be said to be coloured via the IO interface argument in some sense, but the distinction being made is whether IO is done or not, not how the IO is done.
Does a green- but still threaded language qualify for this? Go's runtime is not really async and has proven to choke way sooner than a true async runtime.
Not for a tag of war, I'm genuinely thinking this rn 🙂
You interact with asynchronous functions differently depending upon if you care about cancellation or not. It's not a keyword, it's a value, but the concept is the same.
incendiary title and tone aside, there is no language that actually does this:
Your "clever" coworker can't sneak a seventeen-layer abstraction into the codebase because the language won't let him.
Architecture Astronauts always find a way to build a Jenga-esque stack of abstractions, you can easily build your WidgetFactoryStrategy pattern out in go interfaces and closures if you put your mind to it.
I agree. Typescript is an absolute wart on the face of programming.
Subversion but as if subversion had won.
I'm not a go dev, although I have dabbled. I recently decided to build my own LGTM+ stack containers (alloy, grafana, loki, tempo...) and the claim that Go compiles quickly is laughable to me.
You know what compiles in two seconds, deploys as a single binary, and doesn't shit itself when a transitive dependency gets yanked from npm at 3am? Go.
Not true. My experience is that compilation takes minutes and I need to make sure I have exactly the right flags to make sure I don’t get a statically linked binary.
if err != nil is the feature, not the bug
yeah ok, over my dead body then. I'm going to keep my Haskell stack.
I've hated Go with a passion since it's early versions, but also I think the article makes a good point that a lot of software—particularly web apps—doesn't need to be anywhere near as complex as it often is. That said, I think you could write pretty much the same article with almost any language or web framework, and the very few bits that are truly unique to Go (single static binaries and... that might be it) aren't a particularly good argument to use Go over a language you like more.
I like Go.
I hate go mod. Hate, hate, hate. It never works as I expect. I'm not someone that gets excited with new versions of something. I would much rather use something that is four versions old, because of course things break all the time. go mod was one of those things that I couldn't avoid.
I was quite happy with the pre-mod situation. However, doing goproxy=direct before commands helps a lot. Here's our go mod tutorial that we use at work and put at the top of big go.mod files. It might be out of date, because again I like to use old, stable things. Just my personal experience:
// FUUUUCK GO MOD. - nrdxp
// Go Mod and Go Work Tutorial
// QUICK FIX: goproxy=direct
// Go caches out of date dependencies all the time. Direct will bypass the cache.
//
// Go work/go mod/go has a bug: https://github.com/golang/go/issues/54264
// - **Cannot use `go work` as documented. Must use `go mod`**
// - Cannot use `go work` as documented with `replace`. Until a bug fix, use
// replace in `go mod`.
//
// Here are the five (!) documents that need to be
// thoroughly understood before using `go mod` or `go work`. Remember to use
// `replace`, the most essential part of either.
//
// Go Mod
// - [Go Modules Reference](https://go.dev/ref/mod#go-mod-file-replace)
// - [Managing dependencies](https://go.dev/doc/modules/managing-dependencies#local_directory)
// - [Module release and versioning workflow](https://go.dev/doc/modules/release-workflow#unpublished)
// Go Work
// - [Get familiar with workspaces](https://go.dev/blog/get-familiar-with-workspaces)
// - [Tutorial: Getting started with multi-module workspaces](https://go.dev/doc/tutorial/workspaces)
//
// # Go Workspace
// To set up workspaces, do the following in $GOPATH, which adds everything:
// `go.work` belongs here: `$GOPATH/go.work` `go work init` makes the file
// `go.work` with the single line, `go 1.19`, so don't do this. Just make it
// manually.
// `go work use -r src` will add all modules currently in `src`
// `go env GOWORK` will be populated if go is in workspace mode.
// ```
// cd $GOPATH
// go work init
// go work use -r src
// go env GOWORK
// ```
//
// For new projects, they will need to be added to work, especially for the
// error `no required module provides package <package>; to add it: go get
// <package>` which is wrong and misleading.
// ```
// go work use .
// ```
//
// (The `!` character is the escape character for upper case directories)
//
// # How to update modules:
// `go get -u && go mod tidy`
//
//
// Go install has crazy bugs with `go work`. See https://github.com/golang/go/issues/44840
// go build -o proposal: https://github.com/golang/go/issues/44469
With LLMs to deal with the sawtooth nature of writing Rust syntax and deal with the more elaborate type and lifetime information (something LLMs excel at) I'd say just use rust. Go has a small advantage for CLI utilities because of the stdlib coverage but the gap is small. Go completely collapses if you need to use native anything, and being able to either opt in to work stealing (async Tokio) and have extensive control over it means a lot in a production system. There is simply too much magic in Go's concepts of coroutines, GC, scheduling, network and file I/O, despite it appearing simple to the programmer.
I don't have much of a dog in this fight but:
Goroutines are not threads.... You can spawn a hundred thousand of them on a laptop.
Fact check: true
Channels are typed pipes between goroutines.
Fact check: in every Go project I've worked on, every single use of a goroutine+channel to move work off the main thread shipped with at least one goroutine leak we had to fix later.
Also: goroutines scale up to hundreds of thousands. Channels do not. At least they didn't a few years ago. You will use them for timeouts or the occasional "poll for this goroutines result and do something else while we wait" pattern. (See the previous point about goroutine leaks.)
If you need shared state instead, sync.Mutex is right there, and the race detector will tell you when you screwed up.
You are going to try to protect access to data with channels. You will discover they have abysmal throughput. You will need to use all the same old synchronization primitives everywhere. The race detector will help you but will obviously not catch everything.
My experience with Go is very mixed. I do like the fact it is boring. Building a rather small service or even a CLI is really fun. That being said I would think twice before starting a company with growth potential using Go.
The main reason is that you find yourself writing a lot of infra code you just wouldnt with other languages. And for some weird reason people like the author here will make you feel really awful for grabbing gin or an ORM god forbid to be able to move faster and not get hit by the same mistakes 200 companies made before me.
It is my daily driver and I love it though.
Meanwhile I do not know much about Go, I am very much in agreement with this:
The boring choice is the right choice. It always was.
Sure, there's nothing wrong with playing around with languages and testing exciting ways of thinking things.
But for the "I need money for food" work, boring is and always was the best choice. Then again I'm bit odd and I genuinely enjoy testing out the most boring languages out there such as Go even for my hobby stuff: I can work on things instead of battling my language knowledge.
go mod vendor
is great, but rarely used by authors making tarball releases, which gives packagers a headache.
As I also observed in an HN thread about this page...
Apropos of nothing: it's interesting that a page pushing Go so emphatically is built0 using the Rust-based Zola rather than the Go-based Hugo.
Yeah, I don't like Hugo's design choices for structuring content, especially for a very basic site like mine.
I'll use Go when I find a compiler that doesn't spy on me.
According to https://go.dev/doc/telemetry, the Go compiler does not upload your data anywhere unless you opt into that. You are also able to disable collection of local-only data with go telemetry off.