DSA stands for Data Structures and Algorithms: the ways a program can hold its data, and the step-by-step methods that work on it. This course covers them in Go, in the order that builds on itself — arrays first, dynamic programming last. Every module is “build it from scratch, then use it”: you implement the structure with no library help, then solve classic problems with it. Eighty tests across ten modules check your work, so you always know when you’re done.
The course is a Go module: ten folders of lessons, exercise stubs, tests, and reference solutions, plus a small progress tool. Download it, unzip it, and work in your own editor — the tests run on your machine.
⬇ Download go-data-structures-and-algorithms.zip
Requirements: Go 1.22 or newer. No third-party dependencies. If go version doesn’t print anything yet, Install Go first.
unzip go-data-structures-and-algorithms.zip cd go-data-structures-and-algorithms go test -tags solutions ./...
Every lesson, exercise file, test file, and solution is also on this site, one page per module, with copy buttons on the code. The module pages are listed below.
Each module is build it from scratch, then use it: Part A has you implement the structure with no library help; later parts are classic problems that use it. Take them in order — each one leans on the ones before.
The foundation. Contiguous memory, O(1) indexing, and the slice machinery Go builds on top of it.
Build: Dynamic array with doubling. Solve: Rotation, Kadane, prefix sums, two pointers.
2Character manipulation, counting, sliding windows, and pattern search — with Go's bytes-versus-runes twist.
Solve: Palindromes, anagrams, sliding window, KMP.
3Nodes, pointers, and dynamic memory — the pointer-juggling skills trees and graphs depend on.
Build: Generic list with head/tail. Solve: Reverse, cycle detection, merge, n-th from end.
4Last in, first out. Matching, parsing, undo, and the monotonic-stack trick.
Build: Generic stack, min-stack. Solve: Brackets, RPN, monotonic stack.
5First in, first out. Ring buffers, deques, and the queue behind every breadth-first search.
Build: Ring buffer, two-stack queue. Solve: Josephus, sliding-window max, rate counter.
6Hash tables, collisions, and load factors — then the map-powered patterns that turn O(n²) into O(n).
Build: Hash map with chaining + resize. Solve: Two Sum, group anagrams, top-k, subarray sums.
7Hierarchies, recursion, traversals, and binary search trees.
Build: Binary search tree with delete. Solve: Traversals, validation, balance, LCA.
8Always know the smallest (or largest) item. Priority queues, top-k, and container/heap.
Build: Generic min-heap, heapify, heap sort. Solve: k-th largest, k-way merge, running median.
9Vertices and edges. BFS, DFS, topological order, Dijkstra, and union-find.
Build: Union-find. Solve: BFS, DFS, topological sort, Dijkstra, islands.
10Solve each subproblem once, remember the answer. Memoization, tabulation, and the classic DP families.
Solve: Coin change, LCS, edit distance, knapsack, LIS.
Every module folder has the same five files:
exercises.go and solution.go define the same functions behind opposite build tags, so only one is compiled at a time:
go test ./01-arrays/ # test YOUR exercises go test -tags solutions ./01-arrays/ # test the reference solutions go test -run TestRotateRight -v ./01-arrays/ # one test, verbose
README.md.exercises.go and work top to bottom. Each stub's comment gives the spec, an example, a hint at the pattern, and a target complexity.solution.go and compare. Different-but-correct is fine; notice where the reference is simpler.Try not to open solution.go until your tests pass, or until you've been stuck for a solid half hour. If having it in the folder is too tempting, hide solution.go in your editor's file explorer (in VS Code: "files.exclude": {"**/solution.go": true}).
The course ships with a progress tool. It runs every module’s tests and shows how far along you are, and which test to do next:
That’s a real run, part-way through: module 1 done and the first four string exercises written. The Next up line gives you the exact command for the next failing test.
There’s also a Makefile with shortcuts: make test, make solutions, make progress, make vet.
.PHONY: test solutions progress vet # Test your exercises (every module). Expect failures until you implement them. test: go test ./0* ./10* # Test the reference solutions — should be all green. solutions: go test -tags solutions ./... # Show a per-module progress report. progress: go run ./cmd/progress # Vet both builds. vet: go vet ./... go vet -tags solutions ./...
module github.com/bozcode/go-dsa-course go 1.22
// Command progress runs the course tests and shows how far along you are. // // go run ./cmd/progress # your exercises // go run ./cmd/progress -tags solutions # the reference solutions (all green) // // Any arguments are passed straight through to `go test`. package main import ( "bufio" "bytes" "encoding/json" "fmt" "os" "os/exec" "path/filepath" "regexp" "sort" "strings" ) type module struct { dir string // e.g. "01-arrays" title string // e.g. "Arrays" tests []string // top-level test names, in file order passed map[string]bool failed map[string]bool buildErr bool } var testFunc = regexp.MustCompile(`(?m)^func (Test\w+)\(t \*testing\.T\)`) func main() { root, err := findRoot() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } mods, err := discover(root) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } args := append([]string{"test", "-json", "-count=1"}, os.Args[1:]...) for _, m := range mods { args = append(args, "./"+m.dir+"/") } cmd := exec.Command("go", args...) cmd.Dir = root var out bytes.Buffer cmd.Stdout = &out cmd.Stderr = &out _ = cmd.Run() // a non-zero exit just means some tests fail byPkg := map[string]*module{} for _, m := range mods { byPkg[m.dir] = m } sc := bufio.NewScanner(&out) sc.Buffer(make([]byte, 1024*1024), 1024*1024) for sc.Scan() { var ev struct { Action string Package string Test string Output string } if json.Unmarshal(sc.Bytes(), &ev) != nil { continue } m := byPkg[filepath.Base(ev.Package)] if m == nil { continue } if ev.Test == "" { if ev.Action == "output" && strings.Contains(ev.Output, "[build failed]") { m.buildErr = true } continue } if strings.Contains(ev.Test, "/") { continue // subtest } switch ev.Action { case "pass": m.passed[ev.Test] = true case "fail": m.failed[ev.Test] = true } } report(mods) } func report(mods []*module) { fmt.Println() fmt.Println(" Data Structures and Algorithms in Go — progress") fmt.Println() total, done := 0, 0 var nextMod *module var nextTest string for i, m := range mods { n := len(m.tests) p := 0 for _, t := range m.tests { if m.passed[t] { p++ } else if nextMod == nil { nextMod, nextTest = m, t } } total += n done += p status := "" switch { case m.buildErr: status = " ✗ doesn't compile — run: go vet ./" + m.dir + "/" case p == n: status = " ✓ complete" } fmt.Printf(" %2d %-20s %s %2d/%-2d%s\n", i+1, m.title, bar(p, n, 12), p, n, status) } fmt.Println() fmt.Printf(" Overall: %d/%d tests passing (%d%%)\n", done, total, pct(done, total)) if nextMod != nil { fmt.Printf(" Next up: %s → %s\n", nextMod.title, nextTest) fmt.Printf(" go test -run '^%s$' -v ./%s/\n", nextTest, nextMod.dir) } else { fmt.Println(" Every test passes. Nice work.") } fmt.Println() } func bar(p, n, width int) string { if n == 0 { return strings.Repeat("·", width) } filled := p * width / n return strings.Repeat("█", filled) + strings.Repeat("░", width-filled) } func pct(a, b int) int { if b == 0 { return 0 } return a * 100 / b } // discover finds the numbered module directories and their test names. func discover(root string) ([]*module, error) { entries, err := os.ReadDir(root) if err != nil { return nil, err } numbered := regexp.MustCompile(`^\d\d-`) var mods []*module for _, e := range entries { if !e.IsDir() || !numbered.MatchString(e.Name()) { continue } m := &module{ dir: e.Name(), title: titleFor(e.Name()), passed: map[string]bool{}, failed: map[string]bool{}, } files, _ := filepath.Glob(filepath.Join(root, e.Name(), "*_test.go")) sort.Strings(files) for _, f := range files { src, err := os.ReadFile(f) if err != nil { return nil, err } for _, match := range testFunc.FindAllSubmatch(src, -1) { m.tests = append(m.tests, string(match[1])) } } mods = append(mods, m) } sort.Slice(mods, func(i, j int) bool { return mods[i].dir < mods[j].dir }) if len(mods) == 0 { return nil, fmt.Errorf("no module directories found under %s", root) } return mods, nil } // titleFor turns "03-linked-lists" into "Linked Lists". func titleFor(dir string) string { words := strings.Split(dir[3:], "-") for i, w := range words { words[i] = strings.ToUpper(w[:1]) + w[1:] } return strings.Join(words, " ") } // findRoot walks up from the working directory to the directory holding go.mod. func findRoot() (string, error) { dir, err := os.Getwd() if err != nil { return "", err } for { if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { return dir, nil } parent := filepath.Dir(dir) if parent == dir { return "", fmt.Errorf("go.mod not found — run this from inside the course folder") } dir = parent } }
At a few hours a week, plan on about a week per module, with Trees, Graphs, and Dynamic Programming taking longer. Going faster is fine — going in order matters more: queues power BFS in trees and graphs, heaps power Dijkstra, and hashing shows up in nearly everything after Module 6.
Commit after each module so you can see your own progress in git log.
The course leans on Big O from the first page — every exercise names a target like O(n). If that notation is new, Big O Notation, by Example is a one-sitting primer. And when you finish, the Go Interview Questions are the same patterns under exam conditions.
Build it, test it, then read the solution. Ten times.