They also make use of constructs that are overkill for such simple examples. For example, the first example could be written as:
Thread.startVirtualThread(() -> say("world"));
say("hello");
which would also replicate same concurrency bug as in the original Go program (the Java example, as written, does not have that bug, but neither would var t = Thread.startVirtualThread(() -> say("world"));
say("hello");
t.join();
).Go makes certain concurrency bugs easy to introduce and hard to find, while Java helps avoid them, but that is also not easy to see from these particular examples.
Disenchanted to hear (some) concurrency bugs easy to introduce and hard to find in Go. Could someone mention how does this compare to Elixir?
These cases work flawlessly in Go, in my experience:
* Batch 10 tasks and run them concurrently (using WaitGroups).
* Locking resources while reading and writing with a mutex like sync.RWMutex, using .Lock() and .Unlock(), often together with defer.
* Locking resources while only reading with a sync.RWMutex, using .RLock() and .RUnlock(), often together with defer.
* Running code "in the background" with goroutines, ie. go func() { <code goes here> }().
* Detecting race conditions by building with go build -race and then running the program.
However, when using channels, select, closing channels, waiting on channels in a for loop etc, things can easily become more subtle.
On a general basis, I disagree that concurrency bugs are easy to introduce and hard to find in Go. However, pure functional programming languages like Haskell, with little state, will always have the upper hand when it comes to concurrency.
See also:
https://gobyexample.com/waitgroups
https://gobyexample.com/mutexes
I would have used .startVirtualThread more if I wasn't under the impression that I needed to do Thread.currentThread().interrupt() when catching and re-throwing as a RuntimeException.
So I was choosing between the noise of that in the examples and the noise of the executor + submit with a lambda that returned null.
For me Golang has far weaker and less flexible error handling semantics.
And concurrency is more verbose in Java but it's easy enough to abstract it behind a wrapper.
Where as you can't escape Golang's error handling.
I do believe you probably have more control over Java's threads, making them a bit more verbose. But, both Java itself - as you mention - and languages built on top of the JVM have ways to make it less verbose.
While in Java, it tries to mostly keep the existing Java syntax.
I guess people that write Java are used to a lot of "lasagna" all-is-object boilerplate and ignore it. The same way go people are used to ignore the `err != nil` lines everywhere.
I do however wonder, now, what I’m missing out on with other languages, particularly swift and rust.