Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

I was working on a small internal tool the other day where I needed to rotate through a set of API keys. You know the drill: if the first key hits a rate limit, move to the second; if the last one hits a limit, go back to the first. My first instinct was to use a slice and a modulo operator on an integer index. It works, but it feels like I'm managing too much state manually.

That's when I remembered container/ring. It's one of those packages that stays in the shadows, but for circular data structures, it's exactly what we need. Let's dive in and see how it actually behaves, because the API is a bit quirkier than a standard list.

The "Wait, where does the data go?" moment

I'll start by creating a ring of three elements to represent my API keys. I'm expecting something like a Push() method, but looking at the docs, ring.New(n) just gives me a ring of size n with empty values.

package main

import (
	"fmt"
	"container/ring"
)

func main() {
	r := ring.New(3)
	fmt.Println("Initial ring value:", r.Value)
}

Running this, I get Initial ring value: <nil>. Right. The ring is already built; it's just empty. To actually put my keys in, I have to manually traverse the ring and assign values to the Value field of each element. This feels a bit tedious at first, but it's how the structure is designed.

	r := ring.New(3)
	r.Value = "Key_A"
	r = r.Next()
	r.Value = "Key_B"
	r = r.Next()
	r.Value = "Key_C"
    
    // Now I'm at Key_C. If I call Next() again...
    r = r.Next()
    fmt.Println("Back to:", r.Value) // Should be Key_A

That works. It's a perfect circle. But here is where I almost tripped up: the Next() method doesn't "advance" the ring object itself; it returns a pointer to the next element. If I want to keep track of my "current" key, I have to reassign my variable to the result of Next().

Moving without losing my place

What if I want to jump forward a few steps? I don't want to call Next() in a loop. There's a Move(n) method, but I noticed something strange when I first tried it. Let's see.

	r := ring.New(3)
	r.Value = "A"
	r.Next().Value = "B"
	r.Next().Next().Value = "C"

	fmt.Println("Start:", r.Value) // A
	r.Move(1)
	fmt.Println("Move 1:", r.Value) // B
	r.Move(2)
	fmt.Println("Move 2:", r.Value) // A (Wait, why A?)

I paused there for a second. I moved 1 (to B), then I moved 2 more. B $\rightarrow$ C $\rightarrow$ A. So I'm back at A. The Move method is relative to the current position of the ring pointer. It doesn't reset to the "start" of the ring because, well, in a circular list, there is no start. It's just a loop.

Splicing the circle

Now, let's say one of my API keys gets revoked. I need to remove it from the rotation. I tried looking for a Remove() method, but it doesn't exist. Instead, we have Unlink(n).

Unlink(n) removes the element n steps ahead of the current one. If I'm at "A" and I want to remove "B" (which is 1 step ahead), I call r.Unlink(1).

	r := ring.New(3)
	r.Value = "A"
	r.Next().Value = "B"
	r.Next().Next().Value = "C"

	r.Unlink(1) // Remove "B"

	// Let's see what's left
	fmt.Println(r.Value)        // A
	fmt.Println(r.Next().Value) // C
	fmt.Println(r.Next().Next().Value) // A (Circle closed!)

The ring automatically heals itself. "A" now points directly to "C", and "C" points back to "A". It's a very clean way to handle a dynamic pool of resources without having to slice-and-dice arrays and worry about off-by-one errors when wrapping around the end of the list.

One final tip: if you ever need to check if you've come full circle while iterating, you can't just check for nil because the ring never ends. You have to store the original pointer to the starting element and compare your current pointer to it.




📋 Practical Task

Exercise: Round-Robin Load Balancer Simulator

Build a small program that simulates a load balancer distributing requests across a set of backend servers using container/ring.

  • Initialize a ring with 4 server names (e.g., "Server-1", "Server-2", etc.).
  • Create a function getNextServer(r *ring.Ring) string that returns the current server's name and then moves the ring pointer forward by one for the next call.
  • Simulate 10 incoming requests by calling getNextServer in a loop and printing which server handled each request.
  • Halfway through the simulation (after 5 requests), use Unlink to remove "Server-2" from the rotation.
  • Observe and print the output to verify that the requests now cycle through only the remaining 3 servers.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.