Introduction

Go’s goroutines are lightweight, but spawning thousands of them without bounds can still exhaust memory and degrade performance. Object pool libraries solve this by pre-allocating resources and reusing them — reducing GC pressure, bounding concurrency, and improving throughput in self-hosted Go services.

This article compares four popular Go pooling libraries: ants (14,437⭐) — the goroutine pool powerhouse, pond (2,160⭐) — a minimalistic worker pool with task grouping, bytebufferpool (1,329⭐) — an anti-waste byte buffer pool, and sourcegraph/conc (10,401⭐) — structured concurrency with pool support. We compare their APIs, use cases, and performance characteristics.

Feature Comparison

Featureantspondbytebufferpoolconc
Pool TypeGoroutineWorker/ TaskByte BufferStructured Concurrency
GitHub Stars14,4372,1601,32910,401
Pre-allocationYesNoYesNo
Dynamic ScalingYes (auto-tune)Fixed pool sizeNo (fixed caps)N/A
Non-blocking SubmitYesYesN/AN/A
Task GroupingNoYes (Group/Wait)N/AYes (WaitGroup)
Panic RecoveryBuilt-inBuilt-inN/ABuilt-in
Memory ReuseGoroutine recyclingGoroutine recyclingByte slice recyclingGoroutine recycling
Timeout SupportYesYes (Task timeout)NoNo
Stats/MetricsRunning, Waiting, CapRunning, WaitingGet, Put countsN/A
Go Version1.16+1.18+1.12+1.21+

ants: The Goroutine Pool Powerhouse

ants is the most popular Go goroutine pool library, designed for high-throughput server applications. It pre-allocates a pool of goroutines that process submitted functions, eliminating the overhead of spawning new goroutines for each task.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

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

func main() {
    // Create a pool with 100 goroutines
    pool, _ := ants.NewPool(100,
        ants.WithPreAlloc(true),                 // Pre-allocate all workers
        ants.WithNonblocking(true),              // Don't block when pool is full
        ants.WithPanicHandler(func(err interface{}) {
            log.Printf("Worker panic: %v", err)
        }),
    )
    defer pool.Release()

    // Submit 1000 tasks
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        taskID := i
        pool.Submit(func() {
            defer wg.Done()
            // Simulate work
            time.Sleep(10 * time.Millisecond)
            fmt.Printf("Task %d completed
", taskID)
        })
    }
    wg.Wait()

    // Pool stats
    fmt.Printf("Running: %d, Waiting: %d, Capacity: %d
",
        pool.Running(), pool.Waiting(), pool.Cap())
}

ants supports dynamic tuning — you can adjust the pool size at runtime with pool.Tune(size) to respond to changing load. The WithExpiryDuration option cleans up idle workers after a configurable timeout, preventing memory bloat during quiet periods.

For function invocation patterns, ants provides a generic MultiPool:

1
2
3
4
5
6
mp, _ := ants.NewMultiPool(10, ants.RoundRobin, ants.WithPreAlloc(true))
defer mp.ReleaseTimeout(5 * time.Second)

for i := 0; i < 100; i++ {
    mp.Submit(i%10, taskFunction) // distribute across 10 sub-pools
}

pond: Task-Oriented Worker Pool

pond takes a different approach — it focuses on task management rather than raw goroutine pooling. Its standout feature is task grouping, which lets you submit a batch of tasks and wait for all of them to complete or a combined result.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import (
    "context"
    "fmt"
    "github.com/alitto/pond"
)

func main() {
    // Create a worker pool with 50 max workers
    pool := pond.New(50, 1000,  // maxWorkers, maxCapacity
        pond.Strategy(pond.Lazy()), // Create workers on demand
    )
    defer pool.StopAndWait()

    // Submit individual tasks with context
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    pool.SubmitWithContext(ctx, func() error {
        // Task with error return
        return processData("file-1.csv")
    })

    // Task grouping: submit a batch and wait
    group := pool.Group()
    for i := 0; i < 100; i++ {
        i := i
        group.Submit(func() error {
            return processBatch(i)
        })
    }
    
    // Wait for all grouped tasks
    if err := group.Wait(); err != nil {
        log.Printf("Group processing failed: %v", err)
    }

    // Pool metrics
    fmt.Printf("Workers: %d/%d, Tasks Waiting: %d
",
        pool.RunningWorkers(), pool.MaxWorkers(), pool.WaitingTasks())
}

pond’s PoolWithResults variant supports tasks that return values, enabling map-reduce patterns where workers process data and collect results concurrently.

bytebufferpool: Anti-Waste Byte Buffer Pool

bytebufferpool is a specialized pool for byte slices — the most common source of GC pressure in Go network services. Every HTTP request handler, JSON encoder, or protocol parser allocates temporary byte buffers; bytebufferpool recycles them.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
    "fmt"
    "github.com/valyala/bytebufferpool"
)

func main() {
    // Get a buffer from the pool
    buf := bytebufferpool.Get()
    defer bytebufferpool.Put(buf) // Return to pool after use

    // Use it like bytes.Buffer
    buf.WriteString(`{"status":"ok","data":`)
    buf.Write(encodeJSONPayload())
    buf.WriteString(`}`)

    fmt.Println(buf.String()) // Zero-copy string conversion
}

// Integration with HTTP handlers
func handleRequest(w http.ResponseWriter, r *http.Request) {
    buf := bytebufferpool.Get()
    defer bytebufferpool.Put(buf)

    // Build response without allocations
    buf.WriteString("HTTP response content")
    w.Write(buf.Bytes())
}

The pool’s key optimization: it maintains multiple buckets of different buffer sizes and calibrates itself based on actual usage patterns. A buffer that grew to 64KB during one request gets recycled for the next request that needs a 64KB buffer — eliminating repeated allocation + GC cycles.

For fasthttp and other high-performance HTTP frameworks, bytebufferpool is the standard choice for request/response buffering.

sourcegraph/conc: Structured Concurrency with Iterators

conc (pronounced “conk”) is the newest entrant — it provides structured concurrency primitives rather than a traditional worker pool. Its pool package offers a simple bounded goroutine pool, while iter provides parallel map operations.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
    "context"
    "fmt"
    "github.com/sourcegraph/conc/pool"
    "github.com/sourcegraph/conc/iter"
)

func main() {
    // Bounded goroutine pool
    p := pool.New().
        WithMaxGoroutines(50).     // Cap concurrency
        WithErrors()               // Collect errors
    
    for i := 0; i < 100; i++ {
        i := i
        p.Go(func() error {
            return processItem(i)
        })
    }
    
    if errs := p.Wait(); len(errs) > 0 {
        log.Printf("Errors: %v", errs)
    }

    // Parallel map with bounded concurrency
    input := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    mapper := iter.NewMapper(50) // Max 50 concurrent operations

    results := mapper.Map(input, func(i *int) string {
        return fmt.Sprintf("result-%d", *i)
    })
    
    for _, r := range results {
        fmt.Println(r)
    }
}

conc’s pool.ContextPool integrates with Go’s context package for cancellation propagation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
p := pool.New().WithContext(ctx).WithMaxGoroutines(20)

for _, url := range urls {
    url := url
    p.Go(func(ctx context.Context) error {
        return fetchURL(ctx, url)
    })
}

if err := p.Wait(); err != nil {
    log.Printf("Fetch failed: %v", err)
}

Performance Characteristics and Benchmarks

When choosing a pool library, consider these performance factors:

Metricantspondbytebufferpoolconc
Goroutine reuse rateHigh (pre-allocation)Medium (lazy)N/AMedium
Allocations per task0 (after warmup)1-20 (reuse)1-2
GC pressureLowMediumVery LowMedium
Memory overhead~200 bytes/worker~100 bytes/workerNegligible~150 bytes/worker

For CPU-bound workloads, ants’ pre-allocation provides the best throughput. For I/O-bound workloads where workers are often idle, pond’s lazy strategy reduces memory usage. For data transformation pipelines, combining bytebufferpool for buffer management with ants/pond for goroutine pooling provides optimal performance.

FAQ

When should I use a goroutine pool vs plain goroutines?

Use a goroutine pool when: (1) you have an unbounded number of incoming tasks (HTTP requests, event streams), (2) you need to limit concurrency to prevent resource exhaustion, or (3) you want to reduce GC pressure in high-throughput services. Plain goroutines are fine for bounded workloads where you control the concurrency level.

Can I use these libraries with fasthttp or other custom HTTP frameworks?

Yes. bytebufferpool is specifically designed by the fasthttp author and integrates natively. For ants and pond, create the pool during application startup and reuse it across request handlers. Avoid creating pools inside request handlers — the initialization cost defeats the purpose of pooling.

How do I tune the pool size?

Start with runtime.GOMAXPROCS(0) * 2 for CPU-bound workloads and runtime.GOMAXPROCS(0) * 50 for I/O-bound workloads. Monitor pool.Running() and pool.Waiting() metrics: if Waiting is consistently non-zero, increase pool size. If Running rarely reaches capacity, decrease it.

What happens to tasks submitted during graceful shutdown?

All four libraries support graceful shutdown. pool.Release() in ants waits for running tasks to complete. pool.StopAndWait() in pond drains the task queue. Always call the cleanup method before your application exits to prevent dropped tasks.

Are these libraries compatible with Go’s race detector?

Yes, all four libraries pass the Go race detector. They use standard synchronization primitives (mutexes, channels, atomics) and are tested with -race in CI. Always run your application with -race during development when using concurrency primitives.

Why Self-Host Your Object Pool Management?

Embedding pool logic in your application code gives you fine-grained control over resource usage without relying on external rate limiters or connection poolers. Your pooling strategy is version-controlled alongside your business logic, and pool metrics can feed directly into your observability stack.

For concurrent data structures, see our lock-free concurrent data structures guide. For Go caching patterns, check our Go cache libraries comparison. For database connection pooling, see our connection pool monitoring guide.


💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com