Skip to Content
Course content

102: Common Go Interview Questions on Goroutines and Channels

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you're heading into a Go interview, there is a very high chance you'll be asked to implement something that fetches data from multiple sources concurrently. It seems simple enough—just throw a go keyword in front of a function call, right? But that's exactly where most candidates stumble. Interviewers aren't looking to see if you know the syntax; they're looking to see if you understand how to prevent your application from leaking memory like a sieve.

The "Fire and Forget" Leak

Usually, the first instinct is to use a sync.WaitGroup to make sure all the work finishes before the function returns. I've seen this approach countless times. You spin up five goroutines to call five different APIs, you Add(1) to the group, and you Wait() at the bottom. On the surface, it works. But imagine if the user cancels the request or if you have a strict 200ms timeout for the whole operation.

// The "Naive" way: Danger of Goroutine Leaks
func FetchAllData(urls []string) []string {
    var wg sync.WaitGroup
    results := make(chan string)
    
    for _, url := range urls {
        wg.Add(1)
        go func(u string) {
            defer wg.Done()
            // Imagine this takes 10 seconds if the server is hanging
            data := slowNetworkCall(u) 
            results <- data
        }(url)
    }

    // What happens if we decide to stop waiting after 500ms?
    // If we return here, the goroutines above are still blocked 
    // trying to send to 'results', and they will NEVER be garbage collected.
    return collectResults(results) 
}

Here is the problem: in Go, a goroutine is not garbage collected if it's blocked. In the code above, if the main function returns early, those goroutines stay alive, suspended forever, trying to send data into a channel that no one is listening to anymore. In a high-traffic production service, this is how you crash a server—not with a panic, but by slowly consuming every byte of RAM with "zombie" goroutines.

Owning the Lifecycle with Context

To pass the interview (and write production-grade code), you need to demonstrate that you know how to signal a goroutine to stop working. This is where context.Context comes in. You don't just tell a goroutine to start; you give it a way to know when it's no longer needed. I always tell my juniors: "Never start a goroutine without knowing exactly how it will end."

// The "Professional" way: Context-aware concurrency
func FetchAllData(ctx context.Context, urls []string) ([]string, error) {
    results := make(chan string, len(urls)) // Buffered to prevent blocking
    
    for _, url := range urls {
        go func(u string) {
            // We wrap the network call in a select block
            resCh := make(chan string, 1)
            go func() { resCh <- slowNetworkCall(u) }()

            select {
            case <-ctx.Done():
                // The request timed out or was cancelled; exit immediately
                return
            case res := <-resCh:
                results <- res
            }
        }(url)
    }

    var finalResults []string
    for i := 0; i < len(urls); i++ {
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        case res := <-results:
            finalResults = append(finalResults, res)
        }
    }
    return finalResults, nil
}

The Trade-off: Buffer Size and Complexity

You'll notice I added a buffer to the results channel: make(chan string, len(urls)). This is a subtle but critical point. If the channel is unbuffered and the main loop exits (due to a timeout), any goroutine that finishes after the timeout will block forever on the send operation results <- res. By buffering the channel to the exact number of expected responses, we guarantee that every goroutine can finish its send and exit, regardless of whether the receiver is still there. It costs a tiny bit more memory upfront, but it's a cheap price to pay for a leak-free system.

When an interviewer asks "Why did you buffer that channel?", don't just say "to make it faster." Tell them you're ensuring that the worker goroutines aren't blocked on a send operation after the orchestrating function has already returned. That is the answer that separates a beginner from an engineer.




📋 Practical Task

Exercise: Building a Leak-Proof URL Status Checker

Your goal is to build a tool that checks the HTTP status of a list of URLs concurrently, but with a strict global timeout. If the timeout is hit, the function must return immediately, and no goroutines should be left running in the background.

Requirements:

  • Create a function CheckStatuses(ctx context.Context, urls []string) (map[string]int, error).
  • Use a context.WithTimeout in your main function to limit the total execution time to 1 second.
  • Each URL check should be performed in its own goroutine.
  • Use a buffered channel to collect the results (URL and status code).
  • Ensure that if the context is cancelled, all worker goroutines exit promptly.
  • Test it by including one URL that is intentionally slow (e.g., http://httpbin.org/delay/5) to verify that your program doesn't hang and returns the context deadline exceeded error correctly.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.