Home Go: Select for concurrency
Post
Cancel

Go: Select for concurrency

Select in Go

In the context of Go programming language, the select statement is used to choose between multiple communication operations on channels. It allows a Go program to perform non-blocking operations on channels, enabling the program to wait for one or more channels to be ready for communication.

The select statement works by evaluating the communication operations in the order they appear in the code, and if multiple operations are ready, it randomly selects one of them to execute. It can be used in conjunction with channel sends and receives, allowing the program to perform different actions depending on which channel becomes ready first.

Code Usage

Let’s say we are to build a Racer that determines which HTTP Server returns the response faster.

To test the code, we want to test which URL returns faster.

1
2
3
4
5
6
7
8
9
10
11
func TestRacer(t *testing.T) {
	slowURL := "http://www.facebook.com"
	fastURL := "http://www.quii.dev"

	want := fastURL
	got := Racer(slowURL, fastURL)

	if got != want {
		t.Errorf("got %q, want %q", got, want)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
}

So we take two URLs and make them race with each other, hence returning the faster website.

  • time.Since(): takes in a timestamp object as its parameter and then automatically calculates the elapsed time
  • http.Get: Perform an HTTP GET request against the URL. The function will either return an http.Response or error.

However when we access HTTP requests, we don’t want to directly use the real external services because they can be rather slow and flaky. Instead, we can use the net/http/httptest package to mock an HTTP server.

So now, we switch over to use mocks in our tests.

Mocking

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func TestRacer(t *testing.T) {

	slowServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(20 * time.Millisecond)
		w.WriteHeader(http.StatusOK)
	}))

	fastServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))

	slowURL := slowServer.URL
	fastURL := fastServer.URL

	want := fastURL
	got := Racer(slowURL, fastURL)

	if got != want {
		t.Errorf("got %q, want %q", got, want)
	}

	slowServer.Close()
	fastServer.Close()
}

http.HandlerFunc is the goto standard for defining a real HTTP server in Go. However wrapping it in a httptest.NewServer makes it easier for us to test the HTTP server. We send in the http.HandlerFunc via an anonymous function.

The slowServer adds an additional 20 milliseconds sleep deliberately for the test case.

Refactoring

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func Racer(a, b string) (winner string) {
	aDuration := measureResponseTime(a)
	bDuration := measureResponseTime(b)

	if aDuration < bDuration {
		return a
	}

	return b
}

func measureResponseTime(url string) time.Duration {
	start := time.Now()
	http.Get(url)
	return time.Since(start)
}

We remove the repetitiveness in measuring the elapsed time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func TestRacer(t *testing.T) {

	slowServer := makeDelayedServer(20 * time.Millisecond)
	fastServer := makeDelayedServer(0 * time.Millisecond)

	defer slowServer.Close()
	defer fastServer.Close()

	slowURL := slowServer.URL
	fastURL := fastServer.URL

	want := fastURL
	got := Racer(slowURL, fastURL)

	if got != want {
		t.Errorf("got %q, want %q", got, want)
	}
}

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)
	}))
}

And go through the same process for the testing too. Here we notice a new term: defer. The defer prefix for the function call means that the function will be called at the end of the containing function. For this case, it will serve to close the server and not leave any hanging ports or resources that need to be cleaned up. The defer prefix guarantees that the function will be called but can be also called near the declaration for code reading-efficiency.

Synchronising Processes

##

But with Go, we have a great tool called concurrency at hand. This abstracts the exact response times and only cares about which one is the actual one to come back sooner.

This is where select comes in.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
}

Ping

The ping creates a chan struct{} because we don’t care what type is sent to the channel. Just signal once it’s done.

But why struct?

Structs are the smallest data type from a memory perspective as it doesn’t require allocation

Defining Channels

Using make to create a channel is preferable over declaring var ch chan struct{} as the var would automatically initialize the value with zero for integers and “” for strings. For channels, the default initialization is nil which is problematic when sending as such values will be blocked.

Select

Once a value is sent to a channel, you can wait for values with := ←ch which is a blocking call whilst waiting.

If you want to wait on multiple channels, use select. The first value to be received will “win”.

In our code above, ping will set up two channels and whichever writes to the channel first will be executed.

Add error if HTTP takes more than 10 seconds

1
2
3
4
5
6
7
8
9
10
11
12
13
t.Run("returns an error if a server doesn't respond within 10s", func(t *testing.T) {
	serverA := makeDelayedServer(11 * time.Second)
	serverB := makeDelayedServer(12 * time.Second)

	defer serverA.Close()
	defer serverB.Close()

	_, err := Racer(serverA.URL, serverB.URL)

	if err == nil {
		t.Error("expected an error but didn't get one")
	}
})
1
2
3
4
5
6
7
8
9
10
func Racer(a, b string) (winner string, error error) {
	select {
	case <-ping(a):
		return a, nil
	case <-ping(b):
		return b, nil
	case <-time.After(10 * time.Second):
		return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
	}
}

So ultimately, we want to add an additional case that returns an error if the function exceeds 10 seconds. For this we use time.After which returns a chan to signal it down after the specified time.

Final Refactoring

However the test takes 10 seconds to run, so we want to make the timeout configurable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var tenSecondTimeout = 10 * time.Second

func Racer(a, b string) (winner string, error error) {
	return ConfigurableRacer(a, b, tenSecondTimeout)
}

func ConfigurableRacer(a, b string, timeout time.Duration) (winner string, error 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)
	}
}

So we add a timeout to manually configure our Racer, now called ConfigurableRacer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func TestRacer(t *testing.T) {

	t.Run("compares speeds of servers, returning the url of the fastest one", func(t *testing.T) {
		slowServer := makeDelayedServer(20 * time.Millisecond)
		fastServer := makeDelayedServer(0 * time.Millisecond)

		defer slowServer.Close()
		defer fastServer.Close()

		slowURL := slowServer.URL
		fastURL := fastServer.URL

		want := fastURL
		got, err := Racer(slowURL, fastURL)

		if err != nil {
			t.Fatalf("did not expect an error but got one %v", err)
		}

		if got != want {
			t.Errorf("got %q, want %q", got, want)
		}
	})

	t.Run("returns an error if a server doesn't respond within the specified time", func(t *testing.T) {
		server := makeDelayedServer(25 * time.Millisecond)

		defer server.Close()

		_, err := ConfigurableRacer(server.URL, server.URL, 20*time.Millisecond)

		if err == nil {
			t.Error("expected an error but didn't get one")
		}
	})
}

Now, we can separate the two functionalities we want to test. So in our first test, where we race to servers to see which returns faster, we use the default Racer which gives a 10 seconds timeout. And then in our second test we check whether the function returns an error properly if the HTTP connections take longer than our configured time duration.

Finale

Note that the select statement does not block if none of the communication operations are ready. It allows the program to continue execution without waiting indefinitely for a channel operation to complete. This post was influenced by Learn Go with tests.

This post is licensed under CC BY 4.0 by the author.

Go: Mocking

OLTP vs. OLAP: How Workloads Shape Database Design