Advent of Code 2025: How did it Go?

Drawing of 3 gophers working on an odd-looking machine, with tape reels, punch cards, and a brain in a jar.
Gopher image by Renée French, licensed under Creative Commons 4.0 Attribution license.

I posted a few weeks ago about learning Go with Advent of Code. I’m a big fan of these puzzles — they’re still hard enough for me that it’s a fun challenge, and trying to do them in a new language really lets me explore whether I’ve learned it well enough for it to get out of my way and let me think about the problem I’m solving.

I’ve now finished Advent of Code 2025 (delighted to get a full set of stars!). Minor spoilers, but I avoid going into much detail about specific puzzles.

Screenshot of VS Code with day 1 test running. It asserts 1 equals 2, and is showing a message: "our first test failure".
Everything in place just before the first puzzle unlocked

Things I appreciated

I had fun trying Go; as I mentioned last time, it’s designed to be simple so people can get productive quickly, and has various tools for IDEs to help everyone do things consistently:

  • gofmt (”Go Format”) tidies all spacing and layout into a consistent format every time you save, and I love this — any sensible choice is fine, and this lets me just type and insta-correct without taking any of my attention. A Go Proverb describes lots of people’s similar reaction.
  • gopls (”Go Please”) gives lots of helpful “maybe you’d like to do it like this” suggestions that can be automatically accepted. I had lots of “oh I didn’t realise I could” lightbulbs from this, especially its modernize package that pointed out recently-added conveniences that weren’t in some older tutorials I’d been reading.

I also feel like Go has a nice “just enough” approach to type declarations. JavaScript and Python let you write no type info at all, but over time things like TypeScript and Python type hints have been widely adopted as people found the kind of bugs that could lead to. Despite Advent of Code needing only short programs where lots of verbosity is annoying, Go’s types felt helpful rather than an overhead.

I’m also impressed with how easy it is to declare and extend types. My favourite example was when I found Go doesn’t have a set type. You can certainly use a map, with empty values, but working with that looked ugly. It was so simple to make a nice set type, and adding things like Union or IsDisjoint would be easy too.

type Set[T comparable] map[T]struct{}

func (s Set[T]) Add(item T) {
	s[item] = struct{}{}
}

func (s Set[T]) Has(item T) bool {
	_, ok := s[item]
	return ok
}

This was easy to import and reuse across the days — and surprisingly, when I tried the handy browser-based Go Playground, that could import from my AoC repo too.

Screenshot of the Mastodon post linked just before this image. I posted live from a tram near Cornbrook to show this working. It has an image of the Go Playground importing my helper package.

Small confusions

Mod for negative numbers: Using the modulo operation (%) where one of the numbers is negative is something that comes up rarely, except in Advent of Code where it comes up about once a year. In Python this works just as I’d expect, but I found Go chose the same surprising (to me) thing that JavaScript and C do.

Wikipedia says “In number theory, the positive remainder is always chosen, but in computing, programming languages choose depending on the language”

In computing, Donald Knuth promoted the “floored” option (Python’s choice) in The Art of Computer Programming.

It’s easy to work around once you realise what a language is doing, but different languages choosing different definitions always catches me out!

Making expressions more regular: I’m not sure where Google result summaries come from. This result says Go uses the same syntax as Perl and Python — I tried and got an error.

The actual page says it “uses the same general syntax as Perl and Python”, and helpfully points to the exact differences. For this solution, I wanted to solve part 2 with a one-line check — a regex with a backreference would do the trick. Since Go regexes don’t allow those I had to write something bit wordier.

I can see why backreferences and other “fancy” regex features have been left out: the regex syntax we all use today was invented by Ken Thompson (co-creator of Go), along with the Thompson NFA algorithm that makes evaluating them incredibly fast. This doesn’t work at all with add-ons like backreferences (in fact, adding those isn’t possible by the definition of “regular” expressions). A post explains just how differently the NFA version works compared to the approach taken by most languages.

Image from the post linked just above. On the left, a graph of string length against time shows Perl starting off near zero seconds, then getting dramatically slower once length gets to about 25 - the line is turning vertical. On the right, the Thompson NFA graph is pretty linear and stays below 25 microseconds for the same string lengths Perl struggled with.
Comparison of runtimes for particularly surprising cases. Note that the left graph’s Y axis is in seconds, while the right one is in microseconds.

This choice fits in with Go’s goals: in addition to keeping things fast, leaving out “fancy” features from regexes helps keep things simple, as regexes are famously daunting to get to grips with.

Nice touches

Every day has a similar starting point: read in the input file, and go through each line extracting the data for your puzzle. I was very happy with how Go handled this:

//go:embed input.txt
var puzzleData string

func main() {
	lines := input.SplitIntoLines(puzzleData)

This brings input.txt‘s content in at compile time — that’s sensible for these puzzles, and sidesteps lots of does file exist / do we have access / many more complications. I did experiment with scooping up chunks of the file at a time, to avoid bringing it all into memory at once — that worked fine but will rarely be needed.

Parsing the input is almost always handled by an input.Parse function I wrote, which gives useful error messages when an input line doesn’t match regexp, I’ve miscounted the capturing groups or types, and several other common fiddly gotchas.

type Range struct{ Min, Max int }
var rangeRe = regexp.MustCompile(`^(\d+)-(\d+)$`)

var r Range
err := input.Parse(rangeRe, ln, &r.Min, &r.Max)

I get along without you very well (except sometimes)

One thing was on my mind as I solved these puzzles in Go: this language makes for loops nice and readable, but I’ve been such a fan of declarative, functional approaches for so many years it’s quite a shift. When I used TypeScript for Advent of Code I wrote about why I like these and how I’d explored adding them to TS.

I felt fine about leaving that and other things to one side to learn Go properly, I’m interested in understanding its idioms. The only time I really missed Python was when I hit a dead-end trying to express one of the challenges as a linear optimisation problem.

I’d carefully worked through the equations on notepaper and knew the exact tool I’d reach for if this was Python … and got completely stuck trying to find a suitable library for this in Go.

Kylo Ren screenshot, with text: "I know what I have to do, but I don't know if I have the linear optimsation library to do it".
Why yes I am very skilled at putting hilarious memes together, thanks for noticing.

I gave up on Go and switched to Python! Was not expecting this, and it probably says more about my inexperience with Go rather than anything else. I investigated:

  • glop and other Google OR-tools libraries, which have Python and other wrappers, nothing for Go — unless I’ve missed something?
  • golp (github.com/draffensperger/golp) wouldn’t work, I installed lpsolve as the instructions say but kept getting package import errors.
  • gspl (github.com/chriso345/gspl) seemed to work fine, but keeps stopping on the real input – and it gets stuck on a different line every time I rerun it! Maybe I was doing something foolish.
  • Some other libraries (from lanl and bartolsthoorn) wrap the useful HiGHS solver but have only been built for Linux. I’m working on Windows, which has been absolutely fine up till this point — maybe switching would help?

After hours on my quest to find a Go way, I gave up and:

  • Made a Python source and test file; VS Code instantly started autocompleting and offering test runners for these.
  • pip install scipy gave me the library I needed in just a moment.
  • Wrote a short function, with help from the SciPy docs (I’ve used this but not for a few years)
  • Solved on the first go, and claimed my star.

I did try one other foray into Python this year — I struggled with one day’s part 2 for ages, eventually getting a Go program that solves it in 2 minutes. I had a hunch that Python’s NumPy could work its fast number-crunching magic on this so tried it out. NumPy let me build a 10 billion element array and find the answer, running in 1 second. It’s an amazingly fast library (under the hood it uses C code and clever memory vectorization tricks), but of course the real answer in this case is for me to think about a solution that doesn’t need all this brute-force processing. I’ll come back to this.

Screenshot of github commits for day 9: 

"not working yet! See failing test"

"new idea, still failing"

"don't do it this way"

"still working on it"
This one was a real monster for me to solve.

Some things to explore further

Small stand-alone scripts don’t give much chance to try many of Go’s interesting qualities, I’d like to play with something bigger. Go’s famously good at making concurrency easy to work with, and making deployment/updates in cloud environments straightforward, I haven’t tried any of that yet.

I’ve done quite a lot of “trust me it’ll be fine” ignoring error values in this year’s AoC, I think for real Go training I should work on actually handling these. You can see lots of examples in my repo where I’ve panicked “this isn’t what we expect”, and commented to reference the “don’t panic” Go proverb.

Another bit of wisdom I’d suggest: any time an error variable is named “_” is a little gotcha waiting to happen. I did this several times in my short solutions, as the constant checking if err == nil and passing any problems on up the call stack seemed too wordy. I’ve read some interesting error handing advice (”Errors are values” from Rob Pike) and I’m keen to see how well that works.

These, and several other topics, are making me interested enough to carry on exploring the Go language. I might write some follow ups about how that goes.

The Go gopher wearing top hat and tails, holding a champagne glass.
Fancy Gopher, featured at the Gopher Gala. By Renée French, licensed under Creative Commons 4.0 Attribution license.


Posted

in

by