Go is still not good
(blog.habets.se)587 points by ustad a day ago
587 points by ustad a day ago
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?
Really? I find the opposite is true. If I need lexical scope then I’d just write, for example
f.Close() // without defer
The reason I might want function scope defer is because there might be a lot of different exit points from that function.With lexical scope, there’s only three ways to safely jump the scope:
1. reaching the end of the procedure, in which case you don’t need a defer)
2. A ‘return’, in which case you’re also exiting the function scope
3. a ‘break’ or ‘continue’, which admittedly could see the benefit of a lexical scope defer but they’re also generally trivial to break into their own functions; and arguably should be if your code is getting complex enough that you’ve got enough branches to want a defer.
If Go had other control flows like try/catch, and so on and so forth, then there would be a stronger case for lexical defer. But it’s not really a problem for anyone aside those who are also looking for other features that Go also doesn’t support.
You put the files in the collection as you open them, and you register the defer before opening any of them. It works fine. Defer should be lexically scoped.
1. it avoids a level of indentation until you wrap it in a function
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)
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.
As it is, you can have it both ways. Wrap the body in a function if that's what you want. Don't wrap to get wider scope.
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.
I think it's only a real issue when you're coming from a language that has different rules. Block-scoping (and thus not being able to e.g. conditionally remove a temp file at the end of a function) would be equally surprising for someone coming from Go.
But I do definitely agree that the dynamic nature of defer and it not being block-scoped is probably not the best
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 :)
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.
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.
> The original developers chose Go because they thought it was a good fit for our use case.
I don't completely get this. If you are memory requirements are strict, this makes little to no sense to me. I was programming J2ME games 20 years ago for Nokia devices. We were trying to fit games into 50-128kb RAM and all of this with Java of all the languages. No sane Java developer would have looked at that code without fainting - no dynamic allocations, everything was static, byte and char were the most common data types used. Images in the games were shaved, no headers, nothing. You really have to think it through if you got memory constraints on your target device.
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"`
Or 'close(chan x)' but 'file.Close()', etc etc.
I haven't use Go since 2024, but I was going to say something similar--seems like I was pretty happy doing all my Functional style coding in Go. The problem for me was the client didn't want us to use it. We were given the choice between Java (ugh) and Python to build APIs. We chose Python because I cross my arms and bite my lip and refuse to write anymore Java in these days of containers as the portability. I never really liked Java, or maybe I never really like the kinds of jobs you get using Java? <-- that
Fair I stopped using Go pre-generics so I am pretty out of date. I just remember having this conversation about generics and at the time there was a large anti-generics group. Is it a lot better with generics? I was worried that a lot of the library code was already written pre-generics.
The generics are a weak mimicry of what generics could be, almost as if to say "there we did it" without actually making the language that much more expressive.
For example, you're not allowed to write the following:
type Option[T any] struct { t *T }
func (o *Option[T]) Map[U any](f func(T) U) *Option[U] { ... }
That fails because methods can't have type parameters, only structs and functions. It hurts the ergonomics of generics quite a bit.And, as you rightly point out, the stdlib is largely pre-generics, so now there's a bunch of duplicate functions, like "strings.Sort" and "slices.Sort", "atomic.Pointer" and "atomic.Value", quite possible a sync/v2 soon https://github.com/golang/go/issues/71076, etc.
The old non-generic versions also aren't deprecated typically, so they're just there to trap people that don't know "no never use atomic.Value, always use atomic.Pointer".
> Now if only Go was consistent about methods vs functions
This also hurts discoverability. `slices`, `maps`, `iter`, `sort` are all top-level packages you simply need to know about to work efficiently with iteration. You cannot just `items.sort().map(foo)`, guided and discoverable by auto-completion.
> Now if only Go was consistent about methods vs functions
Generics can only be on function and not methods because of it's type system. So don't hold your breath and modifying this would be a breaking change.
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've worked with Java, C#, C++, etc., and I find that for all its faults, Go's minimalism strikes a balance between abstraction and concreteness that works just fine.
In practice, I've never struggled to write modular, extensible software. Like in any language you have to think about the boundaries between modules and layers, and how to design the interfaces between them to avoid spaghetti soup. It's not more difficult in Go, just different. Java's OO approach kind of inextricably pulls you towards AbstractSingletonProxyFactoryBean situations that just feel wrong to replicate in Go. A lot of the "performative" software structure kind of falls away and you end up with very concrete, readable code.
Go is often derided for being "too simple", but I think this kind of simplicity is underrated in our industry. I'm a fan of the Niklaus Wirth approach to software development. Go is basically a reskinned Modula 2 with GC and concurrency, and that it's the Wirth influence that drives its core design.
To be clear, Go is chock full of warts and faults, some of them quite egregious. But they're mostly about the practical aspects of the language (invasive error handling, lack of sum types and pattern matching, the badness of channels) that don't really relate to what you're talking about.
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#.
Just because you can learn about something doesn't mean you need to. C# now offers top-level programs that are indistinguishable from python scripts at a quick glance. No namespaces, classes or main methods are required. Just the code you want to execute and one simple file.
https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals...
And they will trip over the remains 20% percent for the rest of their days.
"Simple syntax with usually only one way to do things" is pretty much the exact opposite of Java.
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.
There is no one paradigm for concurrency, no method is strictly better than the other. Channels are not the only primitive used in go either, so it's a bit moot point.
What's important is how good primitives you have access to. Java has platform and virtual threads now (the latter simplifying a lot of cases where reactive stuff was prevalent before) with proper concurrent data structures.
But that's only because they're older and were around before modern concurrent programming was invented.
In C#, for example, there are multiple ways, but you should generally be using the modern approach of async/Task, which is trivial to learn and used exclusively in examples for years.
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.
Fair, but I think the JavaScript ecosystem is unique in that the language is very separate from the thing that executes/compiles it. When you write Go, 99.9% of the time you're writing for the Go compiler.
When you write JavaScript (or TypeScript that gets transpiled), it's not as easy to assume the target is Node (V8). It could be Bun (JavaScriptCore), Deno, a browser, etc.
it is pedantic, everyone knows what "node" means in this context
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.
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.
Go is simplistic in comparison.
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".
Yeah I think, given its gradual typing approach, that any discussion about the quality and utility of Python's type system assumes that one is using one of the best in class type checkers right now.
I didn't really use it much until the last few years. It was limited and annoyiongly verbose. Now it's great, you don't even have to do things like explicitly notate covariant/contravariant types, and a lot of what used to be clumsy annotation with imports from typing is now just specified with normal Python.
And best of all, more and more libraries are viewing type support as a huge priority, so there's usually no more having to download type mocks and annotation packages and worry about keeping them in sync. There are some libraries that do annoying things like adding " | None" after all their return types to allow themselves to be sloppy, but at least they are sort of calling out to you that they could be sloppy instead of letting it surprise you.
It's now good and easy enough that it saves me time to use type annotations even for small scripts, as the time it saves from quickly catching typos or messing up a return type.
Like you said, Pydantic is often the magic that makes it really useful. It is just easy enough and adds enough value that it's worth not lugging around data in dicts or tuples.
My main gripe with Go's typing has always been that I think the structural typing of its interfaces is convenient but really it's convenient in the same way that duck typing is. In the same way that a hunter with a duck call is the same as a duck with duck typing, a F16 and a VCR are both things that have an ejection feature.
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.
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.
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.
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.
C# is a highly underrated language that has evolved very quickly over the last decade into a nice mix of OOP and functional.
It's fast enough, easy enough (being very similar now to TypeScript), versatile enough, well-documented (so LLMs do a great job), broad and well-maintained first party libraries, and the team has over time really focused on improving terseness of the language (pattern matching and switch expressions are really one thing I miss a lot when switching between C# and TS).
EF Core is also easily one of the best ORMs: super mature, stable, well-documented, performant, easy to use, and expressive. Having been in the Node ecosystem for the past year, there's really no comparison for building fast with less papercuts (Prisma, Drizzle, etc. all abound with papercuts).
It's too bad that it seems that many folks I've chatted with have a bad taste from .NET Framework (legacy, Windows only) and may have previously worked in C# when it was Windows only and never gave it another look.
This, Go came and I just have no desire to go back to those languages spawned from hell that force fed me horrible patterns and called it best practices.
Go is amazing in that it lets you tell the machine what you want, simply and you can easily verify that that is indeed what the machine should be doing.
Regarding defer, idk about other, but I never assumed it was a gotcha, you read the go docs once and all is just clear and you don’t make most mistakes that others claim are footguns.
I spent untold hours and years mastering inversion of control and late-binding and all those design patterns that were SOOOOO important for interviews only to never really utilize all that stuff because once the apps were built, they rarely, if ever, got reconfigured to do something besides the exact thing they were built for. We might as well have not bothered with any of it and just wrote modular, testable non-OOP code and called it a day. After just about 25 years, I look back at all the time I spent using Spring Framework, Struts, and older products and just kind of shake my head. It was all someone else's money making scheme.
I'm also reminded about the time that Tomcat stopped being an application you deploy to and just being an embedded library in the runtime! It was like the collective light went on that Web containers were just a sham. That didn't prevent employers from forcing me to keep using Websphere/WAS because "they paid for that and by god they're going to use it!" Meanwhile it was totally obsolete as docker containers just swept them all by the wayside.
I wonder what "Webshere admins" are doing these days? That was once a lucrative role to be able to manage those Jython configs, lol.
Plus it seems hopeful to think you'll be only working with "New java" paradigm when most enterprise software is stuck on older versions. Just like python, in theory you can make great new green field project but 80% of the work in the industry is on older or legacy components.
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.
If you believe that's the reason for the JVM (and that it's a "VM" in the traditional sense), you are greatly mistaken. It's like saying C is no longer needed because there is only Arm and x86..
The JVM is a runtime, just like what Go has. It allows for the best observability of any platform (you can literally connect to a prod instance and check e.g. the object allocations) and has stellar performance and stability.
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.
I can reasonably likely run a 30 years old compiled, .jar file on the latest Java version. Java is the epitome of backwards and forward-compatible changes, and the language was very carefully grown so the syntax is not too indifferent, someone hibernated since Java 7 will probably have no problem reading Java 25 code.
> Go, with all its faults, tries very hard to shun complexity
The whole field is about managing complexity. You don't shun complexity, you give tools to people to be able to manage it.
And Go goes the low end of the spectrum, of not giving enough features to manage that complexity -- it's simplistic, not simple.
I think the optimum as actually at Java - it is a very easy language with not much going on (compared to, say, Scala), but just enough expressivity that you can have efficient and comfortable to use libraries for all kind of stuff (e.g. a completely type safe SQL DSL)
There are still a LOT of places running old versions of Java, like JDK 8.
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.
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.
The comparative strictness and simplicity of Go also makes it a good option for LLM-assisted programming.
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.
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.
Google is 100% writing a whole load of new services, and Go is 13 years old (even older within Google), so it surely has had ample opportunities to take.
As for hot swap, I haven't heard it being used for production, that's mostly for faster development cycles - though I could be wrong. Generally it is safer to bring up the new version, direct requests over, and shut down the old version. It's problematic to just hot swap classes, e.g. if you were to add a new field to one of your classes, how would old instances that lack it behave?
HotSwap is really useful to be able to make small adjustments, e.g. to add a logging statement somewhere to test your hypothesis. It's probably not safe to use to change the behaviour significantly, certainly not in production :)
What modern language is a better fit for new projects in your opinion?
I love Elixir but you cannot compile it into a single binary, it is massively concurrent but single-threaded slow, and deployment is still nontrivial.
And lists are slower than arrays, even if they provide functional guarantees (everything is a tradeoff…)
That said, pretty much everything else about it is amazing though IMHO and it has unique features you won’t find almost anywhere else
That doesn’t exist yet. Also Elixir is in no way a replacement for Go.
It can’t match it for performance. There’s no mutable array, almost everything is a linked list, and message passing is the only way to share data.
I primarily use Elixir in my day job, but I just had to write high performance tool for data migration and I used Go for that.
My vote is for Elixir as well, but it's not a competitor for multiple important reasons. There are some languages in that niche, although too small and immature, like Crystal, Nim. Still waiting for something better.
P.S. Swift, anyone?
yeah, if the requirement is "makes it pretty straightforward to write reliable, highly concurrent services that don't rely on heavy multithreading", Elixir is a perfect match.
And even without types (which are coming and are looking good), Elixir's pattern matching is a thousands times better than the horror of Go error handling
For web frontend: js
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.
I'd swap java with go any day of the week. I never liked how much 'code-padding' is required with java `public static void main`
An expert Ruby programmer can do wonders and be insanely productive, but I think there is a size from which it doesn't scale as nicely (both from a performance and a larger team perspective).
PHP's frameworks are fantastic and they hide a lot from an otherwise minefield of a language (though steadily improved over the years).
Both are decent choices if this is what you/your developers know.
But they wouldn't be my personal first choice.
Absolutely no on Java. Even if the core language has seen improvements over the years, choosing Java almost certainly means that your team will be tied to using proprietary / enterprise tools (IntelliJ) because every time you work at a Java/C# shop, local environments are tied to IDE configurations. Not to mention Spring -- now every code review will render "Large diffs are not rendered by default." in Github because a simple module in Java must be a new class at least >500 LOC long.
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.
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.
Rust async makes it quite easy to shoot yourself in the foot in multiple ways.
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.
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.
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.
Go has problems, sure. But I’ve yet to see a hit piece on Go that actually holds up to real scrutiny.
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.
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]
Notable mentions:
- No sum types
- No string interpolation
I wrote a book on Go, so I'm biased. But when I started using Go more than a decado ago, it really felt like a breath of fresh air. It made coding _fun_ again, less boilerplate heavy than Java, simple enough to pick up, and performance was generally good.
There's no single 'best language', and it depends on what your use-cases are. But I'd say that for many typical backend tasks, Go is a choice you won't really regret, even if you have some gripes with the language.
Often, when I have some home DIY or woodworking problem, I reach for my trusty Dremel:
* The Dremel is approachable: I don't have to worry about cutting off my hand with the jigsaw or set up a jig with the circular saw. I don't have to haul my workpiece out to the garage.
* The Dremel is simple: One slider for speed. Apply spinny bit to workpiece.
* The Dremel is fun: It fits comfortably in my hand. It's not super loud. I don't worry about hurting myself with it. It very satisfyingly shaves bits of stuff off things.
In so many respects, the Dremel is a great tool. But 90% of the time when I use it, it ends up taking my five times as long (but an enjoyable 5x!) and the end result is a wobbly scratchy mess. I curse myself for not spending the upfront willpower to use the right tool for the job.
I find myself doing this with all sorts of real and software tools: Over-optimizing for fun and ease-of-entry and forgetting the value of the end result and using the proper tool for the job.
I think of this as the "Dremel effect" and I try to be mindful of it when selecting tools.
That's a fun analogy.
Most of my coding these days is definitely in the 'for fun' bucket given my current role. So I'd rather take 5x and have fun.
That said, I don't think Go is only fun, I think it's also a viable option for many backend projects where you'd traditionally have reached for Java / C#. And IMO, it sure beats the recent tendency of having JS/Python powering backend microservices.
Agreed that if Go is a Dremel (I'm not sure whether or not it is) then JS is, like, a rusty non-locking pocket knife.
For the most part I've loved Go since just before 1.0 through today. Nits can surely be picked, but "it's still not good" is a strange take.
I think there is little to no chance it can hold on to its central vision as the creators "age out" of the project, which will make the language worse (and render the tradeoffs pointless).
I think allowing it to become pigeon holed as "a language for writing servers" has cost and will continue to cost important mindshare that instead jumps to Rust or remains in Python or etc.
Maybe it's just fun, like harping on about how bad Visual Basic was, which was true but irrelevant, as the people who needed to do the things it did well got on with doing so.
They are forcing people to write Typescript code like it’s Golang where I am right now (amongst other extremely stupid decisions - only unit test service boundaries, do not pull out logic into pure functions, do not write UI tests, etc.). I really must remember to ask organisations to show me their code before joining them.
(I realise this isn’t who is hiring, but email in bio)
I do this and think it works really well...
myfunc(arg: string): Value | Err
I really try not to throw anymore with typescript, I do error checking like in Go. When used with a Go backend, it makes context switching really easy...
Reminded me of this classic talk https://www.youtube.com/watch?v=o9pEzgHorH0
If you don't like Go, then just let go. I hope nobody forces you to use it.
Some critique is definitely valid, but some of it just sounds like they didn't take the time to grasp the language. It's trade offs all the way. For example there is a lot I like about Rust, but still no my favorite language.
Disagree. Most critiques of Go I've read have been weak. This one was decent. And I say that as a big enjoyer of Go.
That said I really wish there was a revamp where they did things right in terms of nil, scoping rules etc. However, they've commited to never breaking existing programs (honorable, understandable) so the design space is extremely limited. I prefer dealing with local awkwardness and even excessive verbosity over systemic issues any day.
Few things are truly forced upon me in life but walking away from everything that I don't like would be foolish. There is compromise everywhere and I don't think entering into a tradeoff means I'm not entitled to have opinions about the things I'm trading off.
I don't think the article sounds like someone didn't take the time to grasp the language. It sounds like it's talking about the kind of thing that really only grates on you after you've seriously used the language for a while.
This article was a well-thought-out one from someone who has obviously really used Go to build real things.
I quite like Go and use it when I can. However, I wish there were something like Go, without these issues. It's worth talking about that. For instance, I think most of these critiques are fair but I would quibble with a few:
1. Error scope: yes, this causes code review to be more complex than it needs to be. It's a place for subtle, unnecessary bugs.
2. Two types of nil: yes, this is super confusing.
3. It's not portable: Go isn't as portable as C89, but it's pretty damn portable. It's plenty portable to write a general-purpose pre-built CLI tool in, for instance, which is about my bar for "pragmatic portability."
4. Append ownership & other slice weirdness: yes.
5. Unenforced `defer`: yes, similar to `err`, this introduces subtle bugs that can only be overcome via documentation, careful review, and boilerplate handling.
6. Exceptions on top of err returns: yes.
7. utf-8: Hasn't bitten me, but I don't know how valid this critique is or isn't.
8. Memory use: imo GC is a selling-point of the language, not a detriment.
In my opinion, the section on data ownership contained the most egregious and unforgivable example of go's flaws. The behavior of append in that example is the kind of bug-causing or esoteric behavior that should never make it into any programming language. As a regular writer of go code, I understand why this particular quirk of the language exists, but I hope I never truly "grasp" it to the extent that I forgive it.
I'm surprised people in these comments aren't focusing more on the append example.
In practice, none of these thing mentioned in the article have been an issue for me, at all. (Upvoted anyway)
What has been an issue for me, though, is working with private repositories outside GitHub (and I have to clarify that, because working with private repositories on GitHub is different, because Go has hardcoded settings specifically to make GitHub work).
I had hopes for the GOAUTH environment variable, but either (1) I'm more dumb and blind than I thought I already was, or (2) there's still no way to force Go to fetch a module using SSH without trying an HTTPS request first. And no, `GOPRIVATE="mymodule"` and `GOPROXY="direct"` don't do the trick, not even combined with Git's `insteadOf`.
Definitely not just you. At my previous job we had a need to fetch private Go modules from Gitlab and, later, a self-hosted instance of Forgejo. CTO and I spent a full day or so doing trial and error to get a clean solution. If I recall correctly, we ultimately resorted to each developer adding `GOPRIVATE={module_namespace}` to their environment and adding the following to their `.netrc`:
``` machine {server} # e.g. gitlab.com login {username} password {read_only_api_key} # Must be actual key and not an ENV var ```
Worked consistently, but not a solution we were thrilled with.
The billion dollar mistake was made in 1965 but the term was coined in 2009, defined as the following:
> I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
I don't agree with most of the article but I believe I know where it comes from.
Golang's biggest shortcoming is the fact that it touches bare metal isn't visible clearly enough. It provides many high level features which makes this ambience of "we got you" but fails on delivering proper education to its users that they are going to have a dirt on their hands.
Take a slice for example: even in naming it means "part of" but in reality it's closer to "box full of pointers" what happens when you modify pointer+1? Or "two types of nil"; there is a difference between having two bytes (simplification), one of struct type and the other of address to that struct and having just a NULL - same as knowing that house doesn't exist and being confident that house exists and saying it's in the middle of the volcano beneath the ocean.
The Foo99 critique is another example. If you'd want to have not 99 loop but 10 billion loops each with mere 10 bytes you'd need 100GiB of memory just to exit it. If you'd reuse the address block you'd only use... 10 bytes.
I also recommend trying to implement lexical scope defer in C and putting them in threads. That's a big bottle of fun.
I think that it ultimately boils down to what kind of engineer one wants to be. I don't like hand holding and rather be left on my own with a rain of unit tests following my code so Go, Zig, C (from low level Languages) just works for me. Some prefer Rust or high level abstractions. That's also fine.
But IMO poking at Go that it doesn't hide abstractions is like making fun of football of being child's play because not only it doesn't have horses but also has players using legs instead of mallets.
> I believe I know where it comes from […] poking at Go that it doesn't hide abstractions
Author here.
No, this is not where it comes from. I've been coding C form more than 30 years, Go for maybe 12-15, and currently prefer Rust. I enjoy C++ (yes, really) and getting all those handle-less knives to fit together.
No, my critique of Go is that it did not take the lessons learned from decades of theory, what worked and didn't work.
I don't fault Go for its leaky abstractions in slices, for example. I do fault it for creating bad abstraction APIs in the first place, handing out footguns when they are avoidable. I know to avoid the footgun of appending to slices while other slices of the same array may still be accessible elsewhere. But I think it's indefensible to have created that footgun in the year Go was created.
Live long enough, and anybody will make a silly mistake. "Just don't make a mistake" is not an option. That's why programming language APIs and syntax matters.
As for bare metal; Go manages to neither get the benefits possible of being high level, and at the same time not being suitable for bare metal.
It's a missed opportunity. Because yes, in 2007 it's not like I could have pointed to something that was strictly better for some target use cases.
I don't share experience about not being suitable about bare metal. But I do have experience with high level languages doing similar things through "innovative" thinking. I've seen int overflows in Rust. I've seen libraries that waited for UDP packet to be rebroadcasted before sending another implemented in Elixir.
No Turing complete language will ever prevent people from being idiots.
It's not only programming language API and syntax. It's a conceptual complexity, which Go has very low. It's a remodeling difficulty which Rust has very high. It's implicit behavior that you get from high stack of JS/TS libraries stitched together. It's accessibility of tooling, size of the ecosystem and availability of APIs. And Golang crosses many of those checkboxes.
All the examples you've shown in your article were "huh? isn't this obvious?" to me. With your experience in C I have no idea you why you don't want to reuse same allocation multiple times and instead keeping all of them separately while reserving allocation space for possibly less than you need.
Even if you'd assume all of this should be on stack you still would crash or bleed memory through implicit allocations that exit the stack.
Add 200 of goroutines and how does that (pun intended) stack?
Is fixing those perceived footguns really a missed opportunity? Go is getting stronger every year and while it's hated by some (and I get it, some people like Rust approach better it's _fine_) it's used more and more as a mature and stable language.
Many applications don't even worry about GC. And if you're developing some critical application, pair it with Zig and enjoy cross-compilation sweetness with as bare metal as possible with all the pipes that are needed.
> With your experience in C I have no idea you why you don't want to reuse same allocation multiple times and instead keeping all of them separately while reserving allocation space for possibly less than you need.
Which part are you referring to, here?
> Even if you'd assume all of this should be on stack you still would crash or bleed memory through implicit allocations that exit the stack.
What do you mean by this? I don't mean to be rude, but this sounds confusing if you understand how memory works. What do you mean an allocation that exits the stack would bleed memory?
> All the examples you've shown in your article were "huh? isn't this obvious?" to me.
It is. None of this was new to me. In C++ defining a non-virtual destructor on a class hierarchy is also not new to me, but a fair critique can be made there too why it's "allowed". I do feel like C++ can defend that one from first principles though, in a way that Go cannot.
I'm not sure what you mean by the foo99 thing. I'm guessing this is about defer inside a loop?
> Is fixing those perceived footguns really a missed opportunity?
In my opinion very yes.
I like Go, but my main annoyance is deciding when to use a pointer or not use a pointer as variable/receiver/argument. And if its an interface variable, it has a pointer to the concrete instance in the interface 'struct'. Some things are canonically passed as pointers like contexts.
It just feels sloppy and I'm worried I'm going to make a mistake.
This confused me too. It is tricky because sometimes it's more performant to copy the data rather than use a pointer, and there's not a clear boundary as to when that is the case. The advice I was given was "profile your code and make your decision data-driven". That didn't make me happy.
Now I always use pointers consistently for the readability.
But just not a pointer to an interface.
Its annoying to need to think about whether I’m working with an interface type of concrete type.
And if use pointers everywhere, why not make it the default?
I 80% of time use structs. common misunderstanding: it does not reduce performance for pointer vs value receivers (Go compiler generates same code for both, no copy of struct receiver happens). most of structs are small anyways, safe to copy. Go also automatically translates value receivers and pointer receivers back-and-forth. and if I see pointer I see something that can be mutated (or very large). in fact, if I see a pointer, I think "here we go.. will it be mutated?". written 400,000 LOC in Go, rarely seeing this issue.
- I’ve seen a lot of debate here comparing Go’s issues (like nil handling or error scoping) to Rust’s strengths.
- As someone who’s worked with C/C++ and Fortran, I think all these languages have their own challenges—Go’s simplicity trades off against Rust’s safety guarantees, for example.
- Could someone share a real-world example where Go’s design caused a production issue that Rust or another language would’ve avoided?
- I’m curious how these trade-offs play out in practice.
Sorry, I don't do Go/Rust coding, still on C/C++/Fotran.
> Go’s design caused a production issue
A simple one, if you create two separate library in Go and try to link with an application, you will have a terrible time.
I've ran into this same issue: https://github.com/golang/go/issues/65050
It's a niche use case to have software that load plugins and it just so happens those plugins are written in Go? No it's not a niche case. If all programing you do in Go is web servers than sure you won't see this.
Recently I was in a meeting where we were considering adopting Go more widely for our backend services, but a couple of the architect level guys brought up the two-types-of-nil issue and ultimately shot it down. I feel like they were being a little dramatic about it, but it is startling to me that its 2025 and the team still has not fixed it. If the only thing you value in language design is never breaking existing code, even if by any definition that existing code is already broken, eventually the only thing using your language will be existing code.
This has already been explained many times, but it's so much fun I'll do it again. :-)
So: The way Go presents it is confusing, but this behavior makes sense, is correct, will never be changed, and is undoubtedly depended on by correct programs.
The confusing thing for people use to C++ or C# or Java or Python or most other languages is that in Go nil is a perfectly valid pointer receiver for a method to have. The method resolution lookup happens statically at compile time, and as long as the method doesn't try to deref the pointer, all good.
It still works if you assign to an interface.
package main
import "fmt"
type Dog struct {}
type Cat struct {}
type Animal interface {
MakeNoise()
}
func (*Dog) MakeNoise() { fmt.Println("bark") }
func (*Cat) MakeNoise() { fmt.Println("meow") }
func main() {
var d *Dog = nil
var c *Cat = nil
var i Animal = d
var j Animal = c
d.MakeNoise()
c.MakeNoise()
i.MakeNoise()
j.MakeNoise()
}
This will print bark
meow
bark
meow
But the interface method lookup can't happen at compile time. So the interface value is actually a pair -- the pointer to the type, and the instance value. The type is not nil, hence the interface value is something like (&Cat,nil) and (&Dog,nil) in each case, which is not the interface zero value, which is (nil, nil).But it's super confusing because Go type cooerces a nil struct value to a non-nil (&type, nil) interface value. There's probably some naming or syntax way to make this clearer.
But the behavior is completely reasonable.
The underlying reason, which you hint on, is that in Go (unlike Python, Java, C#… even C++) the “type” of an “object” is not stored alongside the object.
A struct{a, b int32} takes 8 bytes of memory. It doesn't use any extra bytes to “know” its type, to point to a vtable of “methods,” to store a lock, or any other object “header.”
Dynamic dispatch in Go uses interfaces which are fat pointers that store the both type and a pointer to an object.
With this design it's only natural that you can have nil pointers, nil interfaces (no type and no pointer), and typed interfaces to a nil pointer.
This may be a bad design decision, it may be confusing. It's the reason why data races can corrupt memory.
But saying, as the author, “The reason for the difference boils down to again, not thinking, just typing” is just lazy.
Just as lazy as it is arguing Go is bad for portability.
I've written Go code that uses syscalls extensively and runs in two dozen different platforms, and found it far more sensible than the C approach.
Yeah, I totally agree -- given Go's design, the behavior makes sense (and changing the behavior just to make it more familiar to users of languages that fundamentally work differently would be silly).
However, the non-intuitive punning of nil is unfortunate.
I'm not sure what the ideal design would be.
Perhaps just making an interface not comparable to nil, but instead something like `unset`.
type Cat struct {}
type Animal interface{}
func main() {
var c *Cat = nil
var a Animal = c
if a == nil { // compile error, can't compare interface to nil
;
}
if a == unset { // false, hopefully intuitively
}
}
Still, it's a sharp edge you hit once and then understand. I am surprised people get so bothered by it...it's not like something that impairs your use of the language once you're proficient.(E.g. complaints about nil existing at all, or error handling, are much more relatable!)
(Side note, Go did fix scoping of captured variables in for,range loops, which was a back-incompat change, but they justified it by emperically showing it fixed more bugs than it caused (very reasonable). C# made the same change w/ the same justification earlier, which was inspiration for Go.)
I deeply, seriously, believe that you should have written the words "Its super confusing", meditated on that for a minute, then left it at that. It is super confusing. That's it. Nothing else matters. I understand why it is the way it is. I'm not stupid. As you said: Its super confusing, which is relevant when you're picking languages other people at your company (interns, juniors) have to write in.
> “The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.”
I think our end-state decision, IIRC, was to just expand our usage of TypeScript; which also has Golang beat on all those verticals you list. More mature, way better tooling, way more libraries, easier to hire for, etc.
Though, thinking back, someone should have brought up TypeScript's at least three different ways to represent nil (undefined, null, NaN, a few others). Its at least a little better in TS, because unlike in Go the type-checker doesn't actively lie to you about how many different states of undefined you might be dealing with.
> I feel like they were being a little dramatic about it, but it is startling to me that its 2025 and the team still has not fixed it.
You were right, it's a niche and therefore pretty much irrelevant issue. They may as well have rejected Python due to its "significant whitespace".
As usual, lets revisit something that Pascal could do in 1976,
type
StatusCodes = (Success, Ongoing, Done)
Go in 2025, type StatusCodes int
const (
Success StatusCodes = iota
Ongoing
Done
)
Ouch!! Pascal's lack of popularity certainly isn't due to the fact that it supports such nice enumerated types (or sets for that matter). I think he was just pointing out that such nice things have existed (and been known to exist) for a long time and that it's odd that a new language couldn't have borrowed the feature.
Being used by these folks, https://www.embarcadero.com/
If you prefer, I can provide the same example in C, C++, D, Java, C#, Scala, Kotlin, Swift, Rust, Nim, Zig, Odin.
Just below Go with Perl in between. All above Fortran, all below Visual Basic.
It's alive and kicking, right? :) https://www.freepascal.org They even have a game engine that can compile to a WASM target: https://castle-engine.io/web
Pascal evolved into Modula-2, which Wirth then simplified into Oberon. His student Griesemer did his dissertation on extending Oberon for parallel programming on supercomputers. Concurrently, Pike found Modula-2 an inspiration for some languages he wrote in the 80s and 90s. He got together with Griesemer and Ken Thompson to rework one of those languages, Newsqueak, into Golang. So that's where Pascal is today.
People want sum types because sum types solve a large set of design problems, while being a concept old enough to appear back in SML in 1980s. One of the best phrased complaints I've seen against Go's design is a claim that Go language team ignored 30+ years of programming language design, because the language really seems to introduce design issues and footguns that were solved decades before work on it even started
Great article!
I like Go and Rust, but sometimes I feel like they lack tools that other languages have just because they WANT to be different, without any real benefit.
Whenever I read Go code, I see a lot more error handling code than usual because the language doesn't have exceptions...
And sometimes Go/Rust code is more complex because it also lacks some OOP tools, and there are no tools to replace them.
So, Go/Rust has a lot more boilerplate code than I would expect from modern languages.
For example, in Delphi, an interface can be implemented by a property:
type
TMyClass = class(TInterfacedObject, IMyInterface)
private
FMyInterfaceImpl: TMyInterfaceImplementation; // A field containing the actual implementation
public
constructor Create;
destructor Destroy; override;
property MyInterface: IMyInterface read FMyInterfaceImpl implements IMyInterface;
end;
This isn't possible in Go/Rust. And the Go documentation I read strongly recommended using Composition, without good tools for that.This "new way is the best way, period ignore good things of the past" is common.
When MySQL didn't have transactions, the documentation said "perform operations atomically" without saying exactly how.
MongoDB didn't have transactions until version 4.0. They said it wasn't important.
When Go didn't have generics, there were a bunch of "patterns" to replace generics... which in practice did not replace.
The lack of inheritance in Go/Rust leaves me with the same impression. The new patterns do not replace the inheritance or other tools.
"We don't have this tool in the language because people used it wrong in the old languages." Don't worry, people will use the new tools wrong too!
Go allows deferring an implementation of an interface to a member of a type. It is somewhat unintuitive, and I think the field has to be an unnamed one.
Similarly, if a field implements a trait in Rust, you can expose it via `AsRef` and `AsMutRef`, just return a reference to it.
These are not ideal tools, and I find the Go solution rather unintuitive, but they solve the problems that I would've solved with inheritance in other languages. I rarely use them.
Been using Go for two years now, coming from C. Totally fair points. Go’s quirks can feel more like landmines than design decisions, especially when coming from languages that handle things like RAII, error scope, or nil with more grace. But part of Go’s charm (and curse) is its unapologetic minimalism. It’s not trying to be elegant, just predictable and maintainable at scale. Saying “no sane person” would choose X might feel cathartic, but it shuts down understanding of why rational teams do choose Go and often thrive with it. Go’s not for everyone, but it does what it does on purpose.
A popular language is always going to attract some hate. Also, these kinds of discussions can be useful for helping the language evolve.
But everyone knows in their heart of hearts that a few small language warts definitely don't outweigh Go's simplicity and convenience. Do I wish it had algebraic data types, sure, sure. Is that a deal-breaker, nah. It's the perfect example of something that's popular for a reason.
It is easily one of the most productive languages. No fuss, no muss, just getting stuff done.
I agree with just about everything in the post. I've been bit a time or two by the "two flavors of null." That said, my most pleasant and most productive code bases I've worked in have all been Go.
Some learnings. Don't pass sections of your slices to things that mutate them. Anonymous functions need recovers. Know how all goroutines return.
Every language has its flaws. I respect Go for staying relatively simple. And it has decent concurrency (for my needs).
These days, it seems like languages keep chasing paradigms and over adapt to moving targets.
Look at what Rust and Swift have become. C# has stayed relatively sane somehow, but it's not what I'd call indepedent.
every language has its problems; Go I think is pretty good despite them. not saying points raised in the article are invalid, you def have to be careful, and I hate the "nil interface is not necessarily nil" issue as much as anyone.
It's hard to find a language that will satisfy everyone's needs. Go I find better for smaller, focused applications/utilities... can definitely see how it would cause problems at an "enterprise" level codebase.
I both agree with these points, and also think it absolutely doesn't matter. Go is the best language if you need to ship quickly and have solid performance. Also Go + AI works amazingly well. So in some ways you can actually move faster compared to languages like Node and Python these days.
This has always been my takeaway with Go. An imperfect language for imperfect developers, chosen for organizations (not people) to ensure a baseline usefulness of their engineers from junior to senior. Do I like it? No. Would I ever choose it willingly? No. But when the options at the time were Javascript or untyped Python, it may have seemed like a more attractive option. Python was also dealing with a nasty 2-to-3 upgrade at the time that looks foolish in comparison to Golang's automatic formatting and upgrade mechanisms.
That's why there is the Goo language: Go with syntactic sugar and batteries included
https://github.com/pannous/goo/
• errors handled by truthy if or try syntax • all 0s and nils are falsey • #if PORTABLE put(";}") #end • modifying! methods like "hi".reverse!() • GC can be paused/disabled • many more ease of use QoL enhancements
Has Go become the new PHP? Every now and then I see an article complaining about Go's shortcomings.
No, this has been the case as long as Go has been around, then you look and its some C or C++ developer with specific needs, thats okay, its not for everyone.
I think with C or C++ devs, those who live in glass houses shouldn’t throw stones.
I would criticize Go from the point of view of more modern languages that have powerful type systems like the ML family, Erlang/Elixir or even the up and coming Gleam. These languages succeed in providing powerful primitives and models for creating good, encapsulating abstractions. ML languages can help one entirely avoid certain errors and understand exactly where a change to code affects other parts of the code — while languages like Erlang provided interesting patterns for handling runtime errors without extensive boilerplate like Go.
It’s a language that hobbles developers under the aegis of “simplicity.” Certainly, there are languages like Python which give too much freedom — and those that are too complex like Rust IMO, but Go is at best a step sideways from such languages. If people have fun or get mileage out of it, that’s fine, but we cannot pretend that it’s really this great tool.
My biggest nitpick against Go was, is and still is the package management. Rust did it so nice and NuGet (C#/.NET) got it so right that Microsoft added it as a built-in thing for Visual Studio, it was originally a plugin and not from Microsoft whatsoever, now they fully own it which is fine, and it just works.
Cargo is amazing, and you can do amazing things with it, I wish Go would invest in this area more.
Also funny you mention Python, a LOT of Go devs are former Python devs, especially in the early days.
> Has Go become the new PHP? Every now and then I see an article complaining about Go's shortcomings.
These sorts of articles have been commonplace even before Go released 1.0 in 2013. In fact, most (if not all) of these complaints could have been written identically back then. The only thing missing from this post that could make me believe it truly was written in 2013 would be a complaint about Go not having generics, which were added a few years ago.
People on HN have been complaining about Go since Go was a weird side-project tucked away at Google that even Google itself didn't care about and didn't bother to dedicate any resources to. Meanwhile, people still keep using it and finding it useful.
The last 20% is also deliberately never done. It's the way they like to run their language. I find it frustrating, but it seems to work for some people.
Go is a pretty good example of how mediocre technology that would never have taken off on its own merits benefits from the rose tinted spectacles that get applied when FAANG starts a project.
I don’t buy this at all. I picked up Go because it has fast compilation speed, produces static binaries, can build useful things without a ton of dependencies, is relatively easy to maintain, and has good tooling baked in. I think this is why it gained adoption vs Dart or whatever other corporate-backed languages I’m forgetting.
80% of what programmers write is API glue.
Go _excels_ at API glue. Get JSON as string, marshal it to a struct, apply business logic, send JSON to a different API.
Everything for that is built in to the standard library and by default performant up to levels where you really don't need to worry about it before your API glue SaaS is making actual money.
I tried out one project because of these attributes and then scrapped it fairly quickly in favor of rust. Not enough type safety, too much verbosity. Too much fucking "if err != nil".
The language sits in an awkward space between rust and python where one of them would almost always be a better choice.
But, google rose colored specs...
Exactly.
The other jarring example of this kind of deferring logical thinking to big corps was people defending Apple's soldering of memory and ssd, specially so on this site, until some Chinese lad proved that all the imagined issues for why Apple had to do such and such was bs post hoc rationalisation.
The same goes with Go, but if you spend enough time, every little while you see the disillusionment of some hardcore fans, even from the Go's core team, and they start asking questions but always start with things like "I know this is Go and holy reasons exists and I am doing a sin to question but why X or Y". It is comedy.
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.