Go doesn’t just support multi-threading, it is multi-threaded by design!
Goroutines
Every Go program is executed by a “goroutine”. A goroutine is kind of a mix between a lightweight green thread and a coroutine (that’s why they’re called differently) with a stack that’s 2kb in size and grows dynamically as needed. Any goroutine can spawn other goroutines using the go
keyword in front of a function call (there’s no parent-child relationship though).
All goroutines are multiplexed through a runtime-internal thread pool and cooperatively scheduled. Whenever a goroutine blocks on I/O operations it suspends itself and puts itself onto the “blocked” queue. The scheduler moves it on to the “ready” queue as soon as it finished waiting and executes it as soon as an execution thread becomes available. This is called cooperative multitasking.
All goroutines run in the same process and memory space and can thus share memory without restrictions. You are, however, responsible for synchronizing access to shared mutable state yourself to avoid data races in case your Go code is executed on a multi-core system (it’s therefore recommended to use the built-in Data Race Detector).
Go is inherently async & multi-threaded
Goroutines make Go inherently multi-threaded and non-blocking (asynchronous) even though you write stupid blocking synchronous code. You also don’t have to care about spawning OS threads (which should be used with care because they’re quite expensive) because Go’s compiled-in runtime does this for you. You spawn as many goroutines as you need and that’s usually just fine because of the fact of how cheap they are. Having hundreds of thousands of goroutines in a single process is pretty common.
The runtime.Gosched function can also be used to manually suspend a goroutine and trigger the scheduler.
Cancellation
Goroutines can’t be killed, they have to exit voluntarily, yet all goroutines are killed as soon as the main goroutine dies or returns because all goroutines are part of the OS process which is represented by the main goroutine. Therefore it’s recommended to use sync.WaitGroup to make the main goroutine wait for other goroutines to finish before returning.
context.Context can be used to make a goroutine cancellable as long as the function that’s executed by the goroutine respects cancellable contexts which means it checks the context for cancellation and returns if instructed to.
More details…
William Kennedy posted a series of very detailed articles about this in August 2018 which I highly recommend reading: Scheduling In Go : Part I - OS Scheduler