Goroutine vs Thread in Go: Key Differences and Usage
goroutine is a lightweight, managed unit of concurrency that runs within a single OS thread, while a thread is a heavier operating system resource. Goroutines are cheaper to create and switch between, enabling efficient concurrent programming compared to traditional threads.Quick Comparison
Here is a quick side-by-side comparison of goroutines and threads in Go:
| Aspect | Goroutine | Thread |
|---|---|---|
| Creation Cost | Very low, managed by Go runtime | High, managed by OS |
| Memory Usage | Small stack (~2KB, grows dynamically) | Large fixed stack (MBs) |
| Scheduling | Cooperatively scheduled by Go runtime | Preemptively scheduled by OS |
| Number | Can run millions concurrently | Limited by OS resources |
| Communication | Channels for safe data exchange | Requires locks or other sync primitives |
| Blocking | Non-blocking, runtime handles blocking syscalls | Blocking blocks the thread |
Key Differences
Goroutines are managed by the Go runtime, which means they are multiplexed onto a smaller number of OS threads. This makes them very lightweight and cheap to create compared to OS threads, which require more memory and system calls to manage.
Each goroutine starts with a small stack that grows and shrinks as needed, while threads have a large fixed stack size. The Go scheduler handles switching between goroutines efficiently without involving the OS scheduler for every switch.
Communication between goroutines is designed to be safe and easy using channels, whereas threads often require explicit locking mechanisms to avoid race conditions. Goroutines also allow Go programs to handle millions of concurrent tasks, which is impractical with threads alone.
Code Comparison
This Go code launches two goroutines that print messages concurrently:
package main import ( "fmt" "time" ) func say(message string) { for i := 0; i < 3; i++ { fmt.Println(message) time.Sleep(100 * time.Millisecond) } } func main() { go say("Hello from goroutine 1") go say("Hello from goroutine 2") time.Sleep(500 * time.Millisecond) // Wait for goroutines to finish }
Thread Equivalent
Go does not provide direct control over OS threads, but you can simulate thread-like behavior using the runtime.LockOSThread() function to bind a goroutine to an OS thread. However, this is rarely needed. Below is an example using goroutines with thread locking:
package main import ( "fmt" "runtime" "time" ) func say(message string) { runtime.LockOSThread() // Bind goroutine to current OS thread defer runtime.UnlockOSThread() for i := 0; i < 3; i++ { fmt.Println(message) time.Sleep(100 * time.Millisecond) } } func main() { go say("Hello from locked thread 1") go say("Hello from locked thread 2") time.Sleep(500 * time.Millisecond) // Wait for goroutines to finish }
When to Use Which
Choose goroutines for most concurrent tasks in Go because they are lightweight, easy to use, and efficiently managed by the Go runtime. They allow you to run thousands or millions of concurrent operations without heavy resource use.
Use OS threads or bind goroutines to threads only when you need to interact with thread-local state, call certain C libraries, or require strict control over OS thread behavior. For typical Go programs, goroutines are the best choice.