A goroutine is a lightweight concurrent function managed by the Go runtime. Goroutines make it easy to start background work, handle many requests, run independent tasks, and build responsive network programs.
Concurrency is not the same as parallelism. Concurrency is about structuring multiple tasks that can make progress independently. Parallelism is when tasks literally run at the same time on multiple CPU cores. Go gives you goroutines and channels to express concurrency clearly.
Use the `go` keyword before a function call to run that function in a new goroutine. The main function does not automatically wait for goroutines, so real programs need synchronization.
A practical way to think about goroutines is to treat them as independent tasks that need coordination. Starting the task is easy; designing completion, errors, shared state, cancellation, and resource limits is the real work. In a web server, every incoming request may already run concurrently, so adding extra goroutines inside a handler should be done only when there is a clear benefit and a safe way to stop the work.
Beginners often start goroutines for printing messages, but production goroutines usually represent meaningful background work: sending notifications, consuming queue messages, processing uploaded files, collecting metrics, warming caches, or calling several APIs at the same time. Each of those jobs needs a plan for what happens when one task fails, the process shuts down, or the caller no longer cares about the result.
package main
import (
"fmt"
"sync"
)
func sendEmail(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println("sent email", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go sendEmail(i, &wg)
}
wg.Wait()
fmt.Println("all emails sent")
}
A race condition happens when multiple goroutines access shared data and at least one goroutine writes to it without proper synchronization. The program may appear to work sometimes and fail under load.
Long-running goroutines should have a way to stop. In server code, `context.Context` is commonly used to pass deadlines, cancellation, and request-scoped values.
In production Go services, goroutines should be treated as managed work, not as fire-and-forget shortcuts. Every goroutine should have a clear owner, a clear stop condition, and a clear error path. If a goroutine can block forever on a channel receive, network call, database query, or lock, it can slowly leak memory and make the application harder to shut down.
For request handling, avoid starting background goroutines that continue using request data after the request is cancelled unless that behavior is intentional. Pass a context into downstream work, set timeouts for external calls, and return errors through channels, result structs, logs, or observability tools. This makes concurrent code easier to debug during real traffic.
When concurrency is used to improve throughput, measure before and after. More goroutines can increase speed for I/O-bound work, but they can also overload a database connection pool, hit API rate limits, increase memory use, or make logs harder to follow. A small bounded worker pool is often safer than unlimited goroutine creation.
A worker pool is a common goroutine pattern for processing many jobs with a fixed amount of concurrency. Instead of starting one goroutine for every possible job, you start a limited number of workers and send jobs through a channel. This protects the system from creating too much work at once.
Worker pools are useful for email sending, image processing, queue consumers, API calls, report generation, and background tasks. The important design choice is the worker count. Too few workers may be slow; too many workers may overload the database, network, or external service.
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("worker %d processed job %d\n", id, job)
}
}
func main() {
jobs := make(chan int)
var wg sync.WaitGroup
for id := 1; id <= 3; id++ {
wg.Add(1)
go worker(id, jobs, &wg)
}
for job := 1; job <= 10; job++ {
jobs <- job
}
close(jobs)
wg.Wait()
fmt.Println("all jobs completed")
}
In production Go services, goroutines should be treated as managed work, not as fire-and-forget shortcuts. Every goroutine should have a clear owner, a clear stop condition, and a clear error path. If a goroutine can block forever on a channel receive, network call, database query, or lock, it can slowly leak memory and make the application harder to shut down.
For request handling, avoid starting background goroutines that continue using request data after the request is cancelled unless that behavior is intentional. Pass a context into downstream work, set timeouts for external calls, and return errors through channels, result structs, logs, or observability tools. This makes concurrent code easier to debug during real traffic.
When concurrency is used to improve throughput, measure before and after. More goroutines can increase speed for I/O-bound work, but they can also overload a database connection pool, hit API rate limits, increase memory use, or make logs harder to follow. A small bounded worker pool is often safer than unlimited goroutine creation.
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("worker stopped")
return
default:
fmt.Println("working...")
time.Sleep(200 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 700*time.Millisecond)
defer cancel()
go worker(ctx)
<-ctx.Done()
time.Sleep(100 * time.Millisecond)
}
Start goroutines and let main exit immediately.
Wait for completion with WaitGroup or channel coordination.
Write to shared variables from many goroutines directly.
Use channels, mutexes, or ownership patterns.
No. Goroutines are managed by the Go runtime and are lighter than operating system threads.
No. They help structure concurrent work, but speed depends on CPU, I/O, synchronization, and workload design.
Explore 500+ free tutorials across 20+ languages and frameworks.