Out of curiosity, what is the precise academic definition?
goroutines + channels were specifically designed to solve the concurrency/communication/locking/flow/performance problems that traditional coroutines introduced. Coroutines are much heavier and introduce a lot of overhead.
Goroutines are not necessarily threads; they're more akin to greenthreads. In Go, they're much cheaper and lower latency and still automatically move between processors (subject to your control).
But, aside from lower performance and greater difficulty in locking and multi-threaded communication via shared objects, coroutines are also much harder to code with and reason about.
The primitives in Go are very simple and that is one of the best features of the language. Running a for loop across a set of channels is very easy and built-in to the language, making it childs-play to write a heavily-concurrent network server. Channels are specifically designed to be a high-speed data bus between goroutines, rather than ever use more expensive and less safe shared memory. (Channels are preferred over shared memory in Go)
This is a fantastic video explaining how and why Rob Pike and team developed the idea of goroutines and channels, building on their work from Smalltalk: https://go.dev/blog/waza-talk
If you needed one mutex lock to receive a reference to a data structure, a channel does not make that cheaper. The absolute best case is that the same OS thread served both sides of the channel transaction, but you can't plan your overall performance around that, because it becomes less likely the more loaded your program's goroutines are. In general you still have to think of both the send and receive side of the channel as short-lived exclusive locks of the mutex, with all of the contention and cache effects that implies.
Shared memory is not more expensive. Memory is memory, it's either cached on your core or not. In fact, Go still has to issue fence instructions to ensure that the memory it observes after a channel read is sequenced after any writes to that memory, so it's at best the same cost you'd have with other forms of inter-thread communication in any language.
Anyway, even that is missing the point. Go still shares memory if you used a reference type, and most types in Go end up being reference types, because it's the only way to have a variable-sized data structure (and while we're at it, string is the only variable-sized data structure that's also immutable).
The bigger problem is that Go doesn't enforce thread safety. Channels only make communication safe if you send types that don't contain any mutable references... but Go doesn't give you any way to define your own immutable types. That basically limits you to just string. Instead people send slices, maps, pointers to structs, interfaces, etc. and those are all mutable and Go does nothing to enforce that you didn't mutate them.
Even if all of that somehow wasn't true, many parallelism patterns simply don't map well to channels, so you still end up with mutexes in many parts of real world projects. Even if you don't see the mutexes, they're in your libraries. For example, Go's http.Transport contains a connection pool, but it uses mutexes instead of channels because even the Go team knows that mutexes still make sense for many real-world patterns.
This whole "channels make Go safe" myth has to stop. It's confused a generation of Go programmers about the actual safety (and apparently performance) tradeoffs of channels. They do not make Go safer (mutable references are still mutable after being sent on a channel), they do not make it faster (the memory still has to be fenced), and heck while we're at it, they do not even make it simpler ("idiomatic" use of channels introduces many ways that goroutines can deadlock, and deadlock-free use of channels is much more complicated and less idiomatic).
The most useful thing about channels is that you can select{} on multiple of them so they partly compensate for Go's limitations around selecting on futures in general. They're a poor substitute when you actually needed to select on something like IO, where io.Reader/Writer still don't interact with select, channels, or even cancellation directly.
Comments
Out of curiosity, what is the precise academic definition?
goroutines + channels were specifically designed to solve the concurrency/communication/locking/flow/performance problems that traditional coroutines introduced. Coroutines are much heavier and introduce a lot of overhead.
Goroutines are not necessarily threads; they're more akin to greenthreads. In Go, they're much cheaper and lower latency and still automatically move between processors (subject to your control).
But, aside from lower performance and greater difficulty in locking and multi-threaded communication via shared objects, coroutines are also much harder to code with and reason about.
The primitives in Go are very simple and that is one of the best features of the language. Running a for loop across a set of channels is very easy and built-in to the language, making it childs-play to write a heavily-concurrent network server. Channels are specifically designed to be a high-speed data bus between goroutines, rather than ever use more expensive and less safe shared memory. (Channels are preferred over shared memory in Go)
This is a fantastic video explaining how and why Rob Pike and team developed the idea of goroutines and channels, building on their work from Smalltalk: https://go.dev/blog/waza-talk
You are thinking about threads, not coroutines. Coroutines are not “heavy” and have nothing to do with concurrency or locking.
What do you mean? How do you think channels are implemented? Go is open source, you can see for yourself, channels still use mutexes internally.
https://github.com/golang/go/blob/b6ca586181f3f1531c01d51d63...
If you needed one mutex lock to receive a reference to a data structure, a channel does not make that cheaper. The absolute best case is that the same OS thread served both sides of the channel transaction, but you can't plan your overall performance around that, because it becomes less likely the more loaded your program's goroutines are. In general you still have to think of both the send and receive side of the channel as short-lived exclusive locks of the mutex, with all of the contention and cache effects that implies.
Shared memory is not more expensive. Memory is memory, it's either cached on your core or not. In fact, Go still has to issue fence instructions to ensure that the memory it observes after a channel read is sequenced after any writes to that memory, so it's at best the same cost you'd have with other forms of inter-thread communication in any language.
Anyway, even that is missing the point. Go still shares memory if you used a reference type, and most types in Go end up being reference types, because it's the only way to have a variable-sized data structure (and while we're at it, string is the only variable-sized data structure that's also immutable).
The bigger problem is that Go doesn't enforce thread safety. Channels only make communication safe if you send types that don't contain any mutable references... but Go doesn't give you any way to define your own immutable types. That basically limits you to just string. Instead people send slices, maps, pointers to structs, interfaces, etc. and those are all mutable and Go does nothing to enforce that you didn't mutate them.
Even if all of that somehow wasn't true, many parallelism patterns simply don't map well to channels, so you still end up with mutexes in many parts of real world projects. Even if you don't see the mutexes, they're in your libraries. For example, Go's http.Transport contains a connection pool, but it uses mutexes instead of channels because even the Go team knows that mutexes still make sense for many real-world patterns.
https://github.com/golang/go/blob/b6ca586181f3f1531c01d51d63...
This whole "channels make Go safe" myth has to stop. It's confused a generation of Go programmers about the actual safety (and apparently performance) tradeoffs of channels. They do not make Go safer (mutable references are still mutable after being sent on a channel), they do not make it faster (the memory still has to be fenced), and heck while we're at it, they do not even make it simpler ("idiomatic" use of channels introduces many ways that goroutines can deadlock, and deadlock-free use of channels is much more complicated and less idiomatic).
The most useful thing about channels is that you can select{} on multiple of them so they partly compensate for Go's limitations around selecting on futures in general. They're a poor substitute when you actually needed to select on something like IO, where io.Reader/Writer still don't interact with select, channels, or even cancellation directly.