Goroutines #
Goroutines are Golang’s way of handling concurrency via channels.
package main
import (
"reflect"
"testing"
)
type WebsiteChecker func(string) bool
type result struct {
string
bool
}
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)}
}(url)
}
for i := 0; i < len(urls); i++ {
r := <-resultChannel
results[r.string] = r.bool
}
return results
}
func mockWebsiteChecker(url string) bool {
return url != "waat://furhurterwe.geds"
}
func TestCheckWebsites(t *testing.T) {
websites := []string{
"http://google.com",
"http://blog.gypsydave5.com",
"waat://furhurterwe.geds",
}
want := map[string]bool{
"http://google.com": true,
"http://blog.gypsydave5.com": true,
"waat://furhurterwe.geds": false,
}
got := CheckWebsites(mockWebsiteChecker, websites)
if !reflect.DeepEqual(want, got) {
t.Fatalf("wanted %v, got %v", want, got)
}
}
=== RUN TestCheckWebsites
--- PASS: TestCheckWebsites (0.00s)
PASS
Select #
Think of select as a switch statement for channels. It allows you
to wait on multiple channels at the same time. It blocks until one of the channels in the case
statement block returns
something.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
)
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)
}
}
func ping(url string) chan struct{} {
ch := make(chan struct{})
go func() {
_, _ = http.Get(url)
close(ch)
}()
return ch
}
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")
}
},
)
}
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)
},
),
)
}
=== RUN TestRacer
=== RUN TestRacer/compares_speeds_of_servers,_returning_the_url_of_the_fastest_one
=== RUN TestRacer/returns_an_error_if_a_server_doesn't_respond_within_the_specified_time
--- PASS: TestRacer (0.04s)
--- PASS: TestRacer/compares_speeds_of_servers,_returning_the_url_of_the_fastest_one (0.02s)
--- PASS: TestRacer/returns_an_error_if_a_server_doesn't_respond_within_the_specified_time (0.03s)
PASS
time.After #
time.After is a handy function when using select. It makes sure that select
can’t
block forever. In this case it outputs an error to the user if the ping takes longer than 10 seconds.
Mutex #
To create structs than can be safely used concurrently golang also provides Mutex. This ensures that only one goroutine can access a thing at a time.
A Mutex is a mutual exclusion lock. The zero value for a Mutex is an unlocked mutex.
package main
import (
"sync"
"testing"
)
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *Counter) Value() int {
return c.value
}
func NewCounter() *Counter {
return &Counter{}
}
func TestCounter(t *testing.T) {
t.Run(
"incrementing the counter 3 times leaves it at 3",
func(t *testing.T) {
counter := NewCounter()
counter.Inc()
counter.Inc()
counter.Inc()
assertCounter(t, counter, 3)
},
)
t.Run("it runs safely concurrently", func(t *testing.T) {
wantedCount := 1000
counter := NewCounter()
var wg sync.WaitGroup
wg.Add(wantedCount)
for i := 0; i < wantedCount; i++ {
go func() {
counter.Inc()
wg.Done()
}()
}
wg.Wait()
assertCounter(t, counter, wantedCount)
})
}
func assertCounter(t testing.TB, got *Counter, want int) {
t.Helper()
if got.Value() != want {
t.Errorf("got %d, want %d", got.Value(), want)
}
}
=== RUN TestCounter
=== RUN TestCounter/incrementing_the_counter_3_times_leaves_it_at_3
=== RUN TestCounter/it_runs_safely_concurrently
--- PASS: TestCounter (0.00s)
--- PASS: TestCounter/incrementing_the_counter_3_times_leaves_it_at_3 (0.00s)
--- PASS: TestCounter/it_runs_safely_concurrently (0.00s)
PASS
Channels vs Mutex #
The go wiki has a dedicated page discussing when to use one or the other
A common Go newbie mistake is to over-use channels and goroutines just because it’s possible, and/or because it’s fun. Don’t be afraid to use a sync.Mutex if that fits your problem best. Go is pragmatic in letting you use the tools that solve your problem best and not forcing you into one style of code.