A graphic for a Go programming learning log, featuring the Golang Gopher mascot on a stack of books. Text includes "Five Books to Build One Toy Language" and "Monkey See, Monkey Do • A Go Learning Log." Diagram lines show programming concepts.

Five Books to Build One Toy Language

· 5 min read

I started teaching myself Go at the end of 2023 to break up the monotony of building the same React UI for the billionth time. Then life got busy, and for a long stretch I stopped writing any code that was not for work or for learning something I needed right then. I have been sitting on a stack of Go books that whole time, and I finally feel ready to pick them back up.

I used Claude to map out how to read them so they complement each other rather than overlap. The spine of it is Thorsten Ball’s two books, Writing an Interpreter in Go and Writing a Compiler in Go. Both build the same little language, called Monkey, and the rest of the stack is there to fill in the deeper Go concepts along the way.

This is a learning log. I am writing it as I go, one post per chapter, so you are getting my working notes and thoughts rather than a polished retrospective. The series is called Monkey See, Monkey Do. Part one, Monkey See, is the interpreter learning to read and evaluate itself. Part two, Monkey Do, is the compiler learning to actually run.

Why a Language

I use interpreters and compilers every single day. So do you. Every time I run go build or a browser executes a script, some program is reading my code, making sense of it, and turning it into instructions a machine can follow. I have leaned on that machinery my entire career and never once questioned how it worked.

I wrote a while back about going to the Gopher side and why Go clicked for me in a way other languages did not. I feel that Thorsten’s books can help challenge me to build something I have never tried to build before while helping me grow as a developer not just my Go chops but my overall understanding of how these systems work.

The Five Book Stack

Ball’s two books are the backbone of this series. The other three are companions, pulled in to go deeper on the concepts.

The Go Programming Language, or TGPL, is the deep reference. When the underlying Go feels shaky, this is the book I will reach for first.

100 Go Mistakes and How to Avoid Them is the guardrail. Short, targeted sections that address common bugs and mistakes that are made when writing Go.

The Black Hat Go Manual is the odd one out. It is not a languages book, and it does not line up with Ball’s chapters at all. It is a quick reference of Go security snippets grouped by topic, the kind of thing you keep open in a tab. I am using it for two things: a fast idiom lookup when my Go gets rusty, and its byte and string handling, which is the same muscle the lexer and bytecode encoding lean on.

All the code lives in a companion repo on GitHub, tagged per post, so you can check out the exact state of the project at any point in the series. I chose to host this on GitHub as I do not want to deal with the hassle of making one repository on my self-hosted Forgejo instance public facing.

Testing Before Tokens

Both of Ball’s books are test-driven from front to back. You write the tests for each chapter first, then write the code to make them pass. So before I touch the lexer, the thing to get comfortable with is Go’s testing.

That means TGPL chapter eleven for go test, table-driven tests, and coverage, and the testing chapter in 100 Go Mistakes. If Go is new to you, back up to TGPL chapter one for the basics: structs, slices, and control flow.

Most of what these books ask for is a table-driven testtable-tests: a slice of cases and a loop over them.subtests

func TestReverse(t *testing.T) {
	tests := []struct {
		name  string
		input string
		want  string
	}{
		{name: "empty", input: "", want: ""},
		{name: "single", input: "a", want: "a"},
		{name: "word", input: "monkey", want: "yeknom"},
		{name: "multibyte", input: "🐒", want: "🐒"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := Reverse(tt.input)
			if got != tt.want {
				t.Errorf("Reverse(%q) = %q, want %q", tt.input, got, tt.want)
			}
		})
	}
}

Reverse a string byte by byte and the first three cases pass, but the monkey emoji comes out garbage, because that character is four bytes of UTF-8 and flipping the bytes breaks it. That is the bytes-versus-runes trap 100 Go Mistakes warns about, and the lexer runs straight into it next post.

Wrap Up

That is the plan: Ball’s two books as the core, three companions for the Go underneath them, and testing before anything else. Next post I start on the lexer.

If you want to follow along, grab the two books and clone the repo.

Until next time,

Cody


  1. Dave Cheney makes the case for the pattern in Prefer table driven tests.
  2. The t.Run calls in that loop are subtests — the Go blog covers what they buy you in Using Subtests and Sub-benchmarks.