Every language has an arsenal of footguns. Go is no different. I would say that overall it is not too bad, comparatively.
From all the listed cases, only the first one is easy to get caught by, even as an experienced developer. There, the IDE and syntax highlighting is of tremendous help and for general prevention. The rest is just understanding the language and having some practice.
That first example is an unintended closure, since the err at the top level actually has nothing to do with the errs in the goroutines. I have seen that sometimes, although the use of = rather than := normally makes it obvious that something dodgy is going on.
As to whether it's a common pattern, I see closures on WaitGroups or ErrGroups quite often:
workerCount := 5
var wg sync.WaitGroup
wg.Add(workerCount)
for range workerCount {
go func() {
// Do work
wg.Done()
}()
}
wg.Wait()
You can avoid the closure by making the worker func take a *sync.WaitGroup and passing in &wg, but it doesn't really have any benefit over just using the closure for convenience.
4 ways to demonstrate that the author either knows nothing about closures, structs, mutexes, and atomicity OR they just come from a Rust background and made some super convoluted examples to crap on Go.
“A million ways to segfault in C” and its just the author assigning NULL to a pointer and reading it, then proclaiming C would be better if it didn’t have a NULL value like Rust.
>I have been writing production applications in Go for a few years now. I like some aspects of Go. One aspect I do not like is how easy it is to create data races in Go.
To me it looks like simple, clear examples of potential issues. It's unfortunate to frame that as "crapping on Go", how are new Go programmers going to learn about the pitfalls if all discussion of them are seen as hostility?
Like, rightly or wrongly, Go chose pervasive mutability and shared memory, it inevitably comes with drawbacks. Pretending they don't exist doesn't make them go away.
Go is a bit more of a low level language compared to actor languages where the language enforces that programming model. I think the point of the slogan is that you want to make the shared memory access an implementation detail of your larger system.
Go's creators said "Don't communicate by sharing memory", but then designed goroutines to do exactly that. It's quite hard to not share memory by accident, actually.
It's not like it's a disaster, but it's certainly inconsistent.
I don't think allowing developers to use their discretion to share state is "certainly inconsistent". Not sure what your threshold is for "quite hard" but it seems pretty low to me.
Goroutines could've lacked shared memory by default, requiring you to explicitly pass in pointers to shared things. That would've significantly encouraged sharing memory by communicating.
The opposite default encourages the opposite behaviour.
Threads share the same memory by definition, though. When you isolate these threads from a memory PoV, they become processes.
Moreover, threads are arguably useless without shared memory anyway. A thread is invoked to work on the same data structure with multiple "affectors". Coordination of these affectors is up to you. Atomics, locks, queues... The tools are many.
In fact, processes are just threads which are isolated from each other, and this isolation is enforced by the processor.
Goroutines aren't posix threads. They could've lacked shared memory by default, which could be enforced by a combination of the compiler and runtime like with Erlang.
Concurrent programming is hard and has many pitfalls; people are warned about this from the very, very start. If you then go about it without studying proper usage/common pitfalls and do not use (very) defensive coding practices (violated by all examples) then the main issue is just naivity. No programming language can really defend against that.
Also, these are minimal reproducers, the exact same mistakes can trivially happen in larger codebases across multiple files, where you wouldn't notice them immediately.
> Pretending they don't exist doesn't make them go away.
It's generally assumed that people who defend their favorite programming language are oblivious to the problems the language has or choose to ignore these problems to cope with the language.
There's another possibility: Knowing the footguns and how to avoid them well. This is generally prevalent in (Go/C/C++) vs. Rust discussions. I for one know the footguns, I know how bad it can be, and I know how to avoid them.
Liking a programming language as is, operating within its safe-envelope and pushing this envelope with intent and care is not a bad thing. It's akin to saying that using a katana is bad because you can cut yourself.
We know, we accept, we like the operating envelope of the languages we use. These are tools, and no tool is perfect. Using a tool knowing its modus operandi is not "pretending the problems don't exist".
> Using a tool knowing its modus operandi is not "pretending the problems don't exist".
I said that in response to the hostility ("crap on Go") towards the article. If such articles aren't written, how will newbies learn about the pitfalls in the first place?
While I agree with you in principle, there is a small but important caveat about large codebases with hundreds of contributors or more. It only takes 1 bad apple to ruin the bunch.
During the short time I was working on a Go project I spent a significant amount of time debugging an issue like the one described in his first example in a library we depended on, so it's definitely not a problem of “super convoluted example”.
On the other hand, it should be very obvious for anyone that has experience with concurrency, that changing a field on an object like the author showed can never be safe in a concurrency setting. In any language.
This is not true in the general case. E.g. setting a field to true from potentially multiple threads can be a completely meaningful operation e.g. if you only care about if ANY of the threads have finished execution.
It depends on the platform though (e.g. in Java it is guaranteed that there is no tearing [1]).
[1] In OpenJDK. The JVM spec itself only guarantees it for 32-bit primitives and references, but given that 64-bit CPUs can cheaply/freely write a 64-bit value atomically, that's how it's implemented.
OT: This page uses the term "Learnings" a lot. As a Murrcan in tech comms in Europe, I always corrected this to something else. But, well, is it some sort of Britishism ? Or is it some weird internet usage that is creeping into general usage ?
Likewise for "Trainings". Looks weird to Murrcan eyes but maybe it's a Britishism.
The author is obviously an overcompensating French speaker naively going for the more English-sounding word, i.e. "learnings" instead of "lesson", in this instance an overly literal translation of French "enseignements" as in "tirer des enseignements" meaning "learn a lesson", but since you can also say "tirer des leçons" in French with the same meaning and root, it's just a case of choosing the wrong side haphazardly on the Anglo-Saxon/Latin-Norman-French divide of the English vocabulary, sheep/mutton ox/beef pairs and the like.
I'm English and "learnings" is one piece of corporate speak that really annoys me. It just means "lessons", for people apparently unaware that noun already exists. British corpo drones seem to need their verb/noun pairs to be identical like
From where I sit (in Norway), it seems to have become standard corporate-speak in any company where English is widely used. They've even started using the directly translated noun "læring" in Norwegian, too. It's equally silly. Both variants are usually spoken by the type of manager who sets out all future directions based on whatever their LinkedIn circle is talking about. It's thus a very valuable word, because the rash it elicits lets me know what people to avoid working with.
I'm not sure if the people who use this word think it's proper English. They rarely seem to care what words mean anyway.
I dislike some of this article, my impression is similar to some of the complaints of others here.
However, are Go programs not supposed to typically avoid sharing mutable data across goroutines in the first place? If only immutable messages are shared between goroutines, it should be way easier to avoid many of these issues. That is of course not always viable, for instance due to performance concerns, but in theory can be done a lot of the time.
I have heard others call for making it easier to track mutability and immutability in Go, similar to what the author writes here.
As for closures having explicit capture lists like in C++, I have heard some Rust developers saying they would also have liked that in Rust. It is more verbose, but can be handy.
There is a LOT of demand for explicit capture clauses. This is one thing that C++ got right and Rust got wrong with all its implicit and magic behaviour.
Go is a weird one, because it's super easy to learn -if- you're familiar with say, C. If you're not, it still appears to be super easy to learn, but has enough pitfalls to make your day bad. I feel like much of the article falls into the latter camp.
I recently worked with a 'senior' Go engineer. I asked him why he never used pointer receivers, and after explaining what that meant, he said he didn't really understand when to use asterisks or not. But hey, immutability by default is something I guess.
On a phone and the formatting of the snippets is unreadable with the 8 space tabs…
That said, i think about all languages have their own quirks and footguns. I think people sometimes forget that tools are just that, tools. Go is remarkably easy to be productive in which is what the label on the tin can claims.
It isnt “fearless concurrency” but get shit done before 5 pm because traffics a bitch on Wednesdays
Author here, thanks for the feedback on legibility, I have now just learned about the CSS `tab-size` property to control how much space tabs get rendered with. I have reduced it, should be better now.
In the first one, he complains that one character is enough to cause an issue, but the user should really have a good understanding of variable scope and the difference between assignment and instsntiation if they're writing concurrent go code. Some ides warn the user when they do this with a different color.
Races with mutexes can indicate the author either doesn't understand or refuses to engage with Go's message based concurrency model. You can use mutexes but I believe a lot of these races can be properly avoided using some of the techniques discussed in the go programming language book.
I would argue it doesn't help that all errors are usually named `err` and sprinkled every third line of code in Go. It's an easy mistake to make to assign to an existing variable instead of create a new variable, especially if you frequently switch between languages (which might not have the `:=` operator).
He complains that language design offers no way of avoiding it (in this particular case) and relies only on human or ide. Humans are not perfect and should not be a requirement to write good code.
Whatever the case Go's tooling (i.e. IDE part) is one of the best in class and I think it shouldn't be be dismissed in the context of some footguns that Go has.
I feel like Java's IDE support is best in class. I feel like go is firmly below average.
Like, Java has great tooling for attaching a debugger, including to running processes, and stepping through code, adding conditional breakpoints, poking through the stack at any given moment.
Most Go developers seem to still be stuck in println debugging land, akin to what you get in C.
The gopls language server generally takes noticeably more memory and cpu than my IDE requires for a similarly sized java project, and Go has various IDE features that work way slower (like "find implementations of this interface").
The JVM has all sorts of great knobs and features to help you understand memory usage and tune performance, while Go doesn't even have a "go build -debug" vs "go build -release" to turn on and off optimizations, so even in your fast iteration loop, go is making production builds (since that's the only option), and they also can't add any slow optimizations because that would slow down everyone's default build times. All the other sane compilers I know let you do a slower release build to get more performance.
The Go compiler doesn't emit warnings, insisting that you instead run a separate tool (govet), but since it's a separate tool you now have to effectively compile the code twice just to get your compiler warnings, making it slower than if the compiler just emit warnings.
Go's cgo tooling is also far from best in class, with even nodejs and ruby having better support for linking to C libraries in my opinion.
Like, it's incredibly impressive that Go managed to re-invent so many wheels so well, but they managed to reach the point where things are bearable, not "best in class".
I think the only two languages that achieved actually good IDE tooling are elisp and smalltalk, kinda a shame that they're both unserious languages.
> The gopls language server generally takes noticeably more memory and cpu than my IDE requires for a similarly sized java project
Okay, come on now :D Absolutely everything around Java consumes gigabytes of memory. The culture of wastefulness is real.
The Go vs Java plugins for VSCode are no comparison in terms of RAM usage.
I don't know how much the Go plugin uses, which is how it should be for all software — means usage is low enough I never had to worry about it.
Meanwhile, my small Java projects get OOM killed all the time during what I assume is the compilation the plugin does in the background? We're talking several gigabytes of RAM being used for... ??? I'm not exactly surprised, I've yet to see Java software that didn't demand gigabytes as a baseline. InteliJ is no different btw, asinine startup times during which RAM usage baloons.
Java consumes memory because collecting garbage is extra work and under most circumstances it makes no sense to rush it. Meanwhile Go will rather take time away from your code to collect garbage, decreasing throughput. If there is ample memory available, why waste energy on that?
Nonetheless, it's absolutely trivial to set a single parameter to limit memory usage and Java's GCs being absolute beasts, they will have no problem operating more often.
Also, intellij is a whole IDE that caches all your code in AST form for fast lookoup and stuff like that.. it has to use some extra memory by definition (though it's also configurable if you really want to, but it's a classic space vs time tradeoff again).
Refcounting is significantly slower under most circumstances. You are literally putting a bunch of atomic increments/decrements into your code (if you can't prove that the given object is only used from a single thread) which are crazy expensive operations on modern CPUs, evicting caches.
It absolutely is. There is not many ecosystems where you can attach a debugger to a live prod system with minimal overhead, or one that has something like flight recorder, visualvm, etc.
The mutex case is one where they're using a mutex to guard read/writes to a map.
Please show us how to write that cleanly with channels, since clearly you understand channels better than the author.
I think the golang stdlib authors could use some help too, since they prefer mutexes for basically everything (look at sync.Map, it doesn't spin off a goroutine to handle read/write requests on channels, it uses a mutex).
In fact, almost every major go project seems to end up tending towards mutexes because channels are both incredibly slow, and worse for modeling some types of problems.
... I'll also point out that channels don't save you from data-races necessarily. In rust, passing a value over a channel moves ownership, so the writer can no longer access it. In go, it's incredibly easy to write data-races still, like for example the following is likely to be a data-race:
handleItemChannel <- item
slog.Debug("wrote item", "item", item) // <-- probably races because 'item' ownership should have been passed along.
Developers have a bad habit of adding mutable fields to plain old data objects in Go though, so even if it's immutable now, it's now easy for a developer to create a race down the line. There's no way to indicate that something must be immutability at compile-time, so the compiler won't help you there.
Good points. I have also heard others say the same in the past regarding Go. I know very little about Go or its language development, however.
I wonder if Go could easily add some features regarding that. There are different ways to go about it. 'final' in Java is different from 'const' in C++, for example, and Rust has borrow checking and 'const'. I think the language developers of the OCaml language has experimented with something inspired by Rust regarding concurrency.
You can argue about how likely is code like that is, but both of these examples would result in a hard compiler error in Rust.
A lot of developers without much (or any) Rust experience get the impression that the Rust Borrow checker is there to prevent memory leaks without requiring garbage collection, but that's only 10% of what it does. Most the actual pain dealing with borrow checker errors comes from it's other job: preventing data races.
And it's not only Rust. The first two examples are far less likely even in modern Java or Kotlin for instance. Modern Java HTTP clients (including the standard library one) are immutable, so you cannot run into the (admittedly obvious) issue you see in the second example. And the error-prone workgroup (where a single typo can get you caught in a data race) is highly unlikely if you're using structured concurrency instead.
These languages are obviously not safe against data races like Rust is, but my main gripe about Go is that it's often touted as THE language that "Gets concurrency right", while parts of its concurrency story (essentially things related to synchronization, structured concurrency and data races) are well behind other languages. It has some amazing features (like a highly optimized preemptive scheduler), but it's not the perfect language for concurrent applications it claims to be.
Rust concurrency also has issues, there are many complaints about async [0], and some Rust developers point to Go as having green threads. The original author of Rust originally wanted green threads as I understand it, but Rust evolved in a different direction.
As for Java, there are fibers/virtual threads now, but I know too little of them to comment on them. Go's green thread story is presumably still good, also relative to most other programming languages. Not that concurrency in Java is bad, it has some good aspects to it.
While I agree, in practice they can actually be parallel. Case in point - the Java Vert.x toolkit. It uses event-loop and futures, but they have also adopted virtual threads in the toolkit. So you still got your async concepts in the toolkit but the VTs are your concurrency carriers.
Could you give an example to distinguish them? Async means not-synchronous, which I understand to mean that the next computation to start is not necessarily the next computation to finish. Concurrent means multiple different parts of the program may make progress before any one of them finishes. Are they not the same? (Of course, concurrency famously does not imply parallelism, one counterexample being a single-threaded async runtime.)
Async, for better or worse, in 2025 is generally used to refer to the async/await programming model in particular, or more generally to non-blocking interfaces that notify you when they're finished (often leading to the so-called "callback hell" which motivated the async/await model).
And you would get garbled up bytes in application logic. But it has absolutely no way to mess up the runtime's state, so any future code can still execute correctly.
Meanwhile a memory issue in C/Rust and even Go will immediately drop every assumption out the window, the whole runtime is corrupted from that point on. If we are lucky, it soon ends in a segfault, if we are less lucky it can silently cause much bigger problems.
So there are objective distinctions to have here, e.g. Rust guarantees that the source of such a corruption can only be an incorrect `unsafe` block, and Java flat out has no platform-native unsafe operations, even under data races. Go can segfault with data races on fat pointers.
Of course every language capable of FFI calls can corrupt its runtime, Java is no exception.
From all the listed cases, only the first one is easy to get caught by, even as an experienced developer. There, the IDE and syntax highlighting is of tremendous help and for general prevention. The rest is just understanding the language and having some practice.
As to whether it's a common pattern, I see closures on WaitGroups or ErrGroups quite often:
You can avoid the closure by making the worker func take a *sync.WaitGroup and passing in &wg, but it doesn't really have any benefit over just using the closure for convenience.“A million ways to segfault in C” and its just the author assigning NULL to a pointer and reading it, then proclaiming C would be better if it didn’t have a NULL value like Rust.
I’m mad I read that. I want a refund on my time.
>I have been writing production applications in Go for a few years now. I like some aspects of Go. One aspect I do not like is how easy it is to create data races in Go.
Their examples don't seem terribly convoluted to me. In fact, Uber's blog post is quite similar: https://www.uber.com/blog/data-race-patterns-in-go/
Like, rightly or wrongly, Go chose pervasive mutability and shared memory, it inevitably comes with drawbacks. Pretending they don't exist doesn't make them go away.
> Don't communicate by sharing memory; share memory by communicating.
Anyway, I would stop short of saying "Go chose shared memory". They've always been clear that that's plan B.
It's not like it's a disaster, but it's certainly inconsistent.
The opposite default encourages the opposite behaviour.
Moreover, threads are arguably useless without shared memory anyway. A thread is invoked to work on the same data structure with multiple "affectors". Coordination of these affectors is up to you. Atomics, locks, queues... The tools are many.
In fact, processes are just threads which are isolated from each other, and this isolation is enforced by the processor.
Also, these are minimal reproducers, the exact same mistakes can trivially happen in larger codebases across multiple files, where you wouldn't notice them immediately.
It's generally assumed that people who defend their favorite programming language are oblivious to the problems the language has or choose to ignore these problems to cope with the language.
There's another possibility: Knowing the footguns and how to avoid them well. This is generally prevalent in (Go/C/C++) vs. Rust discussions. I for one know the footguns, I know how bad it can be, and I know how to avoid them.
Liking a programming language as is, operating within its safe-envelope and pushing this envelope with intent and care is not a bad thing. It's akin to saying that using a katana is bad because you can cut yourself.
We know, we accept, we like the operating envelope of the languages we use. These are tools, and no tool is perfect. Using a tool knowing its modus operandi is not "pretending the problems don't exist".
I said that in response to the hostility ("crap on Go") towards the article. If such articles aren't written, how will newbies learn about the pitfalls in the first place?
I'll always love a greenfield C project, though!
The closure compiler flag trick looks interesting though, will give this a spin on some projects.
Subtle linguistic distinctions are not what I want to see in my docs, especially if the context is concurrency.
Which PL do you use then ? Because even Rust makes "Subtle linguistic distinctions" in a lot of places and also in concurrency.
Please explain
It depends on the platform though (e.g. in Java it is guaranteed that there is no tearing [1]).
[1] In OpenJDK. The JVM spec itself only guarantees it for 32-bit primitives and references, but given that 64-bit CPUs can cheaply/freely write a 64-bit value atomically, that's how it's implemented.
Likewise for "Trainings". Looks weird to Murrcan eyes but maybe it's a Britishism.
action/action learnings/learning trainings/training asks/ask strategising/strategy
I'm not sure if the people who use this word think it's proper English. They rarely seem to care what words mean anyway.
However, are Go programs not supposed to typically avoid sharing mutable data across goroutines in the first place? If only immutable messages are shared between goroutines, it should be way easier to avoid many of these issues. That is of course not always viable, for instance due to performance concerns, but in theory can be done a lot of the time.
I have heard others call for making it easier to track mutability and immutability in Go, similar to what the author writes here.
As for closures having explicit capture lists like in C++, I have heard some Rust developers saying they would also have liked that in Rust. It is more verbose, but can be handy.
C programmers aren’t supposed to access pointers after freeing them, either.
“Easy to do, even in clean-looking code, but you shouldn’t do it” more or less is the definition of a pitfall.
https://www.reddit.com/r/rust/comments/1odrf9s/explicit_capt...
I recently worked with a 'senior' Go engineer. I asked him why he never used pointer receivers, and after explaining what that meant, he said he didn't really understand when to use asterisks or not. But hey, immutability by default is something I guess.
That said, i think about all languages have their own quirks and footguns. I think people sometimes forget that tools are just that, tools. Go is remarkably easy to be productive in which is what the label on the tin can claims.
It isnt “fearless concurrency” but get shit done before 5 pm because traffics a bitch on Wednesdays
To feel productive in.
Races with mutexes can indicate the author either doesn't understand or refuses to engage with Go's message based concurrency model. You can use mutexes but I believe a lot of these races can be properly avoided using some of the techniques discussed in the go programming language book.
He complains that language design offers no way of avoiding it (in this particular case) and relies only on human or ide. Humans are not perfect and should not be a requirement to write good code.
I feel like Java's IDE support is best in class. I feel like go is firmly below average.
Like, Java has great tooling for attaching a debugger, including to running processes, and stepping through code, adding conditional breakpoints, poking through the stack at any given moment.
Most Go developers seem to still be stuck in println debugging land, akin to what you get in C.
The gopls language server generally takes noticeably more memory and cpu than my IDE requires for a similarly sized java project, and Go has various IDE features that work way slower (like "find implementations of this interface").
The JVM has all sorts of great knobs and features to help you understand memory usage and tune performance, while Go doesn't even have a "go build -debug" vs "go build -release" to turn on and off optimizations, so even in your fast iteration loop, go is making production builds (since that's the only option), and they also can't add any slow optimizations because that would slow down everyone's default build times. All the other sane compilers I know let you do a slower release build to get more performance.
The Go compiler doesn't emit warnings, insisting that you instead run a separate tool (govet), but since it's a separate tool you now have to effectively compile the code twice just to get your compiler warnings, making it slower than if the compiler just emit warnings.
Go's cgo tooling is also far from best in class, with even nodejs and ruby having better support for linking to C libraries in my opinion.
Like, it's incredibly impressive that Go managed to re-invent so many wheels so well, but they managed to reach the point where things are bearable, not "best in class".
I think the only two languages that achieved actually good IDE tooling are elisp and smalltalk, kinda a shame that they're both unserious languages.
Okay, come on now :D Absolutely everything around Java consumes gigabytes of memory. The culture of wastefulness is real.
The Go vs Java plugins for VSCode are no comparison in terms of RAM usage.
I don't know how much the Go plugin uses, which is how it should be for all software — means usage is low enough I never had to worry about it.
Meanwhile, my small Java projects get OOM killed all the time during what I assume is the compilation the plugin does in the background? We're talking several gigabytes of RAM being used for... ??? I'm not exactly surprised, I've yet to see Java software that didn't demand gigabytes as a baseline. InteliJ is no different btw, asinine startup times during which RAM usage baloons.
Nonetheless, it's absolutely trivial to set a single parameter to limit memory usage and Java's GCs being absolute beasts, they will have no problem operating more often.
Also, intellij is a whole IDE that caches all your code in AST form for fast lookoup and stuff like that.. it has to use some extra memory by definition (though it's also configurable if you really want to, but it's a classic space vs time tradeoff again).
Also, I don't know how it's relevant to Go which uses a tracing GC.
Please show us how to write that cleanly with channels, since clearly you understand channels better than the author.
I think the golang stdlib authors could use some help too, since they prefer mutexes for basically everything (look at sync.Map, it doesn't spin off a goroutine to handle read/write requests on channels, it uses a mutex).
In fact, almost every major go project seems to end up tending towards mutexes because channels are both incredibly slow, and worse for modeling some types of problems.
... I'll also point out that channels don't save you from data-races necessarily. In rust, passing a value over a channel moves ownership, so the writer can no longer access it. In go, it's incredibly easy to write data-races still, like for example the following is likely to be a data-race:
Developers have a bad habit of adding mutable fields to plain old data objects in Go though, so even if it's immutable now, it's now easy for a developer to create a race down the line. There's no way to indicate that something must be immutability at compile-time, so the compiler won't help you there.
I wonder if Go could easily add some features regarding that. There are different ways to go about it. 'final' in Java is different from 'const' in C++, for example, and Rust has borrow checking and 'const'. I think the language developers of the OCaml language has experimented with something inspired by Rust regarding concurrency.
A lot of developers without much (or any) Rust experience get the impression that the Rust Borrow checker is there to prevent memory leaks without requiring garbage collection, but that's only 10% of what it does. Most the actual pain dealing with borrow checker errors comes from it's other job: preventing data races.
And it's not only Rust. The first two examples are far less likely even in modern Java or Kotlin for instance. Modern Java HTTP clients (including the standard library one) are immutable, so you cannot run into the (admittedly obvious) issue you see in the second example. And the error-prone workgroup (where a single typo can get you caught in a data race) is highly unlikely if you're using structured concurrency instead.
These languages are obviously not safe against data races like Rust is, but my main gripe about Go is that it's often touted as THE language that "Gets concurrency right", while parts of its concurrency story (essentially things related to synchronization, structured concurrency and data races) are well behind other languages. It has some amazing features (like a highly optimized preemptive scheduler), but it's not the perfect language for concurrent applications it claims to be.
As for Java, there are fibers/virtual threads now, but I know too little of them to comment on them. Go's green thread story is presumably still good, also relative to most other programming languages. Not that concurrency in Java is bad, it has some good aspects to it.
[0]: An example is https://news.ycombinator.com/item?id=45898923 https://news.ycombinator.com/item?id=45903586 , both for the same article.
That's not the case with Go, so these are significantly worse than both Rust and Java/C#, etc.
Of course you can have memory corruption in Java. The easiest way is to spawn 2 threads that write to the same ByteBuffer without write locks.
Meanwhile a memory issue in C/Rust and even Go will immediately drop every assumption out the window, the whole runtime is corrupted from that point on. If we are lucky, it soon ends in a segfault, if we are less lucky it can silently cause much bigger problems.
So there are objective distinctions to have here, e.g. Rust guarantees that the source of such a corruption can only be an incorrect `unsafe` block, and Java flat out has no platform-native unsafe operations, even under data races. Go can segfault with data races on fat pointers.
Of course every language capable of FFI calls can corrupt its runtime, Java is no exception.