Hacker News new | ask | show | jobs
by pcwalton 2563 days ago
I've never been a fan of the "what color is your function" essay, because it implies that Go is in some sort of unique space. In fact, Go just uses threads. There's no semantic difference between Go and pthreads. The only difference is that Go has a particularly idiosyncratic userland implementation of them.
3 comments

While "what colour is your function" essay highlights the author's pet peeve, asynchronous functions, I always understood it to be less about the underlying implementation, but about syntax and semantics; the whole point is that control flow ends up infecting function-level semantics. That problem extends to anything else a language can treat at "coloured".

For example, in Haskell, side-effectful operations end up being "infected" with the IO monad. This means you're not free to mix and match functions — the moment you need to call some IO function, all callers up the stack need to be monadified, too. This might be a late change — suddenly you need a logger or a random number generator, and it has to be passed all the way up from the outermost point that uses monads. In practice, monads are so deeply ingrained in Haskell now that most devs probably don't see this as a colour problem.

Multi-value-returning functions in Go is another example of colour. The only way to use the return value of a Go function that returns a tuple is to assign them:

  value, err := saveFile()
  if err != nil { ... }
This means functions like these aren't composable. I can't do saveFile().then(success).fail(exit) or whatever, like you can in Rust. The moment you have a function returning more than one value, your only option is to create a variable. It's weird.

Interestingly, you can do this, but I've never done it and never seen it in the wild:

  func foo(v int, err error) { ... }
  func bar() (int, error) { ... }
  func main() {
    foo(bar())
  }
I figure the main feature of goroutines is that it is considered acceptable, for whatever reason, that a library function spawns helper goroutines without telling you (as long as it reigns them in somehow and they don't leak or anything weird like that). If eg. a random C or rust library spawned threads for random tasks without explicitly being a concurrency thing, it would probably raise a lot more eyebrows, no?

In the end this is probably because of the possibly superstitious belief that goroutines are free whereas threads need to be carefully budgeted for, and maybe somewhere between premature optimizations and designing for very niche scalability requirement, but subjectively the result is still that goroutines are "available" in a lot more situations than boring, pedestrian OS-level threads.

I feel like that counts as a semantic difference, even if might be social more than technical.

An implementation with buy-in across the entire ecosystem and language so that you don’t have some systems using threads and other systems using futures and other systems using different reactors, etc.

Also known as the point of the article.

Additionally, that implication is entirely of your own creation. The article explicitly lists many languages besides Go:

> Three more languages that don’t have this problem: Go, Lua, and Ruby.

Perhaps you just have an anti-Go bias?

Last I tried it, if I had a million goroutines calling stat(), Go would attempt to spawn a million kernel threads. So I rolled my own rate limiter, bleh.

Is that still the case? (Happy if not)

If it is still the case, is there a standard solution or is it up to the app author?

It's up to the application author in that case, unfortunately. stat() enters the kernel and resolves in one shot, so it requires a whole thread. I haven't read into it very carefully, but on Linux, perhaps the new io_uring business is going to change this state of affairs. For now, however, you need a semaphore of your own.