Mastering Go Goroutine Pools: Boost Performance with Custom and Ants Implementations

This article explains why high‑concurrency Go programs suffer from CPU spikes, memory bloat, and GC jitter, then introduces the architecture of a goroutine pool, provides a step‑by‑step implementation, compares a simple channel‑based pool with the high‑performance Ants library, and shares benchmark results and optimization tips.

Code Wrench
Code Wrench
Code Wrench
Mastering Go Goroutine Pools: Boost Performance with Custom and Ants Implementations

Why a Goroutine Pool?

Although Go goroutines are lightweight, creating millions of them can cause memory explosion, increase GC pressure, and generate excessive context‑switch overhead. A pool limits the number of concurrent workers and schedules tasks, keeping the system stable.

Core Architecture

The pool consists of three components:

Task Queue : holds pending tasks, similar to a parcel‑sorting center.

Worker : a goroutine that pulls tasks from the queue and executes them.

Scheduler : manages the queue and workers to ensure efficient and safe execution.

┌───────────┐
      │ Task Queue │
      └───────────┘
            │
   ┌────────┴────────┐
   │        │        │
 ┌────┐  ┌────┐  ┌────┐
 │W1  │  │W2  │  │W3  │  <- Workers
 └────┘  └────┘  └────┘

Basic Implementation Example

type Task struct {
    Job func()
}

type Pool struct {
    taskChan   chan Task
    workerCount int
    wg         sync.WaitGroup
}

func NewPool(workerCount int) *Pool {
    return &Pool{
        taskChan:    make(chan Task, workerCount*2),
        workerCount: workerCount,
    }
}

func (p *Pool) Submit(task Task) {
    p.wg.Add(1)
    p.taskChan <- task
}

func (p *Pool) Run() {
    for i := 0; i < p.workerCount; i++ {
        go func(id int) {
            for task := range p.taskChan {
                task.Job()
                p.wg.Done()
            }
        }(i)
    }
}

func (p *Pool) Wait() {
    p.wg.Wait()
}

Classic Implementations and Benchmarks

1. Channel‑Based Pool

Pros : simple and flexible.

Cons : scheduling overhead becomes noticeable under very high concurrency.

Benchmark: 1000 tasks, each sleeping 10 ms.

func main() {
    pool := NewPool(10)
    pool.Run()
    start := time.Now()
    for i := 0; i < 1000; i++ {
        id := i
        pool.Submit(Task{Job: func() {
            time.Sleep(10 * time.Millisecond)
            fmt.Printf("Task %d done
", id)
        }})
    }
    pool.Wait()
    fmt.Printf("All tasks completed in %v
", time.Since(start))
}

Result: total time ≈ 1 s, CPU and memory usage remain moderate.

2. Ants High‑Performance Pool

Goroutine reuse reduces creation/destruction cost.

Supports task timeout.

Dynamic scaling during load spikes.

Same benchmark (1000 × 10 ms).

package main

import (
    "fmt"
    "github.com/panjf2000/ants/v2"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    pool, _ := ants.NewPool(10)
    defer pool.Release()
    start := time.Now()
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        id := i
        _ = pool.Submit(func() {
            time.Sleep(10 * time.Millisecond)
            fmt.Printf("Task %d done
", id)
            wg.Done()
        })
    }
    wg.Wait()
    fmt.Printf("All tasks completed in %v
", time.Since(start))
}

Result: similar total time (~1 s) but with lower CPU and memory consumption and better stability under heavy load.

Implementation Comparison

Channel Pool – total time ~1 s, CPU medium, memory medium.

Ants Pool – total time ~1 s, CPU low, memory low.

Performance Optimization Strategies

Goroutine reuse : avoid creating and destroying goroutines repeatedly.

Batch task submission : send tasks in groups to reduce scheduling overhead.

Dynamic scaling : automatically adjust the number of workers based on load.

Graceful shutdown : wait for all queued tasks to finish before terminating workers.

Benchmark with 10k Tasks (5 ms each)

package main

import (
    "fmt"
    "sync"
    "time"
    "github.com/panjf2000/ants/v2"
)

func task(id int) { time.Sleep(5 * time.Millisecond) }

func goroutineTest(n int) {
    var wg sync.WaitGroup
    start := time.Now()
    for i := 0; i < n; i++ {
        wg.Add(1)
        go func(id int) { task(id); wg.Done() }(i)
    }
    wg.Wait()
    fmt.Printf("Direct goroutine: %v
", time.Since(start))
}

func channelPoolTest(n, workers int) {
    type Task struct { Job func() }
    pool := make(chan Task, workers*2)
    var wg sync.WaitGroup
    for i := 0; i < workers; i++ {
        go func() { for t := range pool { t.Job(); wg.Done() } }()
    }
    start := time.Now()
    for i := 0; i < n; i++ {
        wg.Add(1)
        id := i
        pool <- Task{Job: func() { task(id) }}
    }
    wg.Wait()
    close(pool)
    fmt.Printf("Channel pool: %v
", time.Since(start))
}

func antsPoolTest(n, workers int) {
    var wg sync.WaitGroup
    pool, _ := ants.NewPool(workers)
    defer pool.Release()
    start := time.Now()
    for i := 0; i < n; i++ {
        wg.Add(1)
        id := i
        _ = pool.Submit(func() { task(id); wg.Done() })
    }
    wg.Wait()
    fmt.Printf("Ants pool: %v
", time.Since(start))
}

func main() {
    n := 10000
    workers := 100
    goroutineTest(n)
    channelPoolTest(n, workers)
    antsPoolTest(n, workers)
}

Benchmark Results

Direct goroutine – total time ~12 s, CPU high, memory high.

Channel Pool – total time ~1.2 s, CPU medium, memory medium.

Ants Pool – total time ~1.0 s, CPU low, memory low.

Using a goroutine pool dramatically improves throughput and reduces CPU/memory pressure; Ants offers the best stability under heavy concurrency.

Conclusion

A well‑designed goroutine pool controls the degree of parallelism, reduces GC overhead, and keeps Go applications stable and performant.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

GoANTS
Code Wrench
Written by

Code Wrench

Focuses on code debugging, performance optimization, and real-world engineering, sharing efficient development tips and pitfall guides. We break down technical challenges in a down-to-earth style, helping you craft handy tools so every line of code becomes a problem‑solving weapon. 🔧💻

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.