> With Go it doesn't matter if an operation is blocking or non blocking, that fact can totally be abstracted from the client code.
No, it can't, and pretending that it can is misleading in a way that allows large teams of developers to cause themselves real problems.
The only sense in which this is true is that you can write a Go function with a simple signature like this:
func DoSomeStuff (error) {}
And the caller has no idea whether this involves concurrency under the hood (e.g. the function can spawn its own goroutines and channels as it sees fit).
But, make no bones about it: this function blocks until it completes. This means that while this function may do concurrent work it is absolutely a blocking function. This is fine: sometimes blocking functions are good. But you cannot write a non-blocking Go function in the same way.
To make a function non-blocking you can return a channel out of it for the return value to appear on, like this:
func DoSomeStuff (chan error) {}
In this model the caller really doesn't have to care whether the function is synchronous or not (though the fact that it was written this way strongly suggests that it is going to return asynchronously, or at least that the developer believes it will have to in the future).
Except...that return value just there? That's a Future. It's a terrible, half-implemented version of a Future, but that's exactly what it is. It's a promise to return some kind of result at some point when the underlying process has returned.
And if you don't want to block your current goroutine, you cannot block on that channel receive either. That means that you need a callback. There are two patterns for doing that: you could have some kind of central loop that selects over all channels like this and calls the callback functions (boy that looks a lot like Node's event loop, doesn't it!), or you can manually spawn your own callback functions in their own goroutines. Either way, you have callbacks and futures here: you're just building them yourself and calling them something different.
There are lots of good reasons to switch to Go: it's a language that makes lots of developers remarkably productive, it has an ingrained philosophy of building concurrent programs, it's pretty damn fast, and it runs on all kinds of awesome platforms. But claiming that Go has learned something magic and new about how to write concurrent software in such a way that you don't have to care whether your code is async or not is just not true: you always have to care.
> But you cannot write a non-blocking Go function in the same way.
The caller can make function "non-blocking" by wrapping the call in a goroutine themselves. (There's some subtle differences, but they are mostly irrelevant here). For this reason, I'd say there is (almost) no reason to introduce asynchrony in your API in the way you suggest. The rest of your post built on this example seems shaky to me, since it seems built on an example API that doesn't need to exist.
I'd say that "you don't have to care whether your code is async or not" is a overstating the case. I would append the qualifier "unless you're introducing concurrency". Considering that almost no low-level APIs are asynchronous, this usually happens rarely (or happens in low-level code like the HTTP server). Examples that have come up for me: making N parallel RPCs, writing a TCP server. In those situations, you care about async vs not.
In event-loop based systems, it seems like async is in my face all the time, even when doing things that are entirely sequential.
> The caller can make function "non-blocking" by wrapping the call in a goroutine themselves.
Sure, but if they want the return value then either they need to construct the Future-y wrapper I just described or they need to assemble it together in a collection of other function calls wrapped inside a function that itself is either Future-y or uses a long-lived channel to communicate results.
It is not novel to build up a non-blocking system from purely blocking method invocations. We've been doing that for years: it's called threading. Doing things this way has many advantages when written with appropriate diligence, and I'm not pretending otherwise. However, if you actually care about communicating between these arbitrary threads of execution than you either need Futures or queues (both of which are essentially just channels in Go), and at this point you've got the exact same problems as you get in NodeJS or any other asynchronous programming environment.
> The rest of your post built on this example seems shaky to me, since it seems built on an example API that doesn't need to exist.
I don't think that's fair: as I mentioned above, the fact that you as library author would not write the Future-y extension doesn't mean that the Future-y extension isn't built: you just force your caller to build it. That's fine, it's a perfectly good architectural decision (probably you should't be making those decisions for your user), but it doesn't remove the problem.
> I'd say that "you don't have to care whether your code is async or not" is a overstating the case. I would append the qualifier "unless you're introducing concurrency".
Sure. The thing that matters here is that Node is always introducing concurrency, because Node is concurrent. This is why all Node programs have to care about concurrency: they are all concurrent because their system is concurrent.
This is desperately inconvenient for many one-off programs, which is why I personally don't use Node for anything like that: I'd much rather use Python or Rust or Go. But that was never my argument. My argument was about OP's assertion that "with Go it doesn't matter if an operation is blocking or non blocking, that fact can totally be abstracted from the client code. Writing callbacks is tedious, promises are tedious, co routines with yield need plumbing and async isn't in the spec."
The first sentence is dangerously misleading (while technically true, any system that does that is usable only in that one context), and the second one misses the point, which is that those things get effectively built anyway in any moderate-scale concurrent system in Go.
But my biggest point is this: Go isn't magic in regard to concurrency, and there is a weird amount of magical thinking around Go. Go is a very good language with a lot to like, and I like it quite a lot. But when boiled down to it, Go's concurrency model is threads with a couple of really useful primitives. And that's great, and it works really well. But it's not new or novel.
The sentence "with Go it doesn't matter if an operation is blocking or non blocking, that fact can totally be abstracted from the client code" is equally true if you replace "Go" with "C", or "Python", or "Java", or any language with a threaded concurrency model. There's no magic here. It's the same building blocks everyone else is using.
Go isn't just a threaded concurrency model, it uses an M:N greenthreads pattern. Also, when you say that Go I/O operations are blocking, it is true that they'll logically block a goroutine. However, under the hood, it uses the same libuv-style async IO (or IOCP on Windows) that Node does. An operating system thread doesn't get blocked; the goroutine is "shelved" and woken up again when the I/O is complete. It accomplishes the same kind of thing as Nodejs does, it just abstracts the async nature of the IO away from the programmer. I have to say I like it: procedural execution is easier to reason about.
Honestly, I think the distinction between M:N threading and straight OS threading is pretty minor. It grants some advantages to the language runtime: it can control the stack size, for example. But in terms of how it affects the development style and what kinds of bugs it encourages/discourages I don't think it dramatically differs from the OS threading model.
It is categorically different. OS threads are orders of magnitude more expensive, which makes them a nonstarter for most problems that are a good fit for lightweight conceptual concurrency.
As I said above, green threading has advantages over OS threading, but they behave exactly the same in terms of design patterns and potential bugs.
This is what I was getting at when I said "not that different": compared to the difference between event-loop concurrency and threaded concurrency, M:N green threading is basically just a subcategory of threading.
I believe that depending on the system call the thread handling the call could block, but it's not the same thread developer Goroutines are running on. Yeah though, same as nodejs.
The core problem here is that the Node community invented/popularized a connotation of "blocking" and "non-blocking" that is excessively event-loop-specific. The important difference in their connotation is that code that blocks blocks the whole OS process. The conventional meaning of the term referred just blocking the running thread.
In normal Go, nothing is blocking in the Node sense. (Oh, if you put your mind to it you can manage it, but I've never once encountered this as an practical problem, either in Go or the equivalents you can do in Erlang if you put your mind to it.)
This has profound changes on how you write code.
It's true, Go is not magic. It's just another threaded language in most ways, with the "real" magic in the community best practices around sharing by communicating instead of communicating by sharing. In theory, you could write an equivalent set of C libraries and get most of the same things, but you'd have a lot of library to write. (This is why things like porting C goroutines to C have a hard time getting traction. It can be done, but it's actually the easy part. Also, you'd still be in C, which is its own discussion. But you can get the concurrency.)
The real issue here isn't that Go is necessarily exceptionally strong at concurrency, the real issue is that Node is exceptionally weak. It introduces this new concept of "blocking" that only exists in the first place because it is weak, and then makes you worry about it continuously, to the point that many people seem to internalize the concept as what concurrency is, when it isn't. It's really just something Node laid on you. So when you step out of Node, and you see a community that isn't visibly as worried about "blocking" as the Node community, someone trained by Node thinks they are seeing a community that "isn't good at concurrency". My gosh! Look how cavalier they are about "blocking"! Look how they tell people not to worry about it, and how casual they are about having users wrapping library code in goroutines and explicitly telling library writers not to do the concurrency themselves. But what you're seeing is what happens when you simply no longer have the problems Node and "event-based code" brings to the table. Go is not magic in the general case, but, honestly, when someone coming from the Node world picks up Go, I can see why they might go through a period where they sort of think it is. There are really differences in code style, and how easy it is to write correct code.
You have to make sure you're not letting the limitations of one connotation of "blocking" spill over into the other, or you will have problems. (True in both directions.)
To speak to someone else's point, "futures" in Go don't "suck", they basically don't exist. If you're writing in a recognizably "futures" fashion, you are not writing idiomatic or even particularly good Go. You don't need futures, because (what are today called) futures are basically an embedding of a concurrency-aware language into a non-concurrency-aware language, and you don't need them when the language you're working in is already concurrency-aware. That's why you don't see futures in Haskell or Erlang either. (I have to qualify with "what are today called" because the term has drifted; for instance, Haskell does have explicit support for an older academic definition of the term with MVars, but modern software engineers are not using the term that way.)
I've never in my life programmed in Node. When I say futures I'm talking about the logical concurrency primitive written about by Friedman/Wise in the 70s.
Lots & lots of idiomatic go code exists in that form (anytime you wrap a select that times out in a function you have a future).
Channels, what we are really discussing here, have 2 problems: the first is in abstraction, they don't provide basic primitives that other similar structures provide, like timeouts & cancellation. The second is in implementation. As futures you have to worry about all the edge cases around nil & closed channels. As queues they are highly contended.
I agree with this entirely. As I said many times before, I like Go and I like its approach to concurrency.
All I'm trying to do is to make sure that people who make bold claims about abstracting away blocking code aren't misleading others: when calling other code you should always be aware of how it interacts with the flow control of your program.
In your opinion, how does Python 3.5's async/await syntax compare to Go for writing concurrent programs? I work primarily with Python3 these days and have no Go experience.
He is speaking to the idea that you don't need to care about whether a function blocks or not. It's simply untrue (I'd go further and say if its untrue in all languages but that's a digression).
To have an abstraction where you really don't care about blocking or not you need promises/futures. Go's futures are bad. Real bad.
If you don't want the function to be non-blocking then you are fine with either method signature.
I took it to mean that with Node, some functions have a traditional return value, like functions in most common languages. But if there's even the possibility that they may indirectly invoke an async function, they need to have a different signature using callbacks or promises. For example, here's a simple function:
function isValidFoo(val) {
...return boolean
}
This function is synchronous now, but its contract needs to change if it internally calls something async (say, it's updated to check a value from a cache, which may trigger a DB lookup). The new signature has to add a callback parameter and drop the return value, or return a promise. Then its callers have to change how they invoke the function, and if they were previously synchronous their signatures have to change as well. Every synchronous function up the call stack needs to care about this internal change.
To avoid a ripple effect, all functions have to be designed to be asynchronous from the beginning, but this increases program complexity significantly. It means avoiding built-in language features such as direct return values, and often means substituting async library functions for built-in looping constructs.
Other languages including Go aren't like that. You don't need to wrap a simple return value in a promise or callback just because retrieving it might someday involve I/O. Execution of the thread/goroutine simply resumes whenever the value is ready, and the function returns normally. In those languages, Node's distinction between sync and async is artificial and unnecessary.
...what? The point is that when any function in Go is blocking, it will obviously block the current flow of your own program until it returns. But it only ever blocks the flow of the current goroutine. Other goroutines will continue running just fine, and the runtime will schedule all the ready-to-run goroutines over the OS threads it has available, and that's nothing you need to take care of in your Go program. It essentially abstracts away what Node.js does in its event loop and the programmer does by manually splitting up the program into a series of function callbacks. Just use that knowledge to structure your program accordingly, and don't tried to badly reinvent futures.
I think I follow. But why do I want general-purpose functions to ever be non-blocking in Go?
To me, idiomatic Go suggests that libraries, data structures, business logic, &c is encapsulated in blocking functions, and that concurrency is expressed in glue code.
That doesn't seem to be the argument to me (though I'm totally ignorant about the Node aspect of this).
The argument to me is that Go advertises itself as being easy to write that concurrent glue code & then fails to provide basic abstractions around it.
Everything you mention about things being expressed in glue code would be easier if Golang provided a modern promises library & holds just as reasonably for other modern languages that do.
The "node requires callback hell" is a red herring because so does Golang (that or blocking code), just it does it at the app layer instead of the framework layer.
> The argument to me is that Go advertises itself as being easy to write that concurrent glue code & then fails to provide basic abstractions around it.
What‽ It provides channels, which are a really nice abstraction for concurrency.
> Everything you mention about things being expressed in glue code would be easier if Golang provided a modern promises library & holds just as reasonably for other modern languages that do.
Have you actually written any Go? Using channels in Go is much nicer than using callbacks. Callbacks basically force one to write one's code in continuation-passing style, while callbacks let one work about sync points only where you need.
> What‽ It provides channels, which are a really nice abstraction for concurrency.
Channels are a concurrent safe queue, nothing more. They are not even particularly well implemented concurrent queues as they are highly contended.
What go provides for abstractions around those queues are range and select. Range is a nice way to make simple concurrency cases look like traditional for loops (and is very rarely used in practice as simple concurrency cases are rare, at least for me).
> Have you actually written any Go? Using channels in Go is much nicer than using callbacks.
I have, for 2 years, full time. I have not used node. Have you ever used a language that supports concurrency besides Go? Go does not provide, as part of its concurrency abstractions, things that are deemed bare necessities by other languages such as timeouts, cancellation, supervision, lock free structures, etc.
The entirety of the Golang concurrency story can be wrapped up with 3 things a) the culture of the language prefers message passing concurrency b) the scheduler is a very good example of when simplicity gets you great performance in the main cases c) select as a language keyword is interesting.
By leaving the basics out you push all of that work to the application level, which is why I think the callbacks argument is a red herring. At the application level you are either going to encounter callbacks (as in the stdlib http handler) or blocking code (subject to races, deadlocks, etc).
What I was respond to was this specific sentiment: "With Go it doesn't matter if an operation is blocking or non blocking, that fact can totally be abstracted from the client code." My argument was that that is simply not true. Client code must know the difference between blocking and non-blocking code because it affects the flow of information around the program.
Well it's possible I over stated the necessity for futures as their might be other abstractions that allow you to compose concurrent/non-concurrent systems in a way that is transparent to the api. Its just one of the easier ones to reason about. That said, I don't believe concurrency should be transparent to the api, rather it should be front and center to it.
As for the go futures being bad, channels generally are a bad concurrent queue (contended, lack basic abstractions, etc) and using a single item queue as a future isn't in and of itself bad, but does mean you can't optimize for different usages that futures might have over message passing queues.
> With Go it doesn't matter if an operation is blocking or non blocking, that fact can totally be abstracted from the client code.
No, it can't, and pretending that it can is misleading in a way that allows large teams of developers to cause themselves real problems.
The only sense in which this is true is that you can write a Go function with a simple signature like this:
And the caller has no idea whether this involves concurrency under the hood (e.g. the function can spawn its own goroutines and channels as it sees fit).But, make no bones about it: this function blocks until it completes. This means that while this function may do concurrent work it is absolutely a blocking function. This is fine: sometimes blocking functions are good. But you cannot write a non-blocking Go function in the same way.
To make a function non-blocking you can return a channel out of it for the return value to appear on, like this:
In this model the caller really doesn't have to care whether the function is synchronous or not (though the fact that it was written this way strongly suggests that it is going to return asynchronously, or at least that the developer believes it will have to in the future).Except...that return value just there? That's a Future. It's a terrible, half-implemented version of a Future, but that's exactly what it is. It's a promise to return some kind of result at some point when the underlying process has returned.
And if you don't want to block your current goroutine, you cannot block on that channel receive either. That means that you need a callback. There are two patterns for doing that: you could have some kind of central loop that selects over all channels like this and calls the callback functions (boy that looks a lot like Node's event loop, doesn't it!), or you can manually spawn your own callback functions in their own goroutines. Either way, you have callbacks and futures here: you're just building them yourself and calling them something different.
There are lots of good reasons to switch to Go: it's a language that makes lots of developers remarkably productive, it has an ingrained philosophy of building concurrent programs, it's pretty damn fast, and it runs on all kinds of awesome platforms. But claiming that Go has learned something magic and new about how to write concurrent software in such a way that you don't have to care whether your code is async or not is just not true: you always have to care.