Introducing Gophers, The generic collections package for Go

November 20, 2024

After learning Scala for a small project at work, I was amazed at the flexibility and power of the standard collections library. The sheer number of useful methods associated with Lists, Vectors, Seqs, and Sets really impressed me. It forced me to think of the problem I was solving as a series of concise transformations on collections of data, nothing more, nothing less.

Jumping back to Go, I realized that I was missing out on the same conveniences and wanted to do something about it.

With Generics and Iterators now fully supported in Go since 1.18 and 1.23 respectively, I realized that this was the perfect time to build my own generic collections package and open source it.

Gophers

Gophers Logo

After tinkering with a few different approaches and running into a few gotchas, I finally settled on an API that felt natural and easy to use.

Before diving into package details, let's take a look at a few examples of how you can use Gophers.

Installation

go get github.com/charbz/gophers

Example Usage

import (
  "github.com/charbz/gophers/list"
)

// Create some custom data type Foo
type Foo struct {
  a int
  b string
}

// Create a List of Foo
foos := list.NewList([]Foo{
  {a: 1, b: "one"}, 
  {a: 2, b: "two"}, 
  {a: 3, b: "three"}, 
  {a: 4, b: "four"}, 
  {a: 5, b: "five"},
})

// Now the fun begins


foos.Filter(func(f Foo) bool { return f.a%2 == 0 }) 
// output:
// {{2 two} {4 four}}

foos.FilterNot(func(f Foo) bool { return f.a%2 == 0 }) 
// output:
// {{1 one} {3 three} {5 five}}

foos.Find(func(f Foo) bool { return f.a == 3 }) 
// output:
// {a: 3, b: "three"}

foos.Partition(func(f Foo) bool { return len(f.b) == 3 })
// output:
// {{1 one} {2 two}} , {{3 three} {4 four} {5 five}}

foos.SplitAt(3) 
// output:
// {{1 one} {2 two} {3 three} {4 four}} , {{5 five}}

foos.Count(func(f Foo) bool { return f.a < 3 }) 
// output:
// 2

bars := foos.Concat(list.NewList([]Foo{{a: 1, b: "one"}, {a: 2, b: "two"}})) 
// output:
// {{1 one} {2 two} {3 three} {4 four} {5 five} {1 one} {2 two}}

bars.Distinct(func(i Foo, j Foo) bool { return i.a == j.a }) 
// output:
// {{1 one} {2 two} {3 three} {4 four} {5 five}}

foos.Apply(
  func(f Foo) Foo {
    f.a *= 2
    f.b += " * two"
    return f
  }
)
// output:
// {{2 one * two} {4 two * two} {6 three * two} {8 four * two} {10 five * two}}

That's just the tip of the iceberg, the package is filled with tons of useful methods for building and transforming collections, as well as support for a variety of different collection types. Check out the Readme or the GoDocs for more details.

Design Decisions

First I created an interface that would represent any Collection in the package:

type Collection[T any] interface {
	Add(T)
	Length() int
	New(s ...[]T) Collection[T]
	Values() iter.Seq[T]
}

At a minimum, a collection must be able to add some element to itself (regardless of insertion order), return the number of elements it contains, and return an iterator over its elements.

Then, I quickly realized that some of the methods I wanted to build such as Filter, Map, Reduce can apply to all collection types, while others such as Reverse, Sort, SplitAt only apply to ordered collections where index-based access is meaningful.

This is where the OrderedCollection interface comes in.

type OrderedCollection[T any] interface {
	Collection[T]
	At(index int) T
	All() iter.Seq2[int, T]
	Backward() iter.Seq2[int, T]
	Slice(start, end int) OrderedCollection[T]
	NewOrdered(s ...[]T) OrderedCollection[T]
}

The interfaces are designed to be as minimal as possible, I only wanted to include the methods that would be enough to qualify something as a collection, while the fancier methods can be added to the concrete types. Then I proceeded to implement a few basic collection types that would satisfy the interfaces.

Collection Types:

Sequence
  • an ordered collection wrapping a Go slice.
  • great for fast random access.

ComparableSequence
  • a Sequence of comparable elements.
  • offers extra functionality.

List
  • an ordered collection wrapping a linked list.
  • great for fast insertion, removal, and implementing stacks and queues.

ComparableList
  • a List of comparable elements.
  • offers extra functionality.

Set
  • a hash set of unique elements.
  • offers set operations such as union, intersection, and diff.

Generic Functions

In order to reduce code duplication, I created a few generic functions that accept any Collection[T] as an argument, apply some transfomration, and return a new Collection[T] as the result. These can be found in the functions.go and ordered_functions.go files.

Here's an example of the generic Filter function:

// Filter returns a new collection containing only the elements that
// satisfy the predicate function.
//
// example usage:
//
//	numbers := NewSequence([]int{1,2,3,4,5,6})
//	Filter(numbers, func(t int) bool { return t % 2 == 0 })
//
// output:
//
//	[2,4,6]
func Filter[T any](s Collection[T], f func(T) bool) Collection[T] {
	result := s.New()
	for v := range s.Values() {
		if f(v) {
			result.Add(v)
		}
	}
	return result
}

Collection Methods

Now that I have my logic written in a generic function, I can create a new method on the concrete collection types that call the generic function. This would allow me to write something like sequence.Filter(...) which is more natural.

func (s *Sequence[T]) Filter(f func(T) bool) *Sequence[T] {
	return Filter(s, f).(*Sequence[T])
}

This pattern is followed across the board except in places where the concrete type needs to implement the method natively for efficiency.

What's Next?

I'm excited to see where this project goes, I have a few more ideas of how to expand the package and make it more useful.

  • Add more collection types
  • Add more generic functions
  • Add benchmarks
  • Improve the efficiency of some methods
  • Add Iterators: instead of returning a slice, return an iterator that yields values one at a time.
  • Add support for Monads

Contributing

I welcome any and all contributions to the project, whether it be feature requests, bug reports, or code contributions. Please open an issue or submit a PR if you have any ideas or find any bugs.

To stay in touch, I have a Discord server where we can chat about the project and share ideas.

Happy coding!