3 min read

Concurrency in Go with Tests

Table of Contents

Let’s talk about making your Go code faster with concurrency! 🚀 Imagine you’re checking 100 websites - doing it one by one is slow.

Let’s fix that.

The Problem: Slow Website Checking

First, let’s write a test for our website checker:

func TestCheckWebsites(t *testing.T) {
    mockChecker := func(url string) bool {
        return url != "bad://website"
    }

    urls := []string{
        "http://good.com",
        "http://also.good",
        "bad://website",
    }

    want := map[string]bool{
        "http://good.com":    true,
        "http://also.good":   true,
        "bad://website":     false,
    }

    got := CheckWebsites(mockChecker, urls)

    if !reflect.DeepEqual(want, got) {
        t.Errorf("wanted %v, got %v", want, got)
    }
}

We have a CheckWebsites function that checks URLs:

func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
    results := make(map[string]bool)

    for _, url := range urls {
        results[url] = wc(url) // Checks each URL one at a time
    }

    return results
}

This works, but it’s slow - like waiting for each kettle to boil before making the next cup of tea. ☕️🐌

Making It Faster with Goroutines

The solution? Concurrency!

In Go, we use goroutines (lightweight threads) to do multiple things at once.

First attempt (that doesn’t work):

func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
    results := make(map[string]bool)

    for _, url := range urls {
        go func() {
            results[url] = wc(url) // Oops! Race condition!
        }()
    }

    time.Sleep(2 * time.Second) // Hacky fix
    return results
}

This fails because:

  1. The main function finishes before goroutines complete
  2. Multiple goroutines try to write to the map simultaneously (big no-no in Go!)

The Fix: Channels to the Rescue!

Channels are Go’s way for goroutines to communicate safely:

func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
    results := make(map[string]bool)
    resultChannel := make(chan result)

    for _, url := range urls {
        go func(u string) {
            resultChannel <- result{u, wc(u)} // Send results through channel
        }(url)
    }

    for i := 0; i < len(urls); i++ {
        r := <-resultChannel // Receive results one by one
        results[r.string] = r.bool
    }

    return results
}

This works because:

  • Each website check runs in parallel
  • Channels ensure safe communication between goroutines
  • No more race conditions!

The Result: 100x Speed Boost!

Our benchmark shows:

  • Original: ~2.25 seconds
  • Concurrent version: ~0.023 seconds

That’s like making 100 cups of tea in the time it used to take to make 1! (If only real life worked this way…)

Key Takeaways

  1. Goroutines are cheap threads for concurrent tasks
  2. Channels safely pass data between goroutines
  3. Always test concurrent code (Go’s race detector helps!)
  4. Remember: Make it work → Make it right → Make it fast

Next up: select - for even more concurrency control!

Support my open source work 🚀

Let's Connect

I'm open to discussing new opportunities. Feel free to connect with me or send me an email at [email protected]