Skip to main content
Go: Select Statement
  1. Posts/

Go: Select Statement

·504 words·3 mins·
Roman
Author
Roman
Photographer with MSci in Computer Science and a Home Lab obsession
Table of Contents

Following the Learn Go with Tests guide, exploring Go’s select statement.

Choosing Between Channels
#

Imagine you need to:

  • Wait for multiple channel operations
  • Handle timeouts
  • Race multiple goroutines

The select statement is Go’s solution for this.

Select Syntax
#

select {
case <-channel1:
    // Handle channel1
case data := <-channel2:
    // Handle channel2 with data
default:
    // Non-blocking fallback
}
  • select blocks until one case can proceed
  • default makes it non-blocking, if neither of the channels are ready

Website Race Example
#

Let’s build a function that races two URLs and returns the fastest response:

func Racer(a, b string) (winner string) {
    startA := time.Now()
    http.Get(a)
    aDuration := time.Since(startA)
    
    startB := time.Now()
    http.Get(b)
    bDuration := time.Since(startB)
    
    if aDuration < bDuration {
        return a
    }
    
    return b
}

Problem: Sequential, not actually racing!

Concurrent Racing with Select
#

func Racer(a, b string) (winner string) {
    select {
    case <-ping(a):
        return a
    case <-ping(b):
        return b
    }
}

func ping(url string) chan struct{} {
    ch := make(chan struct{})
    go func() {
        http.Get(url)
        close(ch)
    }()
    return ch
}
  1. Both ping functions start goroutines simultaneously
  2. select waits for the first channel to close
  3. Returns the URL of the winning channel

Adding Timeouts
#

func Racer(a, b string, timeout time.Duration) (winner string, err error) {
    select {
    case <-ping(a):
        return a, nil
    case <-ping(b):
        return b, nil
    case <-time.After(timeout):
        return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
    }
}

time.After returns a channel that will receive a value after the specified duration.

Testing Network Requests
#

Go’s httptest package provides tools for creating test servers without the complexity of real network operations.

httptest.Server
#

The httptest.Server creates a local HTTP server for testing:

func makeDelayedServer(delay time.Duration) *httptest.Server {
    return httptest.NewServer(http.HandlerFunc(
        func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(delay)
        w.WriteHeader(http.StatusOK)
    }))
}
  • httptest.NewServer starts a real HTTP server on a random port
  • http.HandlerFunc converts a function into an HTTP handler
  • time.Sleep(delay) simulates network latency
  • w.WriteHeader(http.StatusOK) sends a 200 response
  • server.URL provides the test server address (e.g., http://127.0.0.1:54321)
  • server.Close() shuts down the server and frees the port

This approach gives us:

  • Predictable timing - we control exactly how long each “server” takes
  • No external dependencies - tests run offline
  • Consistent results - no network variability

Test Structure
#

func TestRacer(t *testing.T) {
    slow := makeDelayedServer(20 * time.Millisecond)
    fast := makeDelayedServer(0 * time.Millisecond)
    defer slow.Close()
    defer fast.Close()
    
    got, _ := Racer(slow.URL, fast.URL, 10*time.Second)
    
    if got != fast.URL {
        t.Errorf("expected fast server")
    }
}

func makeDelayedServer(delay time.Duration) *httptest.Server {
    return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(delay)
        w.WriteHeader(http.StatusOK)
    }))
}

The defer Statement
#

defer schedules a function call to execute when the surrounding function returns:

defer slow.Close()  // Runs when TestRacer finishes
defer fast.Close()  // Runs when TestRacer finishes
  • Deferred calls execute in LIFO order - fast.Close() runs first, then slow.Close()
  • Always executes - even if the function panics or returns early
  • Perfect for cleanup - ensures resources are freed regardless of how the function exits

Defer is usually placed right next to the resource creating to make it more visible.