Skip to main content

Golang Concurrency

·736 words·4 mins·
Table of Contents
Lessons learned from `Learn Go with Tests` - This article is part of a series.
Part 6: This Article

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.

Lessons learned from `Learn Go with Tests` - This article is part of a series.
Part 6: This Article