It'll surprise no one to hear that TypeScript is my favorite programming language. But I do still enjoy dabbling in other languages. It's a great way to get perspective on what makes TypeScript unique, and how other language designers are thinking about the same problems.
My favorite way to learn a new language is through the annual Advent of Code (AoC). AoC runs every year from Dec 1-25. Every day unlocks a new puzzle with two parts which build on each other. Lots of people do these puzzles (over 100,000 completed day 1 this year) and post their solutions on r/adventofcode. In 2019 I did it in Python (not a new language for me) and in 2020 I did it in Rust (which was a new language). This year, I did it in Go (aka Golang).
This post has three parts:
- My impressions of Go (coming from a TypeScript perspective)
- My impressions of this year's Advent of Code
- Notes and links for each day
I first used Go when I was working at Google in 2011. C++ was my main language at the time, so Go wasn't such a big shift. I remember finding it quite confusing until I really "got" the idea of duck typing: the consumer of an interface defines what it needs and the producer doesn't need to declare that it implements this interface. I didn't have much use for Go in my work or personal projects, so I mostly forgot about it.
Some parts of Go's design that seemed strange in 2011 have aged quite well: automatic formatting of source code is very standard now, and I certainly didn't mind Go complaining about unused symbols. One thing that I found confusing in 2011 and still found confusing today: Go's insistence on where you put your source code on disk. I usually put all my GitHub repos in
~/github/reponame. But Go wants them under
$GOROOT, which is
~/gotip) on my system. This made my setup experience a bit frustrating, but once I gave up and put my code in
$GOROOT everything worked OK and I moved on.
Last year my pattern was to solve the puzzle, then look at how more experienced Rust developers solved it on r/adventofcode. I almost invariably learned something from this. That didn't work this year. In Go, the straightforward approach is usually the canonical one. There are just fewer surprises. Go takes pride in being a "boring" language. I learned more about Go from having @derat review my code and make more specific suggestions.
I'm aware that the Advent of Code doesn't particularly play to Go's strengths: there's no concurrency, for example. But I still found it to be a good way to force myself to learn the basics of the language and use it to solve real problems.
Here are a few differences between Go and TypeScript that stood out to me.
In TypeScript, we can create a
Student type with a name and age, and write a function to make a student age by a year:
Student floating around and
AgeAYear modifies it, which is why this code logs 13. (Are primitive types like
string are passed by value or by reference? It's irrelevant because they're immutable.)
In Go, something very different happens:
Go passes objects by value, not by reference, so
AgeAYear is actually operating on a copy of
bobby, not the original. This copy's
age increases, but then the copy gets thrown away when the function returns. C++ has a similar behavior and calls this "value semantics."
If you want to mutate
bobby, you can use a pointer instead:
Now this logs
13 twice. Working with pointers has some famous drawbacks, of course, but it's much easier in Go than it is in C or C++ because there's garbage collection.
Pointers in Go have a way of being contagious. Once you use a pointer to a structure in one place, you can get into trouble if you ever pass it by value someplace else. For example:
This code logs:
References self? true References self? false
I scratched my head a good long time over an issue like this before realizing that when
MakeSelfReference returns, it makes a copy of the object but of course doesn't update the
self pointer. So the two get out of sync. If you want to keep them in sync, you want to return a
It took me some time to build an intuition for this because the rules about what's copied can be surprising. An array (which always has a fixed size in Go) is copied by value, but passing a slice (which is like a small struct with a pointer, length and cap) does not make a copy of the underlying array. Another wrinkle (which I won't dive into here) is that depending on how a method is declared, invoking it can implicitly copy the object, too.
All these implicit copies are important and eventually this became intuitive, but it's quite a shift if you're coming from JS!
Go is definitely not a "batteries included" language: there are many very common and familiar functions that are missing from its standard library, notably reversing a list, the standard
reduce functions, taking the min/max of two numbers or taking the absolute value of an integer. The argument is that these functions are easy to write, so if you want them, you should just write them. But that does lead to some strange interactions. This VS Code shortcut is a real gem:
I guess this beats copy/pasting from Stack Overflow? My old coworker Dan suggested that this was a way to push you towards reverse iteration, which is faster and requires less memory than allocating a new list. The Go team tends to have very strong opinions about things like this. If you agree with them, then it's fun to watch them take a stand. But if you don't, you wind up having to settle for weird workarounds like this one.
The omission of
reduce is likely because these functions aren't that useful without generics. And there is some exciting news on that front!
The Go team accepted a propsal to add generics to the language in mid-2021 and the Go 1.18 beta, which included support for them, came out in the middle of this year's AoC. Generics are a big part of TypeScript, so I was curious to see Go's take on them. I did most of the puzzles using gotip so that I could use generics before they were officially released. Overall this was a pretty smooth process. I found that Go with Generics is a much more pleasant language to use than Go without.
Here's what a generic
min function looks like in Go:
Aside from some syntax differences, this is quite similar to how you'd write this code in TypeScript:
The most interesting bit in the Go code is the
[T constraints.Ordered], which would be similar to
T extends Ordered in TypeScript. Go comes with a
constraints.go module that defines
Ordered and many other constraints that can be placed on type parameters:
Float are themselves constraints (while JS/TS just has
number, there are many numeric types in Go such as
float64). The tilde in
~string is also interesting. It matches any type whose "underlying type" is
string. This confused me for a while until I realized that, unlike TypeScript, Go has nominal types! For example:
This would create two types that are not comparable or assignable to one another. But you can certainly take the min or max of a list of
EnglishWords. This is a feature that has been much discussed in TypeScript but is not natively supported. I found nominal types less useful in the Advent of Code than I would have expected. (I only used them to create distinct types for
ScrambledDigit on Day 8).
So is the Go generic code equivalent to this TypeScript?
Not quite. This TypeScript definition would allow you to take the min of a mixed list of
numbers, which should be an error:
There's no notion of a union type in Go (more on that in a moment), so
T has to be exactly one of the types enumerated in the constraint (or a type with exactly one of those underlying types).
I found Go generics to mostly work as expected and be quite intuitive. They let me factor out some of the repetitive boilerplate that bothered me when I first started with the language.
For example, this is a very common pattern to map a function over a slice:
With generics, you can factor out a
Map function and rewrite this loop:
This is quite verbose compared to how it would look in TypeScript:
The TypeScript version is so much shorter because:
- TypeScript is able to infer two types: both a) the type of the function parameter (
num), which it gets from the type of
numsand the type of
Array.prototype.map, and b) the return type of the arrow function, which it gets from the function body (
num * num). Go infers neither of these, so you have to write out the types. It does infer the return type of the whole
Mapexpression, though (
I expect that having generics will greatly increase the demand for concise function definitions and better type inference. The Go team will either adopt them or declare that the users asking for them are misguided. It'll almost certainly be one of those two — they're quite opinionated!
There's a famous quote from one of the Go developers (Russ Cox) that when it comes to generics, you can either have:
- Slow programmers (i.e. no generics)
- Slow compilers (by emitting a different version of a generic function for every type instantiation)
- Slow programs (by boxing everything and doing runtime dispatch).
Go tries to escape this dilemma by leaving it up to the compiler whether to specialize a function or box it. The whole generics proposal is interesting to read. It only occurred to me halfway through the doc that runtime performance was a consideration. Clearly I've internalized the TypeScript approach to generics!
Overall I found Go generics easy to use and a great addition to the language. It will be interesting to see how they're adopted by the community, and whether they bifurcate libraries. I wanted a Graph library, for example, but there weren't any that used generics. So I just wrote my own.
Here are a few other things I found interesting about Go:
Go has some extremely error-prone constructs. Two examples:
- You write a for loop over a slice as
for idx, el := range vals. So
rangegives you the index first and then the value. This means that if you write
for el := range vals, you'll iterate over the indices, not the values. I much prefer the JS approach of value then index, e.g. for
forEach. It seems much more common to only care about the value than to only care about the index.
a, b := fn()introduce two new variables? It depends. If both
bexist, then it will create two new variables. But if only
aexists, then it will create a new
avariable but reassign
b. I found this quite surprising!
- You write a for loop over a slice as
I really missed union types. Go has nothing like them, and the official stance from the Go team is that they "do not add very much" to untyped solutions. 🤮 Remember what I said about the Go team being opinionated?
You don't need regular expressions as much as you think for parsing.
fmt.Sscanfwoked great for most of the Advent of Code problems.
Like TS, Go has both
constbut they mean pretty different things.
Go doesn't really have a C++-like notion of
As in other years, I found that using
Tis almost always a good idea. It lets you create sparse arrays and accomodates unknown size or negative indices.
Go's type syntax is almost exactly backwards from TypeScript or C, which was a constant source of typos for me. For example,
T. Or for function parameters,
min(a int, b int)instead of
min(int a, int b). Or even
min(a, b int).
I still find Go's pickiness about where I put my code on disk to be weird, confusing and counterproductive. Why can't I
go buildsomething in a subdirectory?
Go types all have a meaningful zero value. I started to learn to work with this more and more as the month went on. For example,
nilworks perfectly well as an empty slice.
I've always been a spaces person but Go uses tabs. It didn't bother me one bit.
The 2021 AoC was harder than 2020 but easier than 2019. As in 2020, none of the solutions built on one another. Each day was a completely independent puzzle. I'm sure Eric Wastl has his reasons for doing it this way, but I missed how 2019's puzzles encouraged you to improve the design of your previous day's solutions. With the independent puzzles, Advent of Code feels more like pure speed coding.
Days 8, 22 and 24 were the real standouts to me as a creative puzzles. For the other days, my first idea on how to solve the puzzle always turned out to be right, even if it took quite a while to implement it. Many, many of the puzzles this year could be solved with some variation on Dijkstra. It's always amazing to see how effective the standard algorithms are once you figure out how to map your problem onto them.
I continue to wish I were in a timezone where it was more reasonable for me to do the puzzles as soon as they came out. And I continue to think this is a great way to learn a language. AoC problems don't really play to Go's strengths (there's no concurrency) but I did feel much more confident writing Go by the end than I did at the start.
Once again, I had a great time doing Advent of Code this year and I enjoyed the opportunity to learn a new language. The days' puzzles gave me something concrete and fun to cling to during a time of complete chaos at my work.
Day 25 in particular stands out. I woke up early (6 AM) to watch the long-anticipated launch of the James Webb Space Telescope, and I raced to collect my final stars before I had to drive off to the airport for a trip to the Dominican Republic, my first international vacation in ~2.5 years.
If you've made it this far and for some reason want even more, check out the notes and code in my GitHub repo.