I'll give you an overview of the scheduler that starts with go 1.7.
The scheduler has three basic structures, called G, M, and P. A G is a goroutine, an M is an operating system thread, and a P is a (logical) processor.
The scheduler has the exact number of Gomaxprocs p (Gomaxprocs is an environment variable and run-time function that sets the concurrency in the program). In order for M to execute a G, it must obtain a p and then run G until it stops. G by making system calls such as I/O operations, blocking a channel operation, calling the C function, is being pre-preempted (pre-emption) or some other small situation to stop. A G can only be preempted in a safe place, and in the current implementation only when the code has a function call Happen.
When a G is blocked like a channel operation, it will be placed in a queue and M will look for another running G. If there is no G to run, M will release P and go to sleep.
When G completes the system call, it must regain p. If no p is available, it will be marked as operational and M will go to sleep.
When the channel operation succeeds, it wakes up another goroutine, marks it as operational, and wakes M to run it if there is a P available.
Although the garbage collector is mostly concurrent, there are a few points to temporarily stop all threads in order to safely transfer to the next collection stage. It pre-preemption by marking all running goroutine. When they reach the safe point, G and M will go to sleep. When the garbage collector is the only remaining running G, it moves to the next stage and then wakes up the gomaxproc number of M, each of which will find a running G and continue.
Runtime. The gosched function urges M to put the current G in the list of goroutine that can be run and select a new G from the list to start running.