Goroutines are user-space threads, created and managed by the Go runtime, not the OS. These are lightweight compared to OS threads w.r.t. research consumption and scheduling overhead.
Go scheduler is a M:N scheduler: few OS threads and N goroutines; and scheduler is responsible to schedule the goroutines on limited number of OS threads.
When a goroutine needs to be paused, chan calls into the scheduler to park G1, what scheduler does is to change G1 from running to waiting. And schedule another goroutine on the OS thread.
This is good for perf. We haven’t stopped the OS thread but scheduled another goroutine by context switching. This is not expensive.
We need to resume the paused goroutine once the channel is not full anymore.
When G1 finally runs, it needs to acquire the lock. But the runtime is actually is so much smarter to make this less costly. Runtime can copy directly to the receiver stack. G1 writes directly to G2’s stack and doesn’t have to acquire any locks.
On resuming, G2 does not need to acquire channel lock and manipulate the buffer. This also means one fewer memory copy.
Unbuffered channels or select?
Not in the scope of this talk. The Go runtime is written is Go and you read the source code to learn.