Go Guide

DP, math & design · Lesson 44 of 62 · about 180 min

Capstone: ship a Go DSA package

Turn what you built into a public, documented, tested and benchmarked Go module that others can import.

On this page · 10 sections
  1. 01Plan the layout
  2. 02Design the API
  3. 03A complete package: generic heap
  4. 04Test and benchmark everything
  5. 05Continuous integration
  6. 06Release
  7. 07A README that earns stars
  8. 08What mastery looks like
  9. 09Check your understanding
  10. 10Practice in Go

Plan the layout#

godsa/
├── go.mod                module github.com/you/godsa
├── README.md             what it is, install, examples, benchmark table
├── LICENSE               MIT or Apache-2.0
├── .github/workflows/ci.yml
├── stack/   stack.go  stack_test.go  example_test.go
├── queue/   …
├── heap/    …
├── dsu/     …
└── trie/    …

One package per structure gives short, readable call sites: heap.New(less), dsu.New(n), trie.New().

Design the API#

Good Go APIs are small and boring:

  • A constructor New… returning a pointer, unless the zero value is already usable.
  • Methods named for what they do: Push, Pop, Len, Find, Union.
  • (value, ok) or (value, error) returns instead of panics for expected situations like popping an empty stack.
  • Generics where the element type varies: Stack[T any], Heap[T any] with a less function, Set[T comparable].
  • An iterator (All() iter.Seq[T]) when callers want to range over contents.
  • Document complexity in the doc comment ("Push runs in O(log n)").

A complete package: generic heap#

Go
// Package heap provides a generic binary heap.
package heap
 
// Heap is a binary heap ordered by a less function.
// The zero value is not usable; create one with New.
type Heap[T any] struct {
	data []T
	less func(a, b T) bool
}
 
// New returns an empty heap. If less(a, b) reports a < b, Pop returns the smallest element.
func New[T any](less func(a, b T) bool) *Heap[T] {
	return &Heap[T]{less: less}
}
 
// Len returns the number of elements.
func (h *Heap[T]) Len() int { return len(h.data) }
 
// Push adds v in O(log n).
func (h *Heap[T]) Push(v T) { /* sift up, as in the heaps lesson */ }
 
// Pop removes and returns the top element in O(log n).
// ok is false when the heap is empty.
func (h *Heap[T]) Pop() (v T, ok bool) { /* sift down */ }

And its executable documentation:

Go
// example_test.go
package heap_test
 
import (
	"fmt"
 
	"github.com/you/godsa/heap"
)
 
func ExampleHeap() {
	h := heap.New(func(a, b int) bool { return a < b })
	for _, v := range []int{5, 1, 3} {
		h.Push(v)
	}
	v, _ := h.Pop()
	fmt.Println(v)
	// Output: 1
}

Using the package name heap_test tests the package from the outside, exactly as users will see it.

The whole package in one runnable file

Before splitting it into a module, it helps to have the complete heap in one file you can run in the Playground. This version fills in Push and Pop, and adds an All iterator so callers can range over the contents:

godsa_heap.go
Go
package main
 
import (
	"fmt"
	"iter"
)
 
// Heap is a binary heap ordered by a less function.
type Heap[T any] struct {
	data []T
	less func(a, b T) bool
}
 
// New returns an empty heap. If less(a, b) reports a < b, Pop returns the smallest element.
func New[T any](less func(a, b T) bool) *Heap[T] { return &Heap[T]{less: less} }
 
// Len returns the number of elements.
func (h *Heap[T]) Len() int { return len(h.data) }
 
// Push adds v in O(log n).
func (h *Heap[T]) Push(v T) {
	h.data = append(h.data, v)
	for i := len(h.data) - 1; i > 0; {
		p := (i - 1) / 2
		if !h.less(h.data[i], h.data[p]) {
			break
		}
		h.data[i], h.data[p] = h.data[p], h.data[i]
		i = p
	}
}
 
// Pop removes and returns the top element in O(log n). ok is false when the heap is empty.
func (h *Heap[T]) Pop() (v T, ok bool) {
	if len(h.data) == 0 {
		return v, false
	}
	v = h.data[0]
	last := len(h.data) - 1
	h.data[0] = h.data[last]
	h.data = h.data[:last]
	for i := 0; ; {
		small := i
		for _, c := range []int{2*i + 1, 2*i + 2} {
			if c < len(h.data) && h.less(h.data[c], h.data[small]) {
				small = c
			}
		}
		if small == i {
			break
		}
		h.data[i], h.data[small] = h.data[small], h.data[i]
		i = small
	}
	return v, true
}
 
// All yields the elements in internal (heap) order, without removing them.
func (h *Heap[T]) All() iter.Seq[T] {
	return func(yield func(T) bool) {
		for _, v := range h.data {
			if !yield(v) {
				return
			}
		}
	}
}
 
type job struct {
	name string
	prio int
}
 
func main() {
	nums := New(func(a, b int) bool { return a < b })
	for _, v := range []int{5, 1, 4, 2, 3} {
		nums.Push(v)
	}
	fmt.Print("heap order:")
	for v := range nums.All() {
		fmt.Print(" ", v)
	}
	fmt.Print("\npop order: ")
	for nums.Len() > 0 {
		v, _ := nums.Pop()
		fmt.Print(" ", v)
	}
	_, ok := nums.Pop()
	fmt.Println("\npop on empty, ok =", ok)
 
	// The same type works for any element, ordered by any rule.
	jobs := New(func(a, b job) bool { return a.prio > b.prio })
	jobs.Push(job{"email", 1})
	jobs.Push(job{"outage", 9})
	jobs.Push(job{"tests", 5})
	top, _ := jobs.Pop()
	fmt.Println("most urgent:", top.name)
}
Output
heap order: 1 2 4 5 3
pop order:  1 2 3 4 5
pop on empty, ok = false
most urgent: outage

"Heap order" is not sorted, only parent-before-children. Document that in the All comment so users are not surprised.

Test and benchmark everything#

Reuse the habits from Module 1: a table test per method including empty and single-element cases, a fuzz test where a property exists (popping everything from a heap returns sorted order), and benchmarks against the standard library or a naive version.

Here is the property test idea as a runnable program you can paste into the Playground:

property.go
Go
package main
 
import (
	"fmt"
	"math/rand"
	"slices"
)
 
type Heap[T any] struct {
	data []T
	less func(a, b T) bool
}
 
func New[T any](less func(a, b T) bool) *Heap[T] { return &Heap[T]{less: less} }
 
func (h *Heap[T]) Len() int { return len(h.data) }
 
func (h *Heap[T]) Push(v T) {
	h.data = append(h.data, v)
	for i := len(h.data) - 1; i > 0; {
		p := (i - 1) / 2
		if !h.less(h.data[i], h.data[p]) {
			break
		}
		h.data[i], h.data[p] = h.data[p], h.data[i]
		i = p
	}
}
 
func (h *Heap[T]) Pop() (T, bool) {
	var zero T
	if len(h.data) == 0 {
		return zero, false
	}
	top := h.data[0]
	last := len(h.data) - 1
	h.data[0] = h.data[last]
	h.data = h.data[:last]
	for i := 0; ; {
		l, r, m := 2*i+1, 2*i+2, i
		if l < len(h.data) && h.less(h.data[l], h.data[m]) {
			m = l
		}
		if r < len(h.data) && h.less(h.data[r], h.data[m]) {
			m = r
		}
		if m == i {
			break
		}
		h.data[i], h.data[m] = h.data[m], h.data[i]
		i = m
	}
	return top, true
}
 
// Property: pushing any values and popping them all yields sorted order.
func main() {
	failures := 0
	for trial := range 1000 {
		n := rand.Intn(50)
		in := make([]int, n)
		for i := range in {
			in[i] = rand.Intn(100) - 50
		}
		h := New(func(a, b int) bool { return a < b })
		for _, v := range in {
			h.Push(v)
		}
		var out []int
		for h.Len() > 0 {
			v, _ := h.Pop()
			out = append(out, v)
		}
		if !slices.IsSorted(out) || len(out) != n {
			failures++
			fmt.Println("trial", trial, "failed on", in)
		}
	}
	_, ok := New(func(a, b int) bool { return a < b }).Pop()
	fmt.Println("failures:", failures, "| pop on empty ok:", ok)
}
Output
failures: 0 | pop on empty ok: false

Continuous integration#

YAML
# .github/workflows/ci.yml
name: ci
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: "1.25"
      - run: go vet ./...
      - run: go test -race ./...

Release#

Shell
git tag v0.1.0
git push origin v0.1.0

Now anyone can run go get github.com/you/godsa@v0.1.0. pkg.go.dev picks up the documentation after the first time someone fetches it (you can request it on the site). Follow semantic versioning: bump the minor version for new features, and the major version (with a /v2 module path) only for breaking changes.

A README that earns stars#

  1. One sentence on what it is and why it exists.
  2. go get line and a 10-line example.
  3. A table of structures with their complexities.
  4. Benchmarks against the standard library, with machine and Go version.
  5. A short note on design choices (why generics, why (v, ok) instead of panics).

What mastery looks like#

You have reached the end of the path when you can:

  • write idiomatic Go that passes gofmt, go vet and staticcheck without thinking about it;
  • pick a data structure from a problem's constraints and explain its complexity;
  • write a table test before the solution, and a benchmark before claiming something is faster;
  • read standard library source and understand why it is written that way;
  • publish a package someone else can use.

Keep solving one or two problems a week with your own library, and read one standard library package a month. That is how the knowledge stays sharp.

Check your understanding

Answer before scrolling back up. If you miss one, reread the matching section, then try again.

  1. Q1.Where do Example functions show up?

  2. Q2.What makes go get github.com/you/godsa@v0.1.0 work?

  3. Q3.Why put each data structure in its own package (stack, heap, dsu)?

Practice in Go

Write a table test first, then the solution. On LeetCode, choose Go as the language. Open a hint only after 15 minutes stuck.

  • Publish the modulebuild

    Push github.com/you/godsa with packages stack, queue, heap, dsu and trie; every exported name documented; tag v0.1.0.

    Show hint

    git tag v0.1.0 && git push origin v0.1.0, then go list -m github.com/you/godsa@v0.1.0 from another folder.

  • CI on every pushbuild

    Add a GitHub Actions workflow running go vet, go test -race ./... and staticcheck.

    Show hint

    Use the official actions/setup-go action and pin a Go version.

  • Benchmarks in the READMEbuild

    Compare your heap with container/heap and your DSU with a naive version; publish the table in your README with your machine and Go version.

  • Use your own librarybuild

    Solve 10 problems from Modules 2 to 4 by importing your package. Fix any API that felt awkward, and release v0.2.0.

Finished this lesson?

Mark it done to track your progress. It is saved in this browser only.