Useful functions when doing Advent of Code in Go

Published 09.12.2023 • Last modified 13.05.2024

Advent of Code is an annual set of Christmas-themed computer programming challenges that follow an Advent calendar. It has been running since 2015.

The programming puzzles cover a variety of skill sets and skill levels and can be solved using any programming language. Participants also compete based on speed on both global and private leaderboards.

Wikipedia

What makes AOC particularly fun is the daily challenge aspect, and the community.

When doing this year’s AOC challenges in Go, I found myself writing a lot of boilerplate code that could be abstracted away. I also found many very useful but unknown functions in the Go standard library. Here’s what I have found:

Reading input #

There are 2 ways to read the input: reading the whole input at once, or scanning it per line.

func inputScanner() *bufio.Scanner {
	f, err := os.Open("input.txt")
	if err != nil {
		panic(err)
	}
	return bufio.NewScanner(f)
}

func inputLines() []string {
	b, err := os.ReadFile("input.txt")
	if err != nil {
		panic(err)
	}
	return strings.Split(string(b), "\n")
}

Whichever you use depends on how the input is formed: if the problem fits neatly into one line per entry, you generally want to use inputLines(). However, if the input consists of many “blocks” separated by empty lines, an inputScanner() is the better choice.

Parsing into data structures #

There are many ways to parse the string input into data structures. One way is to use methods in the strings package:

type Node struct {
	Left, Right string
}

func main() {
	lines := inputLines()

	nodes := map[string]*Node{}
	// Input looks like this:
	// AAA = (BBB, CCC)
	// BBB = (DDD, EEE)
	for _, line := range lines {
		label, pair, _ := strings.Cut(line, " = ")
		left, right, _ := strings.Cut(strings.Trim(pair, "()"), ", ")
		nodes[label] = &Node{left, right}
	}

	fmt.Println(nodes)
}

If the input contains a list of numbers (which it usually does), you can use this atoi funcion to turn them into proper integers:

func main() {
	lines := inputLines()
	// input looks like this:
	// 0 3 6 9  12 15
	// 1 3 6 10 15 21
	for _, line := range lines {
		var numbers []int
		for _, v := range strings.Fields(line) {
			numbers = append(numbers, atoi(v))
		}
		fmt.Println(numbers)
	}
}

func atoi(s string) int {
	v, err := strconv.Atoi(s)
	if err != nil {
		panic(err)
	}
	return v
}

strings.Fields works like strings.Split(line, " "), but it removes ALL whitespace, so you don’t end up with something like ["6", "9", "", "12"]. Super useful!

Another way which I just recently stumbled upon is the wonderful fmt.Sscanf function. We could write the former example like this:

func main() {
	lines := inputLines()

	nodes := map[string]*Node{}
	// Input looks like this:
	// AAA = (BBB, CCC)
	// BBB = (DDD, EEE)
	for _, line := range lines {
		var label string
		node := &Node{}
		fmt.Sscanf(line, "%3s = (%3s, %3s)", &label, &node.Left, &node.Right)
		nodes[label] = node
	}

	fmt.Println(nodes)
}

The %3s indicates that there should be a 3-character long string. Sscanf works like a reverse Sprintf, which means that it can also parse numbers! If the label happened to be a number, we could parse it to an integer using %3d = (%3s, %3s)

The slices package #

The slices package was added into the standard library in Go 1.21.0. It contains a lot of useful generic functions, the most useful of which are slices.Contains, slices.Index, slices.Reverse and slices.Insert. While you could easily implement these yourself, you don’t have to anymore! There is also the slice tricks page for everything else.

Functional programming with samber/lo #

samber/lo has a lot of goodies for operating with slices like lo.Sum, lo.Uniq and lo.Count. I tried using the lo.Map/lo.Reduce functions which accept inner functions, but found it much less readable than what you would typically write:

// Calculate Least Common Multiple of vals
vals := []int{1, 2, 3, 4, 5, 6}

// Idiomatic version:
lcm := vals[0]
for _, v := range vals[1:] {
	lcm = LCM(lcm, v)
}

// samber/lo version:
lcm = lo.Reduce(vals[1:], func(agg int, item int, _ int) int {
	return LCM(agg, item)
}, vals[0])

There is also the function lo.Must, which can be used to panic on errors without writing if err != nil { panic(err) } a million times. We can use it to simplify the utility functions like this:

func inputScanner() *bufio.Scanner {
	f := lo.Must(os.Open("input.txt"))
	return bufio.NewScanner(f)
}

func inputLines() []string {
	b := lo.Must(os.ReadFile("input.txt"))
	return strings.Split(string(b), "\n")
}

func atoi(s string) int {
	return lo.Must(strconv.Atoi(s))
}