It turns out maps in go can be defined recursively, making them great for representing trees. I took advantage of this simple fact to create a lightweight generic package.

The basic definition of the datatype is this:

// node implements Node
type node[K comparable] map[K]Node[K]

That simple definition powers a very expressive interface:

type Node[K comparable] interface {
	Data() K
	Children() map[K]Node[K]
	Spawn(K) Node[K]
	RemoveChild(K)
	Parent() Node[K]
	Walk() [][]K
	Ancestry() []K
	IsTerminal() bool
	Set(K)
	Get(K) (Node[K], bool)
	SetParent(Node[K])
	fmt.Stringer
}

And now you have a fully functional tree! For example:

package main

import (
	"slices"
	"testing"

	ergotree "github.com/sean9999/go-ergonomic-tree"
)

func TestErgonomicTree(t *testing.T) {

	//  create a tree
	life := ergotree.New[string](nil)

	//  create some children
	greatApes := life.Spawn("Primates").Spawn("Apes").Spawn("Great Apes")

	//  create some leaf nodes
	greatApes.Set("Western Gorilla")
	greatApes.Set("Eastern Gorilla")

	//  walk the whole tree, returning full paths to all leaf nodes
	got := life.Walk()

	//  check the data
	want := [][]string{
		{"Primates", "Apes", "Great Apes", "Eastern Gorilla"},
		{"Primates", "Apes", "Great Apes", "Western Gorilla"},
	}
	if !(slices.Equal(got[0], want[0]) || slices.Equal(got[0], want[1])) {
		t.Errorf("got %v but wanted %v", got, want)
	}
	if !(slices.Equal(got[1], want[0]) || slices.Equal(got[1], want[1])) {
		t.Errorf("got %v but wanted %v", got, want)
	}

}