Speed Up Go Programs with Goroutines and Channels: From Seconds to Milliseconds

This article demonstrates how Go's goroutines and channels can transform a sequential task that takes seconds per operation into a highly concurrent solution that completes millions of operations in just a few seconds, complete with code examples and performance benchmarks.

21CTO
21CTO
21CTO
Speed Up Go Programs with Goroutines and Channels: From Seconds to Milliseconds

Sequential Execution in Go

Computers can perform many tasks quickly, but a simple Go function that sleeps for one second illustrates the cost of sequential execution. Running the function ten times takes about ten seconds, and a million repetitions would require roughly ten days.

func doSomething() {
    time.Sleep(time.Second)
}

func doManyThings(n int) {
    for i := 0; i < n; i++ {
        doSomething()
    }
}

func main() {
    doManyThings(10)
}

Benchmarking this code shows a runtime of around 10 seconds for ten calls:

$ time go run main.go
real    0m10.256s

Introducing Concurrency with Goroutines

By launching each call in its own goroutine, the work can be performed in parallel on multiple CPU cores, dramatically reducing total execution time.

func doManyThings(n int) {
    var wg sync.WaitGroup // concurrency‑safe counter
    for i := 0; i < n; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            doSomething()
        }()
    }
    wg.Wait()
}

func main() {
    doManyThings(1_000_000)
}

On a typical laptop this concurrent version finishes one million one‑second sleeps in about 2.7 seconds, a speed‑up of several orders of magnitude.

$ time go run main.go
real    0m2.667s

Collecting Results with Channels

When each goroutine returns a value (for example, an error), a channel can be used to gather the results without needing a shared counter.

func doSomething() error {
    time.Sleep(time.Second)
    if rand.Intn(100) == 0 { // ~1% failure rate
        return errors.New("we got a problem")
    }
    return nil
}

func doManyThings(n int) []error {
    ch := make(chan error, n) // buffered channel holds all results
    for i := 0; i < n; i++ {
        go func() {
            ch <- doSomething()
        }()
    }
    var errs []error
    for i := 0; i < n; i++ {
        if err := <-ch; err != nil {
            errs = append(errs, err)
        }
    }
    return errs
}

func main() {
    n := 1_000_000
    errs := doManyThings(n)
    fmt.Printf("Doing %d things there were %d errors.
", n, len(errs))
}

This approach eliminates the need for explicit counters; the second loop that reads from the channel implicitly waits for all goroutines to finish, and a buffered channel ensures that sends never block even if the receiver hasn't started reading yet.

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.

GoGoroutineChannel
21CTO
Written by

21CTO

21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.

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.