We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.

This lesson's interactive features are locked, please to keep using them

Caching

It's time to implement caching! This will make moving around the map feel a lot snappier. We'll be building a flexible caching system to help with performance in future steps.

What Is a Cache?

A cache temporarily stores data so that future requests for that data can be served faster.

In our case, we'll be caching responses from the PokeAPI so that when we need that same data again, we can grab it from memory instead of making another network request.

Assignment

I used a Cache struct to hold a map[string]cacheEntry and a mutex to protect the map across goroutines. A cacheEntry should be a struct with two fields:

  • createdAt - A time.Time that represents when the entry was created.
  • val - A []byte that represents the raw data we're caching.

You'll probably want to expose a NewCache() function that creates a new cache with a configurable interval (time.Duration).

I used a time.Ticker to make this happen. If you want additional help, see the Tips section below.

Maps are not thread-safe in Go. You should use a sync.Mutex to lock access to the map when you're adding, getting entries or reaping entries. It's unlikely that you'll have issues because reaping only happens every ~5 seconds, but it's still possible, so you should make your cache package safe for concurrent use.

Run and submit the CLI tests from the root of the repo.

Tips

Clearing the Cache

You can use a time.Ticker inside a goroutine started by NewCache. In a loop like for range ticker.C { ... }, check the entries and remove any whose createdAt is older than the cache's interval.

Running Tests

You can run tests for all packages in a Go module by running go test ./... from the root of the module.

func TestAddGet(t *testing.T) {
	const interval = 5 * time.Second
	cases := []struct {
		key string
		val []byte
	}{
		{
			key: "https://example.com",
			val: []byte("testdata"),
		},
		{
			key: "https://example.com/path",
			val: []byte("moretestdata"),
		},
	}

	for i, c := range cases {
		t.Run(fmt.Sprintf("Test case %v", i), func(t *testing.T) {
			cache := NewCache(interval)
			cache.Add(c.key, c.val)
			val, ok := cache.Get(c.key)
			if !ok {
				t.Errorf("expected to find key")
				return
			}
			if string(val) != string(c.val) {
				t.Errorf("expected to find value")
				return
			}
		})
	}
}

func TestReapLoop(t *testing.T) {
	const baseTime = 5 * time.Millisecond
	const waitTime = baseTime + 5*time.Millisecond
	cache := NewCache(baseTime)
	cache.Add("https://example.com", []byte("testdata"))

	_, ok := cache.Get("https://example.com")
	if !ok {
		t.Errorf("expected to find key")
		return
	}

	time.Sleep(waitTime)

	_, ok = cache.Get("https://example.com")
	if ok {
		t.Errorf("expected to not find key")
		return
	}
}