How to Use container/ring in Go

The `container/ring` package provides a circular linked list data structure for storing elements in a fixed-size loop. Create a new ring with `New(n)`, set the `Value` field of each node to store data, and use `Next()` or `Prev()` to traverse the list.

The round-robin loop

You are building a toy operating system simulator. Three processes need CPU time. Process A runs, then B, then C, then back to A. You could use a slice and an index that wraps around, but you also need to remove processes when they finish and insert new ones without shifting everything in memory. A circular linked list handles the wrapping naturally and lets you splice nodes in and out without moving data. That is where container/ring comes in.

The package provides a circular linked list for storing elements in a fixed-size loop. You create a ring, populate nodes, and traverse forward or backward forever. The ring has no beginning and no end. You hold a reference to one node, and that reference defines your current position.

What a ring is

A ring is a circular linked list. Every node points to the next node, and the last node points back to the first. There is no head and no tail. The structure maintains a count of elements and a pointer to the current node. When you move forward, the current pointer advances. When you move backward, it retreats. The ring wraps automatically at the boundaries.

Think of a round table with chairs. Each chair holds a value. You stand at one chair. You can walk clockwise to the next chair or counter-clockwise to the previous one. If you walk past the last chair, you end up at the first. The table never runs out of chairs. You can add a chair next to where you stand or remove the chair you are standing on. The circle remains intact.

The ring has no start. You define the start by where you are.

Minimal example

Here is the basic setup: create a ring, populate it, and walk the loop.

package main

import (
	"container/ring"
	"fmt"
)

func main() {
	// New(3) allocates three nodes linked in a circle.
	// The ring starts pointing at the first node.
	r := ring.New(3)

	// Set the value on the current node.
	r.Value = "alpha"

	// Move to the next node and set its value.
	// Next() returns the next node, updating the cursor.
	r = r.Next()
	r.Value = "beta"

	// Move again. The ring wraps around automatically.
	r = r.Next()
	r.Value = "gamma"

	// Do() walks the ring n times, calling the function on each node.
	// It starts from the current node and stops after n steps.
	r.Do(func(p *ring.Ring) {
		fmt.Println(p.Value)
	})
}
# output:
alpha
beta
gamma

Go code should always be formatted with gofmt. The standard library tools enforce a single style, so you never argue about indentation. Most editors run this automatically on save.

How the ring works

ring.New(n) allocates n nodes. If n is zero, it returns a nil ring. If n is negative, it panics at runtime. The ring structure keeps a pointer to the current node and a cached count of elements. The count allows Len() to run in constant time.

When you call Next(), the ring updates the current pointer to the next node and returns it. The ring always maintains the invariant that the last node's next pointer points to the first node. Do() uses this structure to iterate exactly n times, where n is the length of the ring. It does not rely on a sentinel value to stop. It counts steps.

If you call Do() on a nil ring, nothing happens. The method checks for nil and returns immediately. This is safe but can hide bugs if you expected work to happen. Always check Len() before iterating if you need to distinguish between an empty ring and a nil ring.

New allocates nodes. Next moves the cursor. Do iterates by count.

Navigation and movement

The ring supports moving forward and backward. Move(k) shifts the current node by k steps. Positive k moves forward. Negative k moves backward. The ring handles the wrapping internally. You can jump to any relative position in O(k) time. This is useful for skipping tasks or looking ahead.

package main

import (
	"container/ring"
	"fmt"
)

func main() {
	// Create a ring with five nodes.
	r := ring.New(5)
	for i := 0; i < 5; i++ {
		r.Value = i
		r = r.Next()
	}

	// Move forward two steps.
	// The ring wraps around if k exceeds the length.
	r = r.Move(2)
	fmt.Println("After Move(2):", r.Value)

	// Move backward three steps.
	// Negative k moves in reverse direction.
	r = r.Move(-3)
	fmt.Println("After Move(-3):", r.Value)

	// Prev() is equivalent to Move(-1).
	r = r.Prev()
	fmt.Println("After Prev():", r.Value)
}
# output:
After Move(2): 2
After Move(-3): 4
After Prev(): 3

Move handles wrapping. Negative steps go backward. The ring never runs out of bounds.

Realistic example: Scheduler

Real code needs to modify the structure. Here is a scheduler that removes finished tasks and inserts new ones. The Scheduler struct wraps the ring and exposes methods for adding and running tasks.

package main

import (
	"container/ring"
	"fmt"
)

// Scheduler manages a ring of tasks.
// The receiver name s matches the type Scheduler.
type Scheduler struct {
	ring *ring.Ring
}

// NewScheduler creates a scheduler with initial capacity.
func NewScheduler(capacity int) *Scheduler {
	return &Scheduler{
		ring: ring.New(capacity),
	}
}

// AddTask inserts a task after the current position.
// Link inserts a new ring after the current node.
func (s *Scheduler) AddTask(name string) {
	// Link creates a new node and splices it into the ring.
	// The return value is the ring starting from the node after the insertion.
	s.ring = s.ring.Link(ring.New(1))
	s.ring.Value = name
}

// RunNext advances to the next task and removes it.
// Unlink removes the current node and returns the rest of the ring.
func (s *Scheduler) RunNext() string {
	if s.ring.Len() == 0 {
		return ""
	}

	// Move to the next node.
	s.ring = s.ring.Next()

	// Unlink removes the current node.
	// The ring shrinks by one element.
	// Capture the return value to keep the ring pointer valid.
	task := s.ring.Value
	s.ring = s.ring.Unlink(1)

	return fmt.Sprintf("%v", task)
}

func main() {
	sched := NewScheduler(0)
	sched.AddTask("compile")
	sched.AddTask("test")
	sched.AddTask("deploy")

	// Run tasks until the ring is empty.
	for sched.ring.Len() > 0 {
		fmt.Println("Running:", sched.RunNext())
	}
}
# output:
Running: compile
Running: test
Running: deploy

The Scheduler struct uses a lowercase field name for internal state, following Go's visibility rules. Public methods start with a capital letter. The receiver name s is one letter matching the type.

Unlink returns the ring. Capture the return value or lose the structure.

Merging rings

You can merge two rings. Link(other) splices other into the ring after the current node. The result is a single ring with combined length. The current node remains the same. This is efficient because it just rewires pointers. No copying occurs.

package main

import (
	"container/ring"
	"fmt"
)

func main() {
	// Create two separate rings.
	r1 := ring.New(2)
	r1.Value = "A"
	r1 = r1.Next()
	r1.Value = "B"

	r2 := ring.New(2)
	r2.Value = "C"
	r2 = r2.Next()
	r2.Value = "D"

	// Link r2 into r1 after the current node.
	// r1 now contains four nodes.
	r1 = r1.Link(r2)

	// Iterate the combined ring.
	r1.Do(func(p *ring.Ring) {
		fmt.Print(p.Value, " ")
	})
}
# output:
A B C D

Link rewires pointers. No copying happens. The merge is constant time.

Pitfalls and errors

ring.New(n) panics if n is negative. The compiler will not catch this if n comes from a variable. You get a runtime panic with runtime error: ring: negative size. Validate input before creating the ring.

Calling Do while modifying the ring is dangerous. If the callback function calls Unlink or Link, the iteration might skip nodes or loop forever. The documentation warns that modifications during Do are not guaranteed to visit all nodes. If you need to remove items while iterating, collect the items to remove first, then unlink them. Or use a manual loop with Next and check conditions carefully.

Link and Unlink return the ring starting from the node after the operation. If you ignore the return value, your ring pointer becomes stale. It might point to a detached segment or a node that is no longer part of the main ring. The compiler will not stop you from ignoring the return value, but your logic will break. Always assign the result back to your ring variable.

Nil rings are safe to call but silent. Check length if emptiness matters.

Decision matrix

Use a ring when you need a circular buffer where insertion and removal happen at the cursor without shifting elements. Use a slice with an index when you have fixed-size rotation and random access is more important than splicing. Use a channel when multiple goroutines need to produce and consume items concurrently. Use a container/list when you need a doubly-linked list with explicit front and back operations but no circular wrapping. Use plain sequential code when the data is small and the overhead of a linked list outweighs the benefits.

Rings are for loops. Slices are for arrays. Pick the shape that matches the access pattern.

Where to go next