Goroutine

Lesson 13: Goroutine

Imagine a restaurant: if there's only one waiter (single thread), they must finish taking orders, serving food, and billing each table before moving to the next. Hiring many waiters (multi-threading) would be expensive. Go's solution is to hire a group of lightweight part-time waiters (goroutines) who share the same restaurant system (scheduler) — whoever is available serves next, handling a large number of customers with minimal resources.


Core Concepts


Basic Syntax and Usage

Launching a Goroutine

Add the go keyword before a function call to launch a new goroutine:

GO
go functionName(args)
// or
go func() {
    // anonymous function body
}()

Waiting for Goroutines with sync.WaitGroup

Since the main goroutine does not automatically wait for child goroutines, use sync.WaitGroup for synchronization:

GO
var wg sync.WaitGroup

wg.Add(1)        // increment counter by 1
go func() {
    defer wg.Done() // decrement counter when done
    // perform task
}()
wg.Wait()        // block until counter reaches zero
💡 Tip: wg.Add() must be called before the go statement, otherwise a race condition may occur — the main goroutine might return from Wait before Add is called.

💡 Tip: Do not place wg.Add() inside the goroutine, otherwise wg.Wait() may execute before Add.

💡 Tip: Always use defer wg.Done() to ensure the counter decrements correctly even if a panic occurs.


Examples

Example: Launching Multiple Goroutines (Difficulty ⭐)

GO
package main

import (
	"fmt"
	"sync"
)

func sayHello(id int, wg *sync.WaitGroup) {
	defer wg.Done() // notify WaitGroup when done
	fmt.Printf("Hello, I am goroutine #%d\n", id)
}

func main() {
	var wg sync.WaitGroup

	for i := 1; i <= 5; i++ {
		wg.Add(1)
		go sayHello(i, &wg)
	}

	wg.Wait() // wait for all goroutines to finish
	fmt.Println("All goroutines have completed")
}
▶ Try it Yourself

Output order is non-deterministic (concurrent execution):

TEXT
Hello, I am goroutine #3
Hello, I am goroutine #1
Hello, I am goroutine #5
Hello, I am goroutine #2
Hello, I am goroutine #4
All goroutines have completed

Example: Concurrent Computation and Result Aggregation (Difficulty ⭐⭐)

GO
package main

import (
	"fmt"
	"sync"
)

// Concurrently compute squares and store results in a shared slice
func main() {
	numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	results := make([]int, len(numbers))

	var wg sync.WaitGroup

	for i, num := range numbers {
		wg.Add(1)
		go func(index, val int) {
			defer wg.Done()
			results[index] = val * val // each goroutine writes to a different index, no lock needed
		}(i, num) // pass loop variables as arguments to avoid closure capture issues
	}

	wg.Wait()

	fmt.Println("Original data:", numbers)
	fmt.Println("Squared results:", results)
}
▶ Try it Yourself

Output:

TEXT
Original data: [1 2 3 4 5 6 7 8 9 10]
Squared results: [1 4 9 16 25 36 49 64 81 100]
💡 Tip: Starting from Go 1.22, the for loop variable creates a new copy on each iteration, so explicit parameter passing is no longer required. However, for compatibility and readability, explicit passing is still recommended.


Example: Concurrent Task Pool and Goroutine Leak Prevention (Difficulty ⭐⭐⭐)

GO
package main

import (
	"context"
	"fmt"
	"math/rand"
	"sync"
	"time"
)

// task represents a task that needs to be processed
func task(ctx context.Context, id int) (string, error) {
	// Simulate a time-consuming operation
	duration := time.Duration(rand.Intn(500)) * time.Millisecond

	select {
	case <-time.After(duration):
		return fmt.Sprintf("Task #%d completed (took %v)", id, duration), nil
	case <-ctx.Done():
		return "", ctx.Err() // return immediately when context is cancelled, preventing goroutine leak
	}
}

func main() {
	rand.Seed(time.Now().UnixNano())

	// Set a total timeout to prevent goroutine leaks
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel() // ensure resource release

	taskCount := 20
	results := make(chan string, taskCount) // buffered channel to collect results
	var wg sync.WaitGroup

	// Start 5 worker goroutines as a task pool
	workerCount := 5
	for w := 1; w <= workerCount; w++ {
		wg.Add(1)
		go func(workerID int) {
			defer wg.Done()
			for i := workerID; i <= taskCount; i += workerCount {
				res, err := task(ctx, i)
				if err != nil {
					fmt.Printf("Worker %d: Task #%d cancelled\n", workerID, i)
					return
				}
				results <- res
			}
		}(w)
	}

	// Close the channel after all workers finish
	go func() {
		wg.Wait()
		close(results)
	}()

	// Collect results
	for res := range results {
		fmt.Println(res)
	}

	fmt.Println("All tasks processed")
}
▶ Try it Yourself

Key Points:


Practical Use Cases

Case 1: Concurrent HTTP Requests

GO
package main

import (
	"fmt"
	"io"
	"net/http"
	"sync"
	"time"
)

func fetchURL(url string, wg *sync.WaitGroup) {
	defer wg.Done()

	start := time.Now()
	resp, err := http.Get(url)
	if err != nil {
		fmt.Printf("[Error] %s: %v\n", url, err)
		return
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	elapsed := time.Since(start)
	fmt.Printf("[Done] %s — Status: %d, Size: %d bytes, Time: %v\n",
		url, resp.StatusCode, len(body), elapsed)
}

func main() {
	urls := []string{
		"https://httpbin.org/get",
		"https://httpbin.org/delay/1",
		"https://httpbin.org/delay/2",
		"https://httpbin.org/status/200",
	}

	var wg sync.WaitGroup
	start := time.Now()

	for _, url := range urls {
		wg.Add(1)
		go fetchURL(url, &wg)
	}

	wg.Wait()
	fmt.Printf("\nAll done, total time: %v\n", time.Since(start))
	// Total time is approximately equal to the slowest request, not the sum of all requests
}

Case 2: Asynchronous Log Writing

GO
package main

import (
	"fmt"
	"time"
)

type LogEntry struct {
	Level   string
	Message string
}

// logWriter asynchronously consumes logs in a background goroutine
func logWriter(entries <-chan LogEntry, done chan<- struct{}) {
	for entry := range entries {
		// Simulate writing to a file or remote service
		time.Sleep(50 * time.Millisecond)
		fmt.Printf("[%s] %s\n", entry.Level, entry.Message)
	}
	done <- struct{}{} // notify the main goroutine that writing is complete
}

func main() {
	logCh := make(chan LogEntry, 100) // buffered channel as a log queue
	done := make(chan struct{})

	// Start background log writing goroutine
	go logWriter(logCh, done)

	// Main program runs normally, logging asynchronously
	for i := 1; i <= 5; i++ {
		logCh <- LogEntry{
			Level:   "INFO",
			Message: fmt.Sprintf("Processing request #%d", i),
		}
		fmt.Printf("Submitted log #%d\n", i)
	}

	close(logCh) // close the channel to signal the writer to exit
	<-done       // wait for the writer to finish
	fmt.Println("Program exited")
}

❓ FAQ

1. Why isn't my goroutine executing?

When the main goroutine exits, it forcibly terminates all child goroutines. Common cause:

GO
// ❌ Wrong: main goroutine exits immediately, child goroutine has no chance to run
func main() {
	go fmt.Println("Hello")
}

// ✅ Correct: use WaitGroup to wait
func main() {
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		fmt.Println("Hello")
	}()
	wg.Wait()
}

2. When do goroutines leak?

A goroutine leak means a goroutine can never exit, continuously occupying memory and scheduler resources. Common cause:

GO
// ❌ Leak: channel has no receiver
func leaky() <-chan int {
	ch := make(chan int)
	go func() {
		ch <- 42 // permanently blocked, goroutine cannot exit
	}()
	return ch
}

// ✅ Safe: use a buffered channel or context cancellation
func safe() <-chan int {
	ch := make(chan int, 1) // buffer of 1, send won't block
	go func() {
		ch <- 42
	}()
	return ch
}

3. How to avoid the classic closure capture trap with loop variables?

GO
// ❌ Go 1.21 and earlier: all goroutines may print the same value
for i := 0; i < 5; i++ {
	go func() {
		fmt.Println(i) // may all print 5
	}()
}

// ✅ Recommended: explicit parameter passing, compatible with all Go versions
for i := 0; i < 5; i++ {
	go func(n int) {
		fmt.Println(n) // correctly prints 0, 1, 2, 3, 4
	}(i)
}

4. Can I get the current number of running goroutines?

GO
import "runtime"

fmt.Println("Current goroutine count:", runtime.NumGoroutine())

This is very useful for debugging goroutine leaks. A normal idle program typically has only 1-2 goroutines; if the number keeps growing, there may be a leak.


📖 Summary

Concept Description
go keyword Launches a goroutine, syntax is go func()
Scheduling model G-M-P model, Go runtime handles scheduling, transparent to the user
Lightweight Initial stack is about 2KB, can create hundreds of thousands
WaitGroup Used to wait for a group of goroutines to complete
Context Used to propagate cancellation signals and timeout control
Leak prevention Ensure every goroutine has an exit path

Core Principles:

  1. Always ensure goroutines have clear exit conditions.
  2. Use sync.WaitGroup or channels to synchronize the main goroutine with child goroutines.
  3. Use context to control goroutine lifecycles and prevent leaks.
  4. Be mindful of the interaction between closures and loop variables (explicit parameter passing is recommended).

📝 Exercises

Exercise 1: Concurrent Prime Number Checker

Write a program that uses goroutines to concurrently check whether a set of numbers are prime, then aggregate and output the results.

GO
// Hints:
// - Define an isPrime(n int) bool function
// - Launch a goroutine for each number
// - Use WaitGroup to wait for all to complete
// - Use a mutex or channel to collect results

Exercise 2: Concurrent Download Simulator

Simulate concurrent file downloads: start 3 worker goroutines that pick tasks from a task queue, each download takes a random 100-1000ms, and report the total time and the number of tasks completed by each worker.

GO
// Hints:
// - Use a channel as the task queue
// - Each worker reads tasks from the channel
// - Use context.WithTimeout to control the total timeout
// - Use sync.Map or a mutex to count each worker's completions

Exercise 3: Goroutine Leak Detector

Write a utility function that accepts a task function as a parameter, executes it, and monitors the goroutine count. If the goroutine count after the task completes is higher than before execution, it indicates a possible leak — output a warning.

GO
// Hints:
// - Use runtime.NumGoroutine() to get the goroutine count
// - Record the count before and after task execution
// - Wait a short time (e.g., 100ms) after execution before checking
// - Output the detection result

Next Lesson: Channel →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏