I've been using Go more or less in every full-time job I've had since pre-1.0. It's simple for people on the team to pick up the basics, it generally chugs along (I'm rarely worried about updating to latest version of Go), it has most useful things built in, it compiles fast. Concurrency is tricky but if you spend some time with it, it's nice to express data flow in Go. The type system is most of the time very convenient, if sometimes a bit verbose. Just all-around a trusty tool in the belt.
But I can't help but agree with a lot of points in this article. Go was designed by some old-school folks that maybe stuck a bit too hard to their principles, losing sight of the practical conveniences. That said, it's a _feeling_ I have, and maybe Go would be much worse if it had solved all these quirks. To be fair, I see more leniency in fixing quirks in the last few years, like at some point I didn't think we'd ever see generics, or custom iterators, etc.
The points about RAM and portability seem mostly like personal grievances though. If it was better, that would be nice, of course. But the GC in Go is very unlikely to cause issues in most programs even at very large scale, and it's not that hard to debug. And Go runs on most platforms anyone could ever wish to ship their software on.
But yeah the whole error / nil situation still bothers me. I find myself wishing for Result[Ok, Err] and Optional[T] quite often.
Go was designed by some old-school folks that maybe stuck a bit too hard to their principles, losing sight of the practical conveniences.
I'd say that it's entirely the other way around: they stuck to the practical convenience of solving the problem that they had in front of them, quickly, instead of analyzing the problem from the first principles, and solving the problem correctly (or using a solution that was Not Invented Here).
Go's filesystem API is the perfect example. You need to open files? Great, we'll create
func Open(name string) (*File, error)
function, you can open files now, done. What if the file name is not valid UTF-8, though? Who cares, hasn't happen to me in the first 5 years I used Go.
While the general question about string encoding is fine, unfortunately in a general-purpose and cross-platform language, a file interface that enforces Unicode correctness is actively broken, in that there are files out in the world it will be unable to interact with. If your language is enforcing that, and it doesn't have a fallback to a bag of bytes, it is broken, you just haven't encountered it. Go is correct on this specific API. I'm not celebrating that fact here, nor do I expect the Go designers are either, but it's still correct.
Much more egregious is the fact that the API allows returning both an error and a valid file handle. That may be documented to not happen. But look at the Read method instead. It will return both errors and a length you need to handle at the same time.
If the filename is not valid UTF-8, Golang can still open the file without a problem, as long as your filesystem doesn't attempt to be clever. Linux ext4fs and Go both consider filenames to be binary strings except that they cannot contain NULs.
> they stuck to the practical convenience of solving the problem that they had in front of them, quickly, instead of analyzing the problem from the first principles, and solving the problem correctly (or using a solution that was Not Invented Here).
I've said this before, but much of Go's design looks like it's imitating the C++ style at Google. The comments where I see people saying they like something about Go it's often an idiom that showed up first in the C++ macros or tooling.
I used to check this before I left Google, and I'm sure it's becoming less true over time. But to me it looks like the idea of Go was basically "what if we created a Python-like compiled language that was easier to onboard than C++ but which still had our C++ ergonomics?"
> What if the file name is not valid UTF-8, though?
Then make it valid UTF-8. If you try to solve the long tail of issues in a commonly used function of the library its going to cause a lot of pain. This approach is better. If someone has a weird problem like file names with invalid characters, they can solve it themselves, even publish a package. Why complicate 100% of uses for solving 0.01% of issues?
I recently started writing Go for a new job, after 20 years of not touching a compiled language for something serious (I've done DevKitArm dev. as a hobby).
I know it's mostly a matter of tastes, but darn, it feels horrible. And there are no default parameter values, and the error hanling smells bad, and no real stack trace in production. And the "object orientation" syntax, adding some ugly reference to each function. And the pointers...
It took me back to my C/C++ days. Like programming with 25 year old technology from back when I was in university in 1999.
And then people are amazed for it to achieve compile times, compiled languages were already doing on PCs running at 10 MHz within the constraints of 640 KB (TB, TP, Modula-2, Clipper, QB).
If you want a nice modern compiled language, try Kotlin. It's not ideal, but it's very ergonomic and has very reasonable compile times (to JVM, I did not play with native compilation). People also praise Nim for being nice towards the developer, but I don't have any first-hand experience with it.
But it's not--Go is a thoroughly modern language, minus a few things as noted in this discussion. But it's very and I've written quite a few APIs for corporate clients using it and they are doing great.
My feeling is that in terms of developer ergonomics, it nailed the “very opinionated, very standard, one way of doing things” part. It is a joy to work on a large microservices architecture and not have a different style on each repo, or avoiding formatting discussions because it is included.
The issue is that it was a bit outdated in the choice of _which_ things to choose as the one Go way. People expect a map/filter method rather than a loop with off by one risks, a type system with the smartness of typescript (if less featured and more heavily enforced), error handling is annoying, and so on.
I get that it’s tough to implement some of those features without opening the way to a lot of “creativity” in the bad sense. But I feel like go is sometimes a hard sell for this reason, for young devs whose mother language is JavaScript and not C.
> The issue is that it was a bit outdated in the choice of _which_ things to choose as the one Go way
I agree with this. I feel like Go was a very smart choice to create a new language to be easy and practical and have great tooling, and not to be experimental or super ambitious in any particular direction, only trusting established programming patterns. It's just weird that they missed some things that had been pretty well hashed out by 2009.
Map/filter/etc. are a perfect example. I remember around 2000 the average programmer thought map and filter were pointlessly weird and exotic. Why not use a for loop like a normal human? Ten years later the average programmer was like, for loops are hard to read and are perfect hiding places for bugs, I can't believe we used to use them even for simple things like map, filter, and foreach.
By 2010, even Java had decided that it needed to add its "stream API" and lambda functions, because no matter how awful they looked when bolted onto Java, it was still an improvement in clarity and simplicity.
Somehow Go missed this step forward the industry had taken and decided to double down on "for." Go's different flavors of for are a significant improvement over the C/C++/Java for loop, but I think it would have been more in line with the conservative, pragmatic philosophy of Go to adopt the proven solution that the industry was converging on.
Do they? After too many functional battles I started practicing what I'm jokingly calling "Debugging-Driven Development" and just like TDD keeps the design decisions in mind to allow for testability from the get-go, this makes me write code that will be trivially easy to debug (specially printf-guided debugging and step-by-step execution debugging)
Like, adding a printf in the middle of a for loop, without even needing to understand the logic of the loop. Just make a new line and write a printf. I grew tired of all those tight chains of code that iterate beautifully but later when in a hurry at 3am on a Sunday are hell to decompose and debug.
> Go was designed by some old-school folks that maybe stuck a bit too hard to their principles, losing sight of the practical conveniences.
It feels often like the two principles they stuck/stick to are "what makes writing the compiler easier" and "what makes compilation fast". And those are good goals, but they're only barely developer-oriented.
Not sure it was only that. I remember a lot of "we're not Java" in the discussions around it. I always had the feeling, they were rejecting certain ideas like exceptions and generics more out of principle, than any practical analysis.
Like, yes, those ideas have frequently been driven too far and have led to their own pain points. But people also seem to frequently rediscover that removing them entirety will lead to pain, too.
I am reminded when I read "barely developer oriented" that this comes from Google, who run compute and compilers at Ludicrous Scale. It doesn't seem strange that they might optimize (at least in part) for compiler speed and simplicity.
What makes compilation fast is a good goal at places with large code bases and build times. Maybe makes less sense in smaller startups with a few 100k LOC.
The go language and its runtime is the only system I know that is able to handle concurrency with multicore cpus seamlessly within the language, using the CSP-like (goroutine/channel) formalism which is easy to reason with.
Python is a mess with the gil and async libraries that are hard to reason with. C,C++,Java etc need external libraries to implement threading which cant be reasoned with in the context of the language itself.
So, go is a perfect fit for the http server (or service) usecase and in my experience there is no parallel.
> So, go is a perfect fit for the http server (or service) usecase and in my experience there is no parallel.
Elixir handling 2 million websocket connections on a single machine back in 2015 would like to have a word.[1] This is largely thanks to the Erlang runtime it sits atop.
Having written some tricky Go (I implemented Raft for a class) and a lot of Elixir (professional development), it is my experience that Go's concurrency model works for a few cases but largely sucks in others and is way easier to write footguns in Go than it ought to be.
> Java etc need external libraries to implement threading which cant be reasoned with in the context of the language itself.
What do you mean by this for Java? The library is the runtime that ships with Java, and while they're OS threads under the hood, the abstraction isn't all that leaky, and it doesn't feel like they're actually outside the JVM.
> using the CSP-like (goroutine/channel) formalism which is easy to reason with
I thought it was a seldom mentioned fact in Go that CSP systems are impossible to reason about outside of toy projects so everyone uses mutexes and such for systemic coordination.
I'm not sure I've even seen channels in a production application used for anything more than stopping a goroutine, collecting workgroup results, or something equally localized.
Unless we consider JDK as external library. Speaking of library, Java's concurrency containers are truly powerful yet can be safely used by so many engineers. I don't think Go's ecosystem is even close.
> Java ? Licensing sagas requiring the use of divergent forks. Plus Go is easier to work with, perhaps especially for server-side deployments
Yeah, these are sagas only, because there is basically one, single, completely free implementation anyone uses on the server-side and it's OpenJDK, which was made 100% open-source and the reference implementation by Oracle. Basically all of Corretto, AdoptOpenJDK, etc are just builds of the exact same repository.
People bringing this whole license topic up can't be taken seriously, it's like saying that Linux is proprietary because you can pay for support at Red Hat..
You forgot D. In a world where D exists, it's hard to understand why Go needed to be created. Every critique in this post is not an issue in D. If the effort Google put into Go had gone on making D better, I think D today would be the best language you could use. But as it is, D has had very little investment (by that I mean actual developer time spent on making it better, cleaning it up, writing tools) and it shows.
uv + the new way of adding the required packages in the comments is pretty good.
you can go `uv run script.py` and it'll automatically fetch the libraries and run the script in a virtual environment.
Still no match for Go though, shipping a single cross-compiled binary is a joy. And with a bit of trickery you can even bundle in your whole static website in it :) Works great when you're building business logic with a simple UI on top.
This just makes it even more frustrating to me. Everything good about go is more about the tooling and ecosystem but the language itself is not very good. I wish this effort had been put into a better language.
> Rust crates re-introduces [...] potential for supply-chain attacks.
I have absolutely no idea how go would solve this problem, and in fact I don't think it does at all.
> The Go std-lib is fantastic.
I have seen worse, but I would still not call it decent considering this is a fairly new language that could have done a lot more.
I am going to ignore the incredible amount of asinine and downright wrong stuff in many of the most popular libraries (even the basic ones maintained by google) since you are talking only about the stdlib.
On the top of my head I found inconsistent tagging management for structs (json defaults, omitzero vs omitempty), not even errors on tag typos, the reader/writer pattern that forces you to to write custom connectors between the two, bzip2 has a reader and no writer, the context linked list for K/V. Just look at the consistency of the interfaces in the "encoding" pkg and cry, the package `hash` should actually be `checksum`. Why does `strconv.Atoi`/ItoA still exist? Time.Add() vs Time.Sub()...
It chock full of inconsistencies. It forces me to look at the documentation every single time I don't use something for more than a couple of days. No, the autocomplete with the 2-line documentation does not include the potential pitfalls that are explained at the top of the package only.
And please don't get me started on the wrappers I had to write around stuff in the net library to make it a bit more consistent or just less plain wrong. net/url.Parse!!! I said don't make my start on this package! nil vs NoBody! ARGH!
None of this is stuff at the language level (of which there is plenty to say).
None of it is a dealbreaker per se, but it adds attrition and becomes death by a billion cuts.
I don't even trust any parser written in go anymore, I always try to come up with corner cases to check how it reacts, and I am often surprised by most of them.
Sure, there are worse languages and libraries. Still not something I would pick up in 2025 for a new project.
Yes, My favourite is the `time` package. It's just so elegant how it's just a number under there, the nominal type system truly shines. And using it is a treat.
What do you mean I can do `+= 8*time.Hour` :D
I get you can specifically write code that does not malloc, but I'm curious at scale if there are heap management / fragmentation and compression issues that are equivalent to GC pause issues.
I don't have a lot of experience with the malloc languages at scale, but I do know that heat fragmentation and GC fragmentation are very similar problems.
There are techniques in GC languages to avoid GC like arena allocation and stuff like that, generally considered non-idiomatic.
> But yeah the whole error / nil situation still bothers me. I find myself wishing for Result[Ok, Err] and Optional[T] quite often.
I got insta rejected in interview when i said this in response to interview panels question about 'thoughts about golang' .
Like they said, 'interview is over' and showed me the (virtual) door. I was stunned lol. This was during peak golang mania . Not sure what happened to rancherlabs .
They probably thought you weren't going to be a good fit for writing idiomatic Go. One of the things many people praise Go for is its standard style across codebases, if you don't like it, you're liable to try and write code that uses different patterns, which is painful for everyone involved.
This tends to be true for most languages, even the ones with easier concurrency support. Using it correctly is the tricky part.
I have no real problem with the portability. The area I see Go shining in is stuff like AWS Lambda where you want fast execution and aren't distributing the code to user systems.
I find Result[] and Optional[] somewhat overrated, but nil does bother me. However, nil isn't going to go away (what else is going to be the default value for pointers and interfaces, and not break existing code?). I think something like a non-nilable type annotation/declaration would be all Go needs.
Yeah maybe they're overrated, but they seem like the agreed-upon set of types to avoid null and to standardize error handling (with some support for nice sugars like Rust's ? operator).
I quite often see devs introducing them in other languages like TypeScript, but it just doesn't work as well when it's introduced in userland (usually you just end up with a small island of the codebase following this standard).
Yeah default values are one of Go's original sins, and it's far too late to roll those back. I don't think there are even many benefits—`int i;` is not meaningfully better than `int i = 0;`. If it's struct initialization they were worried about, well, just write a constructor.
Go has chosen explicit over implicit everywhere except initialization—the one place where I really needed "explicit."
The remarkable thing to me about Go is that it was created relatively recently, and the collective mindshare of our industry knew better about these sorts of issues. It would be like inventing a modern record player today with fancy new records that can't be damaged and last forever. Great... but why the fuck are we doing that? We should not be writing low level code like this with all of the boilerplate, verbosity, footguns. Build high level languages that perform like low level languages.
I shouldn't fault the creators. They did what they did, and that is all and good. I am more shocked by the way it has exploded in adoption.
> But I can't help but agree with a lot of points in this article. Go was designed by some old-school folks that maybe stuck a bit too hard to their principles, losing sight of the practical conveniences
I think this is a fine "fail-closed" way of language design. For example, Python has gone the other way and language complexity has gotten pretty bad since the small-language days. Trust what you are, don't try to please everyone, lest you become something like C++.
Golang is great for problem classes where you really, really can't do away with tracing GC. That's a rare case perhaps, but it exists nonetheless. Most GC languages don't have the kind of high-performance concurrent GC that you get out of the box with Golang, and the minimum RAM requirements are quite low as well. (You can of course provide more RAM to try and increase overall throughput, and you probably should - but you don't have to. That makes it a great fit for running on small cloud VM's, where RAM itself can be at a premium.)
Java's GCs are a generation ahead, though, in both throughput-oriented and latency-sensitive workloads [1]. Though Go's GC did/does get a few improvements and it is much better than it was a few years ago.
[1] ZGC has basically decoupled the heap size from the pause time, at that point you get longer pauses from the OS scheduler than from GC.
> I find myself wishing for Optional[T] quite often.
Well, so long as you don't care about compatibility with the broad ecosystem, you can write a perfectly fine Optional yourself:
type Optional[Value any] struct {
value Value
exists bool
}
// New empty.
func New[Value any]() Optional[Value] {}
// New of value.
func Of[Value any](value Value) Optional[Value] {}
// New of pointer.
func OfPointer[Value any](value *Value) Optional[Value] {}
// Only general way to get the value.
func (o Optional[Value]) Get() (Value, bool) {}
// Get value or panic.
func (o Optional[Value]) MustGet() Value {}
// Get value or default.
func (o Optional[Value]) GetOrElse(defaultValue Value) Value {}
// JSON support.
func (o Optional[Value]) MarshalJSON() ([]byte, error) {}
func (o *Optional[Value]) UnmarshalJSON(data []byte) error {}
// DB support.
func (o *Optional[Value]) Scan(value any) error {}
func (o Optional[Value]) Value() (driver.Value, error) {}
But you probably do care about compatibility with everyone else, so... yeah it really sucks that the Go way of dealing with optionality is slinging pointers around.
You can write `Optional`, sure, but you can't un-write `nil`, which is what I really want. I use `Optional<T>` in Java as much as I can, and it hasn't saved me from NullPointerException.
For JSON, you can't encode Optional[T] as nothing at all. It has to encode to something, which usually means null. But when you decode, the absence of the field means UnmarshalJSON doesn't get called at all. This typically results in the default value, which of course you would then re-encode as null. So if you round-trip your JSON, you get a materially different output than input (this matters for some other languages/libraries). Maybe the new encoding/json/v2 library fixes this, I haven't looked yet.
Also, I would usually want Optional[T]{value:nil,exists:true} to be impossible regardless of T. But Go's type system is too limited to express this restriction, or even to express a way for a function to enforce this restriction, without resorting to reflection, and reflection has a type erasure problem making it hard to get right even then! So you'd have to write a bunch of different constructors: one for all primitive types and strings; one each for pointers, maps, and slices; three for channels (chan T, <-chan T, chan<- T); and finally one for interfaces, which has to use reflection.
I still don't understand why defer works on function scope, and not lexical scope, and nobody has been able to explain to me the reason for it.
In fact this was so surprising to me is that I only found out about it when I wrote code that processed files in a loop, and it started crashing once the list of files got too big, because defer didnt close the handles until the function returned.
When I asked some other Go programmers, they told me to wrap the loop body in an anonymus func and invoke that.
Other than that (and some other niggles), I find Go a pleasant, compact language, with an efficient syntax, that kind of doesn't really encourage people trying to be cute. I started my Go journey rewriting a fairly substantial C# project, and was surprised to learn that despite it having like 10% of the features of C#, the code ended up being smaller. It also encourages performant defaults, like not forcing GC allocation at every turn, very good and built-in support for codegen for stuff like serialization, and no insistence to 'eat the world' like C# does with stuff like ORMs that showcase you can write C# instead of SQL for RDBMS and doing GRPC by annotating C# objects. In Go, you do SQL by writing SQL, and you od GRPC by writing protobuf specs.
So sometimes you want it lexical scope, and sometimes function scope; For example, maybe you open a bunch of files in a loop and need them all open for the rest of the function.
Right now it's function scope; if you need it lexical scope, you can wrap it in a function.
Suppose it were lexical scope and you needed it function scope. Then what do you do?
You do what the compiler has to do under the hood: at the top of the function create a list of open files, and have a defer statement that loops over the list closing all of the files. It's really not a complicated construct.
I think you hit the nail on the head - I think it's the stupid decision on Go lang designers part to make panic-s recover-able. This necessitates stack unwinding, meaning defer-s still need to run if a panic happens down the stack.
Since they didn't want to have a 'proper' RAII unwinding mechanism, this is the crappy compromise they came up with.
I’ve worked with languages that have both, and find myself wishing I could have function-level defer inside conditionals when I use the block-level languages.
Yes it does, function-scope defer needs a dynamic data structure to keep track of pending defers, so its not zero cost.
It can be also a source of bugs where you hang onto something for longer than intended - considering there's no indication of something that might block in Go, you can acquire a mutex, defer the release, and be surprised when some function call ends up blocking, and your whole program hangs for a second.
Having to wrap a loop body in a function that's immediately invoked seems like it would make the code harder to read. Especially for a language that prides itself on being "simple" and "straightforward".
I've worked almost exclusively on a large Golang project for over 5 years now and this definitely resonates with me. One component of that project is required to use as little memory as possible, and so much of my life has been spent hitting rough edges with Go on that front. We've hit so many issues where the garbage collector just doesn't clean things up quickly enough, or we get issues with heap fragmentation (because Go, in its infinite wisdom, decided not to have a compacting garbage collector) that we've had to try and avoid allocations entirely. Oh, and when we do have those issues, it's extremely difficult to debug. You can take heap profiles, but those only tell you about the live objects in the heap. They don't tell you about all of the garbage and all of the fragmentation. So diagnosing the issue becomes a matter of reading the tea leaves. For example, the heap profile says function X only allocated 1KB of memory, but it's called in a hot loop, so there's probably 20MB of garbage that this thing has generated that's invisible on the profile.
We pre-allocate a bunch of static buffers and re-use them. But that leads to a ton of ownership issues, like the append footgun mentioned in the article. We've even had to re-implement portions of the standard library because they allocate. And I get that we have a non-standard use case, and most programmers don't need to be this anal about memory usage. But we do, and it would be really nice to not feel like we're fighting the language.
I've found that when you need this it's easier to move stuff offheap, although obviously that's not entirely trivial in a GC language, and it certainly creates a lot of rough edges. If you find yourself writing what's essentially, e.g. C++ or Rust in Go, then you probably should just rewrite that part in the respective language when you can :)
> One component of that project is required to use as little memory as possible, and so much of my life has been spent hitting rough edges with Go on that front.
You made a poor choice of language for the problem. It'd be a good fit for C/C++/Rust/Zig.
I know this comment isn't terribly helpful, so I'm sorry, but it also sounds like Go is entirely the wrong language for this use case and you and your team were forced to use it for some corporate reason, like, the company only uses a subset of widely used programming languages in production.
I've heard the term "beaten path" used for these languages, or languages that an organization chooses to use and forbids the use of others.
No, Go isn’t actually that widely used at my company. The original developers chose Go because they thought it was a good fit for our use case. We were particularly looking for a compiled language that produces binaries with minimal dependencies, didn’t have manual memory management, and was relatively mature (I think Rust was barely 1.0 at the time). We knew we wanted to limit memory usage, but it was more of a “nice to have” than anything else. And Go worked pretty well. It was in production for a couple years before we started getting burnt by these issues. We are looking at porting this to Rust, but that’s a big lift. This is a 50K+ line code base that’s pretty battle tested.
Perhaps the new "Green Tea" GC will help? It's described as "a parallel marking algorithm that, if not memory-centric, is at least memory-aware, in that it endeavors to process objects close to one another together."
I saw that! I’m definitely interested in trying it out to see if it helps for our use case. Of course, at this point we’ve reduced allocations so much the GC doesn’t have a ton of work to do, unless we slip up somewhere (which has happened). I’ll probably have to intentionally add some allocations in a hot path as a stress test.
What I would absolutely love is a compacting garbage collector, but my understanding is Go can’t add that without breaking backwards compatibility, and so likely will never do that.
Go has its fair share of flaws but I still think it hits a sweet spot that no other server side language provides.
It’s faster than Node or Python, with a better type system than either. It’s got a much easier learning curve than Rust. It has a good stdlib and tooling. Simple syntax with usually only one way to do things. Error handling has its problems but I still prefer it over Node, where a catch clause might receive just about anything as an “error”.
Am I missing a language that does this too or more? I’m not a Go fanatic at all, mostly written Node for backends in my career, but I’ve been exploring Go lately.
> It’s faster than Node or Python, with a better type system than either. It’s got a much easier learning curve than Rust. It has a good stdlib and tooling. Simple syntax with usually only one way to do things. Error handling has its problems but I still prefer it over Node, where a catch clause might receive just about anything as an “error”.
I feel like I could write this same paragraph about Java or C#.
I mostly agree with you except the simple syntax with one way of doing things. If my memory serves me, Java supports at least 2 different paradigms for concurrency, for example, maybe more. I don’t know about C#. Correct me if wrong.
Maybe this is a bit pedantic, but it bothers me when people refer to "Node" as a programming language. It's not a language, it's a JavaScript runtime. Which to that you might say "well when people say Node they just mean JavaScript". But that's also probably not accurate, because a good chunk of modern Node-executed projects are written in TypeScript, not JavaScript. So saying "Node" doesn't actually say which programming language you mean. (Also, there are so many non-Node ways to execute JavaScript/TypeScript nowadays)
Anyway, assuming you're talking about TypeScript, I'm surprised to hear that you prefer Go's type system to TypeScript's. There are definitely cases where you can get carried away with TypeScript types, but due to that expressiveness I find it much more productive than Go's type system (and I'd make the same argument for Rust vs. Go).
My intent was just to emphasize that I’m comparing Go against writing JavaScript for the Node runtime and not in the browser, that is all, but you are correct.
Regarding Typescript, I actually am a big fan of it, and I almost never write vanilla JS anymore. I feel my team uses it well and work out the kinks with code review. My primary complaint, though, is that I cannot trust any other team to do the same, and TS supports escape hatches to bypass or lie about typing.
I work on a project with a codebase shared by several other teams. Just this week I have been frustrated numerous times by explicit type assertions of variables to something they are not (`foo as Bar`). In those cases it’s worse than vanilla JS because it misleads.
Yeah, but no one is using v8 directly, even though technically you could if you wanted. Node.js is as much JavaScript as LuaJIT is Lua, or GCC compiles C.
The real cream is that there barely any maintenance. The code I wrote 15years ago still works
That’s the selling point for me. If I’m coming to a legacy code as that no one working wrote, I pray it is go because then it just keeps working through upgrading the compiler and generally the libraries used.
Yeah the big problem is that most languages have their fair share of rough edges. Go is performant and portable* with a good runtime and a good ecosystem. But it also has nil pointers, zero values, no destructors, and no macros. (And before anyone says macros are bad, codegen is worse, and Go has to use a lot of codegen to get around the lack of macros).
There are languages with fewer warts, but they're usually more complicated (e.g. Rust), because most of Go's problems are caused by its creators' fixation with simplicity at all costs.
I thought it was obvious that codegen was better than macros—at least, textual macros. You can't tell me Ken Thompson omitted macros from the Golang design because he didn't have experience using languages with macro systems!
Even AST-based macro systems have tricky problems like nontermination and variable capture. It can be tough to debug why your compiler is stuck in an infinite macro expansion loop. Macro systems that solve these problems, like the R⁵RS syntax-rules system, have other drawbacks like very complex implementations and limited expressive power.
And often there's no easy way to look at the code after it's been through the macro processor, which makes bugs in the generated code introduced by buggy macros hard to track down.
By contrast, if your code generator hangs in an infinite loop, you can debug it the same way you normally debug your programs; it doesn't suffer from tricky bugs due to variable capture; and it's easy to look at its output.
I have a deep hatred of Go for all the things it doesn't have, including a usable type system (if I cannot write SomeClass<T where T extends HorsePlay> or similiar, the type system is not usable for me).
For NodeJS development, you would typically write it in Typescript - which has a very good type system.
Personally I have also written serverside C# code, which is a very nice experience these days. C# is a big language these days though.
It definitely hits a sweet spot. There is basically no other faster, widely used programming language in production used predominantly for web services than Go. You can argue Rust, but I just don't see it in job listings. And virtually no one is writing web services in C or C++ directly.
Yes, Python is massively ahead there. The largest wart is that types can be out of sync with actual implementation, with things blowing up at runtime -- but so can Go with `any` and reflection.
Python, for a number of years at this point, has had structural (!) pattern matching with unpacking, type-checking baked in, with exhaustiveness checking (depending on the type checker you use). And all that works at "type-check time".
It can also facilitate type-state programming through class methods.
Libraries like Pydantic are fantastic in their combination of ergonomics and type safety.
The prime missing piece is sum types, which need language-level support to work well.
Python with a library like Pydantic isn't bad—I wouldn't rate base Python as being near Go's level, at all, though you can get it up to something non-painful with libraries.
Go (and lots of other languages...) wreck it on dependency management and deployment, though. :-/ As the saying goes, "it was easier to invent Docker than fix Python's tooling".
I’ve only used Go for a little toy project but I’m surprised to hear the opinion that it has a better type system than Node, a runtime for which the defacto type system is typescript!
I worked briefly on extending an Go static site generator someone wrote for a client. The code was very clear and easy to read, but difficult to extend due to the many rough edges with the language. Simple changes required altering a lot of code in ways that were not immediately obvious. The ability to encapsulate and abstract is hindered in the name of “simplicity.” Abstraction is the primary way we achieve simple and easy to extend code. John Ousterhoust defined a complex program as one that is difficult to extend rather than necessarily being large or difficult to understand at scale. The average Go program seems to violate this principle a lot. Programs appear “simple” but extension proves difficult and fraught.
Go is a case of the emperor having no clothes. Telling people that they just don’t get it or that it’s a different way of doing things just doesn’t convince me. The only thing it has going for it is a simple dev experience.
I find the way people talk about Go super weird. If people have criticisms people almost always respond that the language is just "fine" and people kind of shame you for wanting it. People say Go is simpler but having to write a for loop to get the list of keys of a map is not simpler.
I agree with your point, but you'll have to update your example of something go can't do
> having to write a for loop to get the list of keys of a map
We now have the stdlib "maps" package, you can do:
keys := slices.Collect(maps.Keys(someMap))
With the wonder of generics, it's finally possible to implement that.
Now if only Go was consistent about methods vs functions, maybe then we could have "keys := someMap.Keys()" instead of it being a weird mix like `http.Request.Headers.Set("key", "value")` but `map["key"] = "value"`
Ooh! Or remember when a bunch of people acted like they had ascended to heaven for looking down on syntax-highlighting because Rob said something about it being a distraction? Or the swarms blasting me for insisting GOPATH was a nightmare that could only be born of Google's hubris (literally at the same time that `godep` was a thing and Kubernetes was spending significant efforts just fucking dealing with GOPATH.).
Happy to not be in that community, happy to not have to write (or read) Go these days.
And frankly, most of the time I see people gushing about Go, it's for features that trivially exist in most languages that aren't C, or are entirely subjective like "it's easy" (while ignoring, you know, reality).
So you used Go once, briefly, and yet you feel competent to pass this judgement so easily?
As someone who's been doing Go since 2015, working on dozens of large codebases counting probably a million lines total, across multiple teams, your criticisms do not ring true.
Go is no worse than C when it comes to extensibility, or C# or Java for that matter. Go programs are only extensible to the extent (ha) developers design their codebases right. Certainly, Go trades expressivity for explicitness more than some languages. You're encouraged to have fewer layers of abstraction and be more concrete and explicit. But in no way does that impede being able to extend code. The ability to write modular, extensible programs is a skill that must be learned, not something a programming language gives you for free.
It sounds like you worked on a poorly constructed codebase and assumed it was Go's fault.
It certainly isn’t impossible to write good code in Go. Perhaps the code base I was working on was bad — it didn’t seem obvious to me that it was. Go is not a bad language in the way that brainfuck is a bad language.
I think Java and C# offer clearly more straightforward ways to extend and modify existing code. Maybe the primary ways extension in Java and C# works are not quite the right ones for every situation.
The primary skill necessary to write modular code is first knowing what the modular interfaces is and second being able to implement it in a clean fashion. Go does offer a form of interfaces. But precisely because it encourages you to be highly explicit and avoid abstraction, it can make it difficult for you to implement the right abstraction and therefore complicate the modular interfaces.
Programming is hard. I don’t think adopting a kind of ascetic language like Go makes programming easier overall. Maybe it’s harder to be an architecture astronaut in Go, but only by eliminating entire classes of abstraction that are sometimes just necessary. Sometimes, inheritance is the right abstraction. Sometimes, you really need highly generic and polymorphic code (see some of the other comments for issues with Go’s implementation of generics).
I personally don't like Go, and it has many shortcomings, but there is a reason it is popular regardless:
Go is a reasonably performant language that makes it pretty straightforward to write reliable, highly concurrent services that don't rely on heavy multithreading - all thanks to the goroutine model.
There really was no other reasonably popular, static, compiled language around when Google came out.
And there still barely is - the only real competitor that sits in a similar space is Java with the new virtual threads.
Languages with async/await promise something similar, but in practice are burdened with a lot of complexity (avoiding blocking in async tasks, function colouring, ...)
I'm not counting Erlang here, because it is a very different type of language...
So I'd say Go is popular despite the myriad of shortcomings, thanks to goroutines and the Google project street cred.
Slowly but surely, the jvm has been closing the go gap. With efforts like virtual threads, zgc, lilliput, Leyden, and Valhalla, the jvm has been closing the gap.
The change from Java 8 to 25 is night and day. And the future looks bright. Java is slowly bringing in more language features that make it quite ergonomic to work with.
I'm still traumatised by Java from my earlier career. So many weird patterns, FactoryFactories and Spring Framework and ORMs that work 90% of the time and the 10% is pure pain.
I have no desire to go back to Java no matter how much the language has evolved.
For me C# has filled the void of Java in enterprise/gaming environments.
That may be true, but navigating 30 years of accumulated cruft, fragmented ecosystems and tooling, and ever-evolving syntax and conventions, is enough to drive anyone away. Personally, I never want to deal with classpath hell again, though this may have improved since I last touched Java ~15 years ago.
Go, with all its faults, tries very hard to shun complexity, which I've found over the years to be the most important quality a language can have. I don't want a language with many features. I want a language with the bare essentials that are robust and well designed, a certain degree of flexibility, and for it to get out of my way. Go does this better than any language I've ever used.
My criticism of the JVM is that it is no longer useful because we don't do portability using that mechanism anymore. We build applications that run in containers and can be compiled in the exact type of environment they are going to run inside of and we control all of that. The old days of Sun Microsystems and Java needing to run on Solaris, DEC, HP, maybe SGI, and later Linux, are LOOOOOOONG gone. And yet here we still are with portability inside our portability for ancient reasons.
Being able to create a self contained Kotlin app (JVM) that starts up quickly and uses the same amount of memory as the equivalent golang app would be amazing.
Well Google isn't really making a ton of new (successful) services these days, so the potential to introduce a new language is quite small unfortunately :). Plus, Go lacks one quite important thing which is ability to do an equivalent of HotSwap in the live service, which is really useful for debugging large complex applications without shutting them down.
Count Rust. From what I can see, it's becoming very popular in the microservices landscape. Not hard to imagine why. Multithreading is a breeze. Memory use is low. Latency is great.
The only silver bullet we know of is building on existing libraries. These are also non-accidentally the top 3 most popular languages according to any ranking worthy of consideration.
There are real pain points with async/await, but I find the criticism there often overblown. Most of the issues go away if you go pure async, mixing older sync code with async is much more difficult though.
My experience is mostly with C#, but async/await works very well there in my experience. You do need to know some basics there to avoid problem, but that's the case for essentially every kind of concurrency. They all have footguns.
Yep, most of what the author complains about are trivial issues you could find in any language. For contrast, some real, deep-rooted language design problems with Go are:
- Zero values, lack of support for constructors
- Poor handling of null
- Mutability by default
- A static type system not designed with generics in mind
- `int` is not arbitrary precision [1]
- The built-in array type (slices) has poorly considered ownership semantics [2]
There are other choices of languages, that are close to and influenced by Golang. Languages such as Vlang[1] (which addresses several issues mentioned) and maybe Odin[2]. Even more, they are at the stage where advance programmers can contribute or influence them in the ways that they might find satisfactory.
Golang is too far down the road and cemented in its ways, to expect such significant changes in direction. At this stage, people need to accept it for what it is or look elsewhere.
I used go for years, and while it's able to get small things up and running quickly, bigger projects soon become death-by-a-thousand-cuts.
Debugging is a nightmare because it refuses to even compile if you have unused X (which you always will have when you're debugging and testing "What happens if I comment out this bit?").
The bureaucracy is annoying. The magic filenames are annoying. The magic field names are annoying. The secret hidden panics in the standard library are annoying. The secret behind-your-back heap copies are annoying (and SLOW). All the magic in go eventually becomes annoying, because usually it's a naively repurposed thing (where they depend on something that was designed for a different purpose under different assumptions, but naively decided to depend on its side effects for their own ever-so-slightly-incompatible machinery - like special file names, and capitalization even though not all characters have such a thing .. was it REALLY such a chore to type "pub" for things you wanted exposed?).
Now that AI has gotten good, I'm rather enjoying Rust because I can just quickly ask the AI why my types don't match or a gnarly mutable borrow is happening - rather than spending hours poring over documentation and SO questions.
> Debugging is a nightmare because it refuses to even compile if you have unused X (which you always will have when you're debugging and testing "What happens if I comment out this bit?")
i made a cli tool[1] to mitigate this problem, it can be integrated into an ide quite easily, currently it has (neo)vim integration described in the readme and a vs code plugin-companion [2] which both can serve as an example and inspiration for creating an integration for your ide of choice
I haven't done serious Rust development since AI got good, but I did have a brief play last December and it's shocking how good they are at Rust. It feels like the verbose syntax and having tons of explicit information everywhere just makes it breeze through problems that would trip up a human for ages.
I once described this "debugging" problem to one of the creators and he did not even understand the problem. It is so amateurish you wonder if they ever dipped a toe outside the academic world.
Btw, AI sucks on GO. One would have guessed that such a simple lang would suit ChatGPT. Turns out ChatGPT is much better at Java, C#, Pyhton and many other langs than GO.
i had similar experience when i created a tool to solve this problem which i mentioned[1] earlier and posted[2] it to golang-nuts, people told me it’s horrible and you are fine doing what my tool does manually
But I can't help but agree with a lot of points in this article. Go was designed by some old-school folks that maybe stuck a bit too hard to their principles, losing sight of the practical conveniences. That said, it's a _feeling_ I have, and maybe Go would be much worse if it had solved all these quirks. To be fair, I see more leniency in fixing quirks in the last few years, like at some point I didn't think we'd ever see generics, or custom iterators, etc.
The points about RAM and portability seem mostly like personal grievances though. If it was better, that would be nice, of course. But the GC in Go is very unlikely to cause issues in most programs even at very large scale, and it's not that hard to debug. And Go runs on most platforms anyone could ever wish to ship their software on.
But yeah the whole error / nil situation still bothers me. I find myself wishing for Result[Ok, Err] and Optional[T] quite often.
I'd say that it's entirely the other way around: they stuck to the practical convenience of solving the problem that they had in front of them, quickly, instead of analyzing the problem from the first principles, and solving the problem correctly (or using a solution that was Not Invented Here).
Go's filesystem API is the perfect example. You need to open files? Great, we'll create
function, you can open files now, done. What if the file name is not valid UTF-8, though? Who cares, hasn't happen to me in the first 5 years I used Go.Nothing? Neither Go nor the OS require file names to be UTF-8, I believe
They could support passing filename as `string | []byte`. But wait, go does not even have union types.
This is one of the minor errors in the post.
I've said this before, but much of Go's design looks like it's imitating the C++ style at Google. The comments where I see people saying they like something about Go it's often an idiom that showed up first in the C++ macros or tooling.
I used to check this before I left Google, and I'm sure it's becoming less true over time. But to me it looks like the idea of Go was basically "what if we created a Python-like compiled language that was easier to onboard than C++ but which still had our C++ ergonomics?"
Deleted Comment
Then make it valid UTF-8. If you try to solve the long tail of issues in a commonly used function of the library its going to cause a lot of pain. This approach is better. If someone has a weird problem like file names with invalid characters, they can solve it themselves, even publish a package. Why complicate 100% of uses for solving 0.01% of issues?
I know it's mostly a matter of tastes, but darn, it feels horrible. And there are no default parameter values, and the error hanling smells bad, and no real stack trace in production. And the "object orientation" syntax, adding some ugly reference to each function. And the pointers...
It took me back to my C/C++ days. Like programming with 25 year old technology from back when I was in university in 1999.
The issue is that it was a bit outdated in the choice of _which_ things to choose as the one Go way. People expect a map/filter method rather than a loop with off by one risks, a type system with the smartness of typescript (if less featured and more heavily enforced), error handling is annoying, and so on.
I get that it’s tough to implement some of those features without opening the way to a lot of “creativity” in the bad sense. But I feel like go is sometimes a hard sell for this reason, for young devs whose mother language is JavaScript and not C.
I agree with this. I feel like Go was a very smart choice to create a new language to be easy and practical and have great tooling, and not to be experimental or super ambitious in any particular direction, only trusting established programming patterns. It's just weird that they missed some things that had been pretty well hashed out by 2009.
Map/filter/etc. are a perfect example. I remember around 2000 the average programmer thought map and filter were pointlessly weird and exotic. Why not use a for loop like a normal human? Ten years later the average programmer was like, for loops are hard to read and are perfect hiding places for bugs, I can't believe we used to use them even for simple things like map, filter, and foreach.
By 2010, even Java had decided that it needed to add its "stream API" and lambda functions, because no matter how awful they looked when bolted onto Java, it was still an improvement in clarity and simplicity.
Somehow Go missed this step forward the industry had taken and decided to double down on "for." Go's different flavors of for are a significant improvement over the C/C++/Java for loop, but I think it would have been more in line with the conservative, pragmatic philosophy of Go to adopt the proven solution that the industry was converging on.
Do they? After too many functional battles I started practicing what I'm jokingly calling "Debugging-Driven Development" and just like TDD keeps the design decisions in mind to allow for testability from the get-go, this makes me write code that will be trivially easy to debug (specially printf-guided debugging and step-by-step execution debugging)
Like, adding a printf in the middle of a for loop, without even needing to understand the logic of the loop. Just make a new line and write a printf. I grew tired of all those tight chains of code that iterate beautifully but later when in a hurry at 3am on a Sunday are hell to decompose and debug.
It feels often like the two principles they stuck/stick to are "what makes writing the compiler easier" and "what makes compilation fast". And those are good goals, but they're only barely developer-oriented.
Like, yes, those ideas have frequently been driven too far and have led to their own pain points. But people also seem to frequently rediscover that removing them entirety will lead to pain, too.
The go language and its runtime is the only system I know that is able to handle concurrency with multicore cpus seamlessly within the language, using the CSP-like (goroutine/channel) formalism which is easy to reason with.
Python is a mess with the gil and async libraries that are hard to reason with. C,C++,Java etc need external libraries to implement threading which cant be reasoned with in the context of the language itself.
So, go is a perfect fit for the http server (or service) usecase and in my experience there is no parallel.
Elixir handling 2 million websocket connections on a single machine back in 2015 would like to have a word.[1] This is largely thanks to the Erlang runtime it sits atop.
Having written some tricky Go (I implemented Raft for a class) and a lot of Elixir (professional development), it is my experience that Go's concurrency model works for a few cases but largely sucks in others and is way easier to write footguns in Go than it ought to be.
[1]: https://phoenixframework.org/blog/the-road-to-2-million-webs...
Java does not need external libraries to implement threading, it's baked into the language and its standard libraries.
What do you mean by this for Java? The library is the runtime that ships with Java, and while they're OS threads under the hood, the abstraction isn't all that leaky, and it doesn't feel like they're actually outside the JVM.
Working with them can be a bit clunky, though.
I believe it’s the only system you know. But it’s far from the only one.
I thought it was a seldom mentioned fact in Go that CSP systems are impossible to reason about outside of toy projects so everyone uses mutexes and such for systemic coordination.
I'm not sure I've even seen channels in a production application used for anything more than stopping a goroutine, collecting workgroup results, or something equally localized.
Ah, what????
I agree.
The Go std-lib is fantastic.
Also no dependency-hell with Go, unlike with Python. Just ship an oven-ready binary.
And what's the alternative ?
Java ? Licensing sagas requiring the use of divergent forks. Plus Go is easier to work with, perhaps especially for server-side deployments.
Zig ? Rust ? Complex learning curve. And having to choose e.g. Rust crates re-introduces dependency hell and the potential for supply-chain attacks.
Yeah, these are sagas only, because there is basically one, single, completely free implementation anyone uses on the server-side and it's OpenJDK, which was made 100% open-source and the reference implementation by Oracle. Basically all of Corretto, AdoptOpenJDK, etc are just builds of the exact same repository.
People bringing this whole license topic up can't be taken seriously, it's like saying that Linux is proprietary because you can pay for support at Red Hat..
I’m only a casual user of both but how are rust crates meaningfully different from go’s dependency management?
you can go `uv run script.py` and it'll automatically fetch the libraries and run the script in a virtual environment.
Still no match for Go though, shipping a single cross-compiled binary is a joy. And with a bit of trickery you can even bundle in your whole static website in it :) Works great when you're building business logic with a simple UI on top.
Deleted Comment
I have absolutely no idea how go would solve this problem, and in fact I don't think it does at all.
> The Go std-lib is fantastic.
I have seen worse, but I would still not call it decent considering this is a fairly new language that could have done a lot more.
I am going to ignore the incredible amount of asinine and downright wrong stuff in many of the most popular libraries (even the basic ones maintained by google) since you are talking only about the stdlib.
On the top of my head I found inconsistent tagging management for structs (json defaults, omitzero vs omitempty), not even errors on tag typos, the reader/writer pattern that forces you to to write custom connectors between the two, bzip2 has a reader and no writer, the context linked list for K/V. Just look at the consistency of the interfaces in the "encoding" pkg and cry, the package `hash` should actually be `checksum`. Why does `strconv.Atoi`/ItoA still exist? Time.Add() vs Time.Sub()...
It chock full of inconsistencies. It forces me to look at the documentation every single time I don't use something for more than a couple of days. No, the autocomplete with the 2-line documentation does not include the potential pitfalls that are explained at the top of the package only.
And please don't get me started on the wrappers I had to write around stuff in the net library to make it a bit more consistent or just less plain wrong. net/url.Parse!!! I said don't make my start on this package! nil vs NoBody! ARGH!
None of this is stuff at the language level (of which there is plenty to say).
None of it is a dealbreaker per se, but it adds attrition and becomes death by a billion cuts.
I don't even trust any parser written in go anymore, I always try to come up with corner cases to check how it reacts, and I am often surprised by most of them.
Sure, there are worse languages and libraries. Still not something I would pick up in 2025 for a new project.
Yes, My favourite is the `time` package. It's just so elegant how it's just a number under there, the nominal type system truly shines. And using it is a treat. What do you mean I can do `+= 8*time.Hour` :D
The code was on the hot path of their central routing server handling Billions (with a B) messages in a second or something crazy like that.
You're not building Discord, the GC will most likely never be even a blip in your metrics. The GC is just fine.
I don't have a lot of experience with the malloc languages at scale, but I do know that heat fragmentation and GC fragmentation are very similar problems.
There are techniques in GC languages to avoid GC like arena allocation and stuff like that, generally considered non-idiomatic.
I got insta rejected in interview when i said this in response to interview panels question about 'thoughts about golang' .
Like they said, 'interview is over' and showed me the (virtual) door. I was stunned lol. This was during peak golang mania . Not sure what happened to rancherlabs .
It’s part trying to keep a common direction and part fear that dislike of their tech risks the hire not staying for long.
I don’t agree with this approach, don’t get me wrong, but I’ve seen it done and it might explain your experience.
This tends to be true for most languages, even the ones with easier concurrency support. Using it correctly is the tricky part.
I have no real problem with the portability. The area I see Go shining in is stuff like AWS Lambda where you want fast execution and aren't distributing the code to user systems.
In what universe?
Is it the best or most robust or can you do fancy shit with it? No
But it works well enough to release reliable software along with the massive linter framework that's built on top of Go.
I quite often see devs introducing them in other languages like TypeScript, but it just doesn't work as well when it's introduced in userland (usually you just end up with a small island of the codebase following this standard).
Go has chosen explicit over implicit everywhere except initialization—the one place where I really needed "explicit."
I shouldn't fault the creators. They did what they did, and that is all and good. I am more shocked by the way it has exploded in adoption.
Would love to see a coffeescript for golang.
It's not viable to use, but: https://github.com/borgo-lang/borgo
I think this is a fine "fail-closed" way of language design. For example, Python has gone the other way and language complexity has gotten pretty bad since the small-language days. Trust what you are, don't try to please everyone, lest you become something like C++.
Clojure is good in this respect.
[1] ZGC has basically decoupled the heap size from the pause time, at that point you get longer pauses from the OS scheduler than from GC.
Well, so long as you don't care about compatibility with the broad ecosystem, you can write a perfectly fine Optional yourself:
But you probably do care about compatibility with everyone else, so... yeah it really sucks that the Go way of dealing with optionality is slinging pointers around.For JSON, you can't encode Optional[T] as nothing at all. It has to encode to something, which usually means null. But when you decode, the absence of the field means UnmarshalJSON doesn't get called at all. This typically results in the default value, which of course you would then re-encode as null. So if you round-trip your JSON, you get a materially different output than input (this matters for some other languages/libraries). Maybe the new encoding/json/v2 library fixes this, I haven't looked yet.
Also, I would usually want Optional[T]{value:nil,exists:true} to be impossible regardless of T. But Go's type system is too limited to express this restriction, or even to express a way for a function to enforce this restriction, without resorting to reflection, and reflection has a type erasure problem making it hard to get right even then! So you'd have to write a bunch of different constructors: one for all primitive types and strings; one each for pointers, maps, and slices; three for channels (chan T, <-chan T, chan<- T); and finally one for interfaces, which has to use reflection.
You hear that Rob Pike? LOL. All those years he shat on Java, it was so irritating. (Yes schadenfreude /g)
In fact this was so surprising to me is that I only found out about it when I wrote code that processed files in a loop, and it started crashing once the list of files got too big, because defer didnt close the handles until the function returned.
When I asked some other Go programmers, they told me to wrap the loop body in an anonymus func and invoke that.
Other than that (and some other niggles), I find Go a pleasant, compact language, with an efficient syntax, that kind of doesn't really encourage people trying to be cute. I started my Go journey rewriting a fairly substantial C# project, and was surprised to learn that despite it having like 10% of the features of C#, the code ended up being smaller. It also encourages performant defaults, like not forcing GC allocation at every turn, very good and built-in support for codegen for stuff like serialization, and no insistence to 'eat the world' like C# does with stuff like ORMs that showcase you can write C# instead of SQL for RDBMS and doing GRPC by annotating C# objects. In Go, you do SQL by writing SQL, and you od GRPC by writing protobuf specs.
Right now it's function scope; if you need it lexical scope, you can wrap it in a function.
Suppose it were lexical scope and you needed it function scope. Then what do you do?
You can just introduce a new scope wherever you want with {} in sane languages, to control the required behavior as you wish.
Defer a bulk thing at the function scope level, and append files to an array after opening them.
?
2. mechanic is tied to call stack / stack unwinding
3. it feels natural when you're coming from C with `goto fail`
(yes it annoys me when I want to defer in a loop & now that loop body needs to be a function)
Since they didn't want to have a 'proper' RAII unwinding mechanism, this is the crappy compromise they came up with.
It can be also a source of bugs where you hang onto something for longer than intended - considering there's no indication of something that might block in Go, you can acquire a mutex, defer the release, and be surprised when some function call ends up blocking, and your whole program hangs for a second.
We pre-allocate a bunch of static buffers and re-use them. But that leads to a ton of ownership issues, like the append footgun mentioned in the article. We've even had to re-implement portions of the standard library because they allocate. And I get that we have a non-standard use case, and most programmers don't need to be this anal about memory usage. But we do, and it would be really nice to not feel like we're fighting the language.
You made a poor choice of language for the problem. It'd be a good fit for C/C++/Rust/Zig.
I've heard the term "beaten path" used for these languages, or languages that an organization chooses to use and forbids the use of others.
https://github.com/golang/go/issues/73581
What I would absolutely love is a compacting garbage collector, but my understanding is Go can’t add that without breaking backwards compatibility, and so likely will never do that.
It’s faster than Node or Python, with a better type system than either. It’s got a much easier learning curve than Rust. It has a good stdlib and tooling. Simple syntax with usually only one way to do things. Error handling has its problems but I still prefer it over Node, where a catch clause might receive just about anything as an “error”.
Am I missing a language that does this too or more? I’m not a Go fanatic at all, mostly written Node for backends in my career, but I’ve been exploring Go lately.
I feel like I could write this same paragraph about Java or C#.
Anyway, assuming you're talking about TypeScript, I'm surprised to hear that you prefer Go's type system to TypeScript's. There are definitely cases where you can get carried away with TypeScript types, but due to that expressiveness I find it much more productive than Go's type system (and I'd make the same argument for Rust vs. Go).
Regarding Typescript, I actually am a big fan of it, and I almost never write vanilla JS anymore. I feel my team uses it well and work out the kinks with code review. My primary complaint, though, is that I cannot trust any other team to do the same, and TS supports escape hatches to bypass or lie about typing.
I work on a project with a codebase shared by several other teams. Just this week I have been frustrated numerous times by explicit type assertions of variables to something they are not (`foo as Bar`). In those cases it’s worse than vanilla JS because it misleads.
That’s the selling point for me. If I’m coming to a legacy code as that no one working wrote, I pray it is go because then it just keeps working through upgrading the compiler and generally the libraries used.
There are languages with fewer warts, but they're usually more complicated (e.g. Rust), because most of Go's problems are caused by its creators' fixation with simplicity at all costs.
Even AST-based macro systems have tricky problems like nontermination and variable capture. It can be tough to debug why your compiler is stuck in an infinite macro expansion loop. Macro systems that solve these problems, like the R⁵RS syntax-rules system, have other drawbacks like very complex implementations and limited expressive power.
And often there's no easy way to look at the code after it's been through the macro processor, which makes bugs in the generated code introduced by buggy macros hard to track down.
By contrast, if your code generator hangs in an infinite loop, you can debug it the same way you normally debug your programs; it doesn't suffer from tricky bugs due to variable capture; and it's easy to look at its output.
For NodeJS development, you would typically write it in Typescript - which has a very good type system.
Personally I have also written serverside C# code, which is a very nice experience these days. C# is a big language these days though.
Given Python's substantial improvements recently, I would put it far ahead of the structural typing done in Go, personally.
Python, for a number of years at this point, has had structural (!) pattern matching with unpacking, type-checking baked in, with exhaustiveness checking (depending on the type checker you use). And all that works at "type-check time".
It can also facilitate type-state programming through class methods.
Libraries like Pydantic are fantastic in their combination of ergonomics and type safety.
The prime missing piece is sum types, which need language-level support to work well.
Go is simplistic in comparison.
Go (and lots of other languages...) wreck it on dependency management and deployment, though. :-/ As the saying goes, "it was easier to invent Docker than fix Python's tooling".
Agree on node/TS error handling. It’s super whack
Go is a case of the emperor having no clothes. Telling people that they just don’t get it or that it’s a different way of doing things just doesn’t convince me. The only thing it has going for it is a simple dev experience.
> having to write a for loop to get the list of keys of a map
We now have the stdlib "maps" package, you can do:
With the wonder of generics, it's finally possible to implement that.Now if only Go was consistent about methods vs functions, maybe then we could have "keys := someMap.Keys()" instead of it being a weird mix like `http.Request.Headers.Set("key", "value")` but `map["key"] = "value"`
Or 'close(chan x)' but 'file.Close()', etc etc.
Happy to not be in that community, happy to not have to write (or read) Go these days.
And frankly, most of the time I see people gushing about Go, it's for features that trivially exist in most languages that aren't C, or are entirely subjective like "it's easy" (while ignoring, you know, reality).
As someone who's been doing Go since 2015, working on dozens of large codebases counting probably a million lines total, across multiple teams, your criticisms do not ring true.
Go is no worse than C when it comes to extensibility, or C# or Java for that matter. Go programs are only extensible to the extent (ha) developers design their codebases right. Certainly, Go trades expressivity for explicitness more than some languages. You're encouraged to have fewer layers of abstraction and be more concrete and explicit. But in no way does that impede being able to extend code. The ability to write modular, extensible programs is a skill that must be learned, not something a programming language gives you for free.
It sounds like you worked on a poorly constructed codebase and assumed it was Go's fault.
I think Java and C# offer clearly more straightforward ways to extend and modify existing code. Maybe the primary ways extension in Java and C# works are not quite the right ones for every situation.
The primary skill necessary to write modular code is first knowing what the modular interfaces is and second being able to implement it in a clean fashion. Go does offer a form of interfaces. But precisely because it encourages you to be highly explicit and avoid abstraction, it can make it difficult for you to implement the right abstraction and therefore complicate the modular interfaces.
Programming is hard. I don’t think adopting a kind of ascetic language like Go makes programming easier overall. Maybe it’s harder to be an architecture astronaut in Go, but only by eliminating entire classes of abstraction that are sometimes just necessary. Sometimes, inheritance is the right abstraction. Sometimes, you really need highly generic and polymorphic code (see some of the other comments for issues with Go’s implementation of generics).
Go is a reasonably performant language that makes it pretty straightforward to write reliable, highly concurrent services that don't rely on heavy multithreading - all thanks to the goroutine model.
There really was no other reasonably popular, static, compiled language around when Google came out.
And there still barely is - the only real competitor that sits in a similar space is Java with the new virtual threads.
Languages with async/await promise something similar, but in practice are burdened with a lot of complexity (avoiding blocking in async tasks, function colouring, ...)
I'm not counting Erlang here, because it is a very different type of language...
So I'd say Go is popular despite the myriad of shortcomings, thanks to goroutines and the Google project street cred.
The change from Java 8 to 25 is night and day. And the future looks bright. Java is slowly bringing in more language features that make it quite ergonomic to work with.
I have no desire to go back to Java no matter how much the language has evolved.
For me C# has filled the void of Java in enterprise/gaming environments.
Go, with all its faults, tries very hard to shun complexity, which I've found over the years to be the most important quality a language can have. I don't want a language with many features. I want a language with the bare essentials that are robust and well designed, a certain degree of flexibility, and for it to get out of my way. Go does this better than any language I've ever used.
(Similar to how Python is finally getting its act together with the uv tool.)
Java is great if you stick to a recent version and update on a regular basis. But a lot of companies hate their own developers.
Every single piece of Go 1.x code scraped from the internet and baked in to the models is still perfectly valid and compiles with the latest version.
Which Google uses far more commonly than Go, still to this day.
Most users writing basic async CRUD servers won't notice, but you very much do if you write complex , highly concurrent servers.
That can be a viable tradeoff, and is for many, but it's far from being as fool-proof as Go.
Deleted Comment
For ML/data: python
For backend/general purpose software: Java
The only silver bullet we know of is building on existing libraries. These are also non-accidentally the top 3 most popular languages according to any ranking worthy of consideration.
My experience is mostly with C#, but async/await works very well there in my experience. You do need to know some basics there to avoid problem, but that's the case for essentially every kind of concurrency. They all have footguns.
Usually, as here, objections to go take the form a technically-correct-but-ultimately-pedantic arguments.
The positives of go are so overwhelmingly high magnitude that all those small things basically don’t matter enough to abandon the language.
Go is good enough to justify using it now while waiting for the slow-but-steady stream of improvements from version to version to make life better.
- Zero values, lack of support for constructors
- Poor handling of null
- Mutability by default
- A static type system not designed with generics in mind
- `int` is not arbitrary precision [1]
- The built-in array type (slices) has poorly considered ownership semantics [2]
Notable mentions:
- No sum types
- No string interpolation
[1]: https://github.com/golang/go/issues/19623
[2]: https://news.ycombinator.com/item?id=39477821
Golang is too far down the road and cemented in its ways, to expect such significant changes in direction. At this stage, people need to accept it for what it is or look elsewhere.
[1]: https://vlang.io/
[2]: https://odin-lang.org/
Debugging is a nightmare because it refuses to even compile if you have unused X (which you always will have when you're debugging and testing "What happens if I comment out this bit?").
The bureaucracy is annoying. The magic filenames are annoying. The magic field names are annoying. The secret hidden panics in the standard library are annoying. The secret behind-your-back heap copies are annoying (and SLOW). All the magic in go eventually becomes annoying, because usually it's a naively repurposed thing (where they depend on something that was designed for a different purpose under different assumptions, but naively decided to depend on its side effects for their own ever-so-slightly-incompatible machinery - like special file names, and capitalization even though not all characters have such a thing .. was it REALLY such a chore to type "pub" for things you wanted exposed?).
Now that AI has gotten good, I'm rather enjoying Rust because I can just quickly ask the AI why my types don't match or a gnarly mutable borrow is happening - rather than spending hours poring over documentation and SO questions.
i made a cli tool[1] to mitigate this problem, it can be integrated into an ide quite easily, currently it has (neo)vim integration described in the readme and a vs code plugin-companion [2] which both can serve as an example and inspiration for creating an integration for your ide of choice
[1] https://github.com/vipkek/gouse
[2] https://marketplace.visualstudio.com/items?itemName=looshch....
edit: formatting
Btw, AI sucks on GO. One would have guessed that such a simple lang would suit ChatGPT. Turns out ChatGPT is much better at Java, C#, Pyhton and many other langs than GO.
I’d say Terraform was the worst. But that shouldn’t be a surprise given it’s niche
[1] https://news.ycombinator.com/item?id=44982491#44986946
[2] https://groups.google.com/g/golang-nuts/c/QL5h8zO7MDo/m/qiLi...
Go people will yell at you because you aren't using the right tool.
Yeah, Go is too rigid with their principles.