Which part of the example go code looks synchronous? It’s using a callback function. That’s a hallmark of async code, nothing invisible.
I also can’t quite grasp the comparison with Java. It’s passing a class instance to the server handler, and that could be doing anything. Nothing there tells you how async or not the server implementation is, and that `os.write()` call might be using a thread pool underneath.
It's not about the callback at all, the example is poor and the explanation is lacking. In Go, many standard I/O functions use async I/O under the hood. When you call such a function (inside the handler callback, that is), the current goroutine is suspended yielding to other goroutines, so waiting for I/O doesn't block the OS thread inside the request handler. It's basically what you'd do in C# with async/await, except it's implicit and you don't have to mark functions as asynchronous manually. I.e. you write simple synchronous code and Go makes it asynchronous under the hood. There are several disadvantages, though: it's not tunable, and if there's a CGO call (call to a C func) which uses I/O itself it will block an entire worker thread because the Go runtime isn't aware of any of it.
As far as I know, Java's new virtual threads have the same idea.
In the case of a CGO call you could prefix the enclosing function definition with the "go" keyword to manually initiate a new goroutine? Am I understanding this correctly?
Goroutines are multiplexed onto a finite number of OS threads, which is usually a pretty small number. If you launch a new goroutine, a funky CGO call will still block an entire OS thread, it won't change anything as far is blocking is concerned. When all OS threads are blocked the runtime will spawn additional OS threads, so in the worst case it can degenerate to "OS thread per request".
>Nothing there tells you how async or not the server implementation is, and that `os.write()` call might be using a thread pool underneath.
It depends on the contract of the method. If it promises to throw an exception if the call failed, then the current thread of execution can't proceed before the call returns, or it would break the semantics. So even if it uses a thread pool, the current thread will have to block, waiting for some acknowledgement by os.write(), wasting an entire OS thread. As far as I know, Go won't block, the scheduler will just switch to a different goroutine in the meantime. Java's new virtual threads will allow to have mechanism similar to Go's.
agreed. the examples don’t show the real pain. it is when every method returns Future or Promise. and you have to map each result. and many languages don’t have good support for that so you end up with lots of nesting. writing synchronous function signatures is much simpler to maintain.
there’s 2 language level syntax features i see that make this better:
one is scalas for comprehension. this lets you write async code that reads like imperative synchronous lines. the other is async/await, which let’s you write synchronous code that behaves like async
The comparison with Java is awful. Especially the performance comparison since the sun http server isn’t meant to be used and isn’t a part of the language spec.
> TL;DR The Go standard runtime allows for asynchronous coding while allowing the developer a simply synchronous interface.
I didn't read the article but this TL;DR captures the essence of simplicity of programming in Go when writing IO apps. Go essentially optimizes for the majority case: run these things one after another. All the await/async fluff in other languages is evidence of poor/old language design. Every non-trivial production code you see these days in Javascript is littered with await/async as is the case in Python, etc. Why the heck do I have to repeat a keyword 1000s of times when that's expected in majority of languages.
Yes, Go may not cover every case perfectly (e.g. mentioned by sibling re CGO blocking OS thread), but when you look at real world stats of how often this is an actual issue, you find that it's almost noise (i.e. do a code search across Github for instance for such workarounds).
My impression of the article is just that the author is a fan of green threads (goroutines). Green threads don’t really make sense without an integration into some kind of IO runtime, so calling out how Go does do this seems moot. Other languages with green threads also with this way (Julia comes to mind).
The article is not very well informed. It doesn't even make a distinction between os threads and green threads.
> To get this performance out of Java you would need to add threadpools, futures or some other async library.
What Java needs to be as efficient is the work of Project Loom: Fibers and Continuations. Go also has value types (e.g. arrays and nested structs with inline memory layout) that's the work of Java's Project Valhalla.
goroutines aren't really green threads, they're more lightweight than conventional green thread implementations due to having a segmented stack. Millions of goroutines can run comfortably on a standard machine. This isn't possible with any green thread implementation I've seen.
Unfortunately the batteries-included runtime of Go effectively forecloses any really high performance server code that scales well to huge machines. What it offers is moderately good and that's all you'll ever get. A Go web server will work OK on 8 cores and it will only go a bit faster on 32 cores and not at all faster, possibly slower, on 256 cores. Meanwhile Java async programs are still scaling up. These results are apparent if you look at the gRPC multi-language benchmarks.
There's only one way to write a server in Go and that has pros and cons.
Thanks for sharing this insight. Is this a issue with just the web server.
If I have a standalone task, which tires to use 128 cores and manages parallel tasks using goroutines communicating via channels - will I be able to successfully leverage 128 cores.
If your tasks are large enough to dominate, then the non-scalability of the goroutine scheduler and the network stack won’t be an issue. For example, if your task is compressing some huge hunk of a video and it takes a second to do it, then it will scale fine. Then again, you can get this scalability from anything, including `xargs`.
It seems to me like the go ecosystem would rather have a less performant language if it means more similar code between projects.
Take for example json: Go's stdlib json implementation is wildly slow. It has to be implemented in reflection, it can't use any code-generation anything, and for backwards compatibility it can never trim any feature, no matter whether it has a performance implication for all users of the package. It's well known to be a slow implementation.
In java, json isn't part of the stdlib. There's jackson if you want one flavor of fast json, jetty ships its own json serializer, etc.
This means that if you pick up a new java codebase written in a different framework, it's possible you have to learn the right pattern to json serialize things.
By nature of these third party libraries in java being out of the stdlib, they've been able to grow and become more performant and compete with each other.
Go sits on the opposite side. The stdlib http implementation is a little slow. The stdlib json implementation is incredibly slow. No one wants to build these features outside of the stdlib though, and people would rather every codebase have the same looking go code, even if that means the actual code itself is slower.
> Go sits on the opposite side. The stdlib http implementation is a little slow. The stdlib json implementation is incredibly slow. No one wants to build these features outside of the stdlib though, and people would rather every codebase have the same looking go code, even if that means the actual code itself is slower.
What? There are multiple json parsing and http server implementations for Go.
Obviously what you say is true, but there is a cultural element to this.
Java has a library ecosystem that is stable but not ideological, unlike golang.
I think of golang as the opposite of "The Curse of Lisp" [1], wherein its so standard that it can't fit the evolutionary cracks as well as a less opinionated framework. The ugliness of Java has been its strength. Whether it's Hotspot or ZGC or arch support or whatever, the JVM bytecode and rather limited capabilities of the language forced more focus on the objective quality of the artifacts of the language rather than the language itself. A lot of those artifacts continue becoming more impressive over time.
They exist (fasthttp and fastjson for example), but no one actually uses them.
I should have written "no one wants to use third-party versions of these packages", and I don't think that small semantic issue changes my comment meaningfully.
Do you take issue with the way I'm presenting the go ecosystem other than that small pedantic point?
The two companies I worked in (and still work as of now) both were using fasthttp and non-stdlib json parser (jsoniter and jstream to be exact).
In fact in my experience - nobody uses stdlib implementations outside of projects were stdlib is actually enough (and that's a lot of projects actually) and temp\prototype projects.
People do use them, though. Look through the fastjson dependents on GitHub[1] and you'll find serious projects from the Kubernetes special interest groups and various blockchains. Heck; elastic even built their own for beats[2]. There's no big philosophy difference here. Most go programmers choose encoding/json over fastjson for the same reason most Jackson programmers choose databind over streaming: it's a simpler API.
> I should have written "no one wants to use third-party versions of these packages", and I don't think that small semantic issue changes my comment meaningfully.
No, they don't. But if performance becomes an issue, they can!
> Do you take issue with the way I'm presenting the go ecosystem other than that small pedantic point?
Well, yes, a little. The comparison you're making is presented as "The Java ecosystem has a better outcome than the Go ecosystem". TBH, that's only for performance, and is basically a non-issue if the performance poor.
IOW, you're doing the ultimate in premature optimisation. In the real world, people who run into performance problems will a) run into them sooner with Java, and b) solve them the same way in Go.
The fact that callbacks in Go may be on a different thread means it's so damn easy to use a variable reference from an outer scope within in a callback. This increases the chances that someone inadvertently causes a race condition. Something that requires additional awareness and screening during code reviews.
This by itself isn't an indictment of the language as threading is essential to quality GUI applications and as such Go is a great language for writing client applications - which it is largely not used for.
You really need to evaluate the value the overhead Go's thread management brings when used in the context of web servers - where we typically split processes up using container orchestration, making them functionally threaded any way.
Comparatively, TypeScript running on Node or Deno is single threaded yet asynchronous. Using it as a web server means you have a better type system which follows a similar structural typing model to go (interfaces and such) and you don't have to screen for race conditions. In K8 or ECS, you spawn as many processes as you have CPUs and Bob's your unkle.
Personally, I would love it if Go focused on GUI application development but I can _bearally_ write GTK apps with it. Windows and MacOS applications are near-to or impossible to write. Don't even talk to me about Web Assembly. Such a missed opportunity.
That’s a weird critique of Go: you make it sound as if you must use threads. But nothing stops you from using callbacks and promises (which have become a bit more comfy with the addition of generics).
And if the there’s one domain where I see an advantage in using inheritance, it’s GUIs.
Well, golang.org does specifically call this out at the top:
> Built-in concurrency and a robust standard library
I think that the joy of Go's async-first design really makes sense most to people who have experienced the awful pains of colored async-await in other languages.
I think what the author is saying is that go has very good async support, but it is no less visible than it is in java, from what I can see. Certainly better syntax, but still visible.
I am working on a front end scripting language that does try to make this stuff invisible:
What are the major issues with this concept? I often wonder why async isn't the standard and we have to manually decide to use it (what if a function we thought would be fast actually takes long?).
Considering that it seems to be a "best practice" to use async functions whenever remotely required, it would be bad developer experience (and lead to unwanted results) to make people do extra work to use it.
And what major issues and downsides this has. One I can identify it would be non obvious on how to do `map` on a Future/Promise, because there is none. So there is no easy way to execute on the green thread code with the result of the remote call, e.g.
let f = asyncFuture()
f.map(|v|: v + 1) // executes also async
let s = await m
println(s)
If f is transparently handled as a future, this is no longer possible, we would have:
let s = asyncFuture()
s = s + 1 // not async
println(s)
One would also, to be safe, always use immutable data structure and copy semantics on other classes (e.g. functional lenses and prisms everywhere transparently) - with the performance impact of immutable data structures.
The issue is that the moment you cross the boundary into some other language (and note that this usually includes the APIs provided by the OS!), the caller and the callee have to agree on how green threads work.
It's not an issue with explicit async/await model, because that one desugars into callbacks, which can be described entirely in terms of the already-existing C ABI that is the baseline for FFI in any modern language.
i think it is because async came so much later in the life of javascript, and there are some pretty serious perf implications to do it completely transparently
To an extent yes, but consider the context: front end scripting
The javascript threading model is that there is a single thread executing so within a given bit of code you can be reasonably sure things are safe. The hyperscript runtime only allows one instance of a given event handler to execute at a time (unless otherwise specified) so the primary source of concurrency issues (the same code executing concurrently) is typically not an issue. By structuring your code such that it is on the same element (perhaps triggered by events elsewhere) you can effectively synchronize code in an intuitive manner.
I certainly wouldn't recommend the language for the back end though!
My team just spent three weeks hunting down a race condition in JavaScript integration tests. Turns out it was a missing `await`. So, I wouldn't claim that JavaScript is anywhere near safe :)
Admittedly, this was back-end, so it's possible that the situation is simpler in the front-end.
Data races happen when there are multiple writers/readers of the same data. If one virtual thread of execution is multiplexed on several OS threads/workers/callbacks/you name it, there's still only one reader/writer at a time, because different parts of a multiplexed function don't run at the same time, they go one after another sequentially as before, with the difference is that there are now implicit continuation points where execution can hop from thread to thread (callback to callback). Can't say for hyperscript, but generally you need to protect your data with the same synchronization primitives as before, such as locks for example. It's only incompatible with thread locals and thread affinity, I'm not sure if JS has those.
If your data is composite (i.e. a JavaScript object or array) and you have several Promise-based flows of execution (technically not a virtual thread by most definitions of the term, but still a scheduled flow of execution) that end up reading/writing sub-parts of your piece of data in unpredictable order. That's basically a data race, in JavaScript. I've had to debug some of those.
As a side-note, some forms of data races (for perhaps a slightly looser definition of the term) can even happen without explicit asynchronicity, just with callbacks/synchronous events.
It isn’t that it’s invisible. It’s just that you didn’t get it until now.
I am not saying that to belittle the author. This is more an observation of how programming seems to have gone from understanding both yourself and what you are doing to shopping bits and pieces and then gluing them together.
Not really, "invisible" is a fair way to describe complexity that developers never interact with. Especially when most other popular languages forces developers to deal with irritating async/await bookkeeping when it comes comes to concurrency.
How so? The point of async/await is to reduce bookkeeping. The Go model is at its core an implementation of M:N user threads, and it creates plenty of complexity wrt. e.g. interfacing with other languages. Which is why M:N threading was mostly abandoned elsewhere.
> it creates plenty of complexity wrt. e.g. interfacing with other languages
Hmm I'm not sure I get this point. Do you mean in comparison with async? Async cross language interfaces should need the same type of continuation mechanisms (eg poll or completion based interop with an event loop) as green threads, no? I think of async as a primarily syntactically explicit construction, while both look similar in ABI, with a virtual stack. If not, I'd love to know why.
Async with explicit await is basically just syntactic sugar for continuation callbacks. The "virtual stack" that results can thus be expressed entirely in terms that the C ABI understands: a callback is just a C function pointer + pointer to the struct representing the virtual stack frame.
Sure, but then the callee is responsible for any async io anyways - meaning it needs to either run its own event loop or use a thread pool if it wants to do IO, right? What I mean is that without a protocol/ABI to access the caller's runtime, you'd have to use your own.
For instance, Rust has explicit async/await but the poll method has a pointer to a waker, and a non-standard global/thread local variable is used to communicate with the runtime. Specifically it needs to access the reactor (in Rust lingo).
It depends. You can treat it as implementation detail, with multiple loops if needed. Or you can use a shared event loop propagated over the same C ABI.
Either way, this all maps nicely to C FFI. But true green threads don't - since every frame is interruptible by default, all languages involved must use the async stack for all functions, in a way that's compatible with them all. Like tail calls, this is an ABI-level thing that you can't slap on top of the existing C ABI.
Ah! Finally I think I'm getting the distinction, thanks for being patient. I also agree about the ABI issue - explicit callbacks is much simpler.
It appears as Go doesn't expose green threads as per your definition for it's FFI though, but rather an even simpler synchronous interface in both directions. This makes it impossible to interface with the event loop, and one must use threads. For Go->C, at least, this should not block the runtime since the Go runtime spins up more threads in case of long running synchronous calls (their version of pre-emption).
For an opposite example, consider async/await in C# - you're not restricted to .NET when using it; it also supports e.g. the native async OS APIs in WinRT (which are callback-based on ABI level: https://docs.microsoft.com/en-us/uwp/api/windows.foundation....). Ditto for co_await in C++20.
Go's approach leads to an insular ecosystem that tends to reimplement everything, because that's the only way to get this transparent async throughout.
Except, you never interact with the (os) threads. So it isn't complexity that the programmer deals with. Async/await also doesn't address data sharing, so we're not even talking about the same thing.
You can implement async/await-like code that emulates other languages in Go, but it would be a somewhat pointless exercise since you have channels, which offer a much safer and better mechanism for communicating state.
Go is marketed as having a programming model that is heavily influenced by CSP. This is one of the big selling points for Go and it is at the core of how it offers concurrency. It is hardly some footnote that is hidden away. It is pretty much in your face.
This type of synchronous code is most impressive to those who come from JavaScript where everything is clearly asynchronous. For instance, in Go you can read a file synchronously using os.ReadFile and it will generally be performant. In NodeJS, you need a callback to achieve the same result.
I kind of disagree with the article's premise - that the fact that Go uses async/await like Js and C#, but presents them as green threads is a good thing.
Imo it creates false expectations in programmers, who are surprised, that long-running compute code doesn't get preempted, since the compiler doesn't insert preemption points.
It also robs us of the possibility, of writing event-driven UI applications, where one can make an asynchronous call on the UI thread safely, which will get marshalled back to the UI thread.
Having used both languages for many years, it was unexpectedly irritating to pick up Java again after doing golang for a few years. So much unneeded complexity to do simple things.
While not strictly the language itself, I find the Java standard library API to be unnecessarily verbose, with often obtuse defaults.
Go’s library is sometimes overly terse, but after having experienced both in production, I much prefer Go’s style.
An example is one of the date parsing functions (I can’t remember details now) which will happily accept an invalid date string (eg 2021-02-35) and silently convert it to a valid date (2021-03-07) despite declaring a parsing Exception.
You see, you only get the exception if you call setLenient(false) or if the date string can’t be coerced. For the life of me, I cannot think of a use case for this behaviour, and it certainly violates the principle of least surprise.
I think Java has one of the best standard libraries out of any language I used. Sure, it has its warts (but the Date class should really not be used anymore over its more modern replacements, which are again, really sane), but it doesn’t make you reach for third party dependencies at every turn, neither for lack of feature nor lack of performance. Sure, you can use hand-tuned collection libs, but it is very seldom needed.
Crappy apis is not beholden to java. Have you looked at go's timer API? Try to re-use one without inadvertently causing a data race, or get wrong behaviour.
I am far from a Java guy, but that seems like a poor example. Functional programming is not in the wheelhouse of Java. In the same way, it would be unfair to criticize Haskell for its object-oriented style.
It really is not that bad, and it is at least possible, both to incorporate it partly (stream api, immutable objects, especially with records) or go full-on with vavr.
As to workarounds for inheritance in Go, that sounds like trying to write in another language but in Go, which is of course not advisable in any language.
> Even the author of Java, James Gosling, disagrees with you.
From the article:
> Interface inheritance (the implements relationship) is preferable. You should avoid implementation inheritance whenever possible.
Key words: preferable, and whenever possible. It's sometimes preferable to have inheritance and not possible to leave it out without unneeded extra complexity.
Furthermore, nowadays interfaces in Java (as well as Kotlin, Scala, C# - all of which are much more expressive and have superior modeling capability than golang) can have default implementations, which increases their application even more and reduces the need for explicit inheritance. Not so in golang since interfaces cannot have default implementations there.
> As to workarounds for inheritance in Go, that sounds like trying to write in another language but in Go, which is of course not advisable in any language.
Not quite, it's just where having inheritance would have simplified the design, and improved readability.
He quite clearly states in the article he would ‘leave out classes’ (i.e. implementation inheritance). You didn’t quote the clearest statement in the article, just preceding your excerpt!?
Again on interfaces, sometimes less is more. Many people prefer the inverted interfaces of go, declared at point of use. It’s fine to prefer the opposite, but these are deliberate choices, not mistakes or failures.
Re inheritance simplifying a design, it does the opposite in my experience, unless by simplify you mean hide program flow and state.
The implication of "whenever possible" is that an OO language should strive to maximize this metric - for example, by making composition easier (e.g. providing syntactic sugar for "implement this interface by delegating all methods to this object").
But yes, Go is definitely worse in that respect, not better.
Yeah; really hard to guess at the effect a feature that hasn't been finalized, has been in progress for years, and has yet to land in a release in any form, will actually have in an environment that is as slow to roll out updates and adopt new features as enterprise Java dev tends to be.
Optimistically we may be able to answer op's question sometime in 2024.
The author is right in principle, i I think Golangs async scheduler which automatically moves blocking code outside Golangs goroutine scheduler is amazing, but his code and explanations are all wrong. also "Java’s green threads" shows a deep lack of understanding.
The author shows a junior level of understanding which co tradicts "I’ve worked professionally with Go for a couple of years"
If you want to show a senior level of understanding coding discussions, please understand that you always should write with a scientific mindset.
This means: not only say "x is good" or "x is bad" without explanation like a little child - but also deliver the source of your knowledge or the proof.
Then your contributions will be seen as mature additions to collective wisdom.
Every operating system kernel also presents a synchronous interface to asynchronous I/O. What Go provides is more efficient multithreading in certain cases, primarily because the kernel scheduler doesn't need to be involved on context switches. But this could be fixed at the kernel level with something like Windows' fibers or Google's SwitchTo patches.
Just looking at the numbers, Go is 71% faster. That could be due to the Go HTTP library being more efficient than the Java HTTP library, rather than language or runtime features.
Nobody uses that internal Oracle HTTP server - it is not meant for production. It isn't even in the Java namespace -your static analysers will complain about using 'com.sun.*' packages.
Use the Java Jetty server and Go will hide itself in a corner in shame when benchmarked.
Hmm. If you pack real blocking code (instead of writing a couple of bytes) into Go's handler it would definitely turn into crippled turtle under high concurrency load. There is no magic in Go's runtime.
I also can’t quite grasp the comparison with Java. It’s passing a class instance to the server handler, and that could be doing anything. Nothing there tells you how async or not the server implementation is, and that `os.write()` call might be using a thread pool underneath.