Following the Learn Go with Tests guide, exploring Go’s
selectstatement.
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
}selectblocks until one case can proceeddefaultmakes 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
}- Both
pingfunctions start goroutines simultaneously selectwaits for the first channel to close- 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.NewServerstarts a real HTTP server on a random porthttp.HandlerFuncconverts a function into an HTTP handlertime.Sleep(delay)simulates network latencyw.WriteHeader(http.StatusOK)sends a 200 responseserver.URLprovides 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, thenslow.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.