0
0
GoComparisonBeginner · 4 min read

Goroutine vs Thread in Go: Key Differences and Usage

In Go, a 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:

AspectGoroutineThread
Creation CostVery low, managed by Go runtimeHigh, managed by OS
Memory UsageSmall stack (~2KB, grows dynamically)Large fixed stack (MBs)
SchedulingCooperatively scheduled by Go runtimePreemptively scheduled by OS
NumberCan run millions concurrentlyLimited by OS resources
CommunicationChannels for safe data exchangeRequires locks or other sync primitives
BlockingNon-blocking, runtime handles blocking syscallsBlocking 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:

go
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
}
Output
Hello from goroutine 1 Hello from goroutine 2 Hello from goroutine 1 Hello from goroutine 2 Hello from goroutine 1 Hello from goroutine 2
↔️

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:

go
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
}
Output
Hello from locked thread 1 Hello from locked thread 2 Hello from locked thread 1 Hello from locked thread 2 Hello from locked thread 1 Hello from locked thread 2
🎯

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.

Key Takeaways

Goroutines are lightweight, managed by Go runtime, and cheaper than OS threads.
Threads are heavier OS resources with fixed stack size and preemptive scheduling.
Goroutines communicate safely via channels; threads need explicit synchronization.
Use goroutines for most concurrency; use threads only for special OS-level needs.