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
My Impressions of Go
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 ~/go
(or ~/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.
Implicit copies
I'm not aware of any situation in which JavaScript makes a copy of an object without your asking it to. In Go, however, this happens all the time. Building a mental model for when these copies happens is always relevant for performance, and sometimes relevant for correctness.
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:
|
In JavaScript, objects are always passed by reference. There's only one 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 *SelfReference
instead.
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!
Batteries not included
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 map
/ filter
/ 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 map
/ filter
/ reduce
is likely because these functions aren't that useful without generics. And there is some exciting news on that front!
Generics
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:
|
Both Integer
and Float
are themselves constraints (while JS/TS just has number
, there are many numeric types in Go such as uint32
or 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 EnglishWord
s. 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 Digit
and 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 string
s and number
s, 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:
- JavaScript's arrow functions have a very compact syntax compared to Go's
func
andreturn
. - TypeScript is able to infer two types: both a) the type of the function parameter (
num
), which it gets from the type ofnums
and the type ofArray.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 wholeMap
expression, though ([]int
).
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).
In this scheme, JavaScript forces TypeScript to choose "slow programs" (though JS JITs are quite good!). And TypeScript has a complex enough type system that it's often a slow compiler, even without C++-style compile-time specialization.
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.
Other bits
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
. Sorange
gives you the index first and then the value. This means that if you writefor el := range vals
, you'll iterate over the indices, not the values. I much prefer the JS approach of value then index, e.g. formap
andforEach
. It seems much more common to only care about the value than to only care about the index. - Does
a, b := fn()
introduce two new variables? It depends. If botha
andb
exist, then it will create two new variables. But if onlya
exists, then it will create a newa
variable but reassignb
. 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.Sscanf
woked great for most of the Advent of Code problems.Like TS, Go has both
var
andconst
but they mean pretty different things.Go doesn't really have a C++-like notion of
const
or TypeScript'sreadonly
.As in other years, I found that using
map[Coord]T
instead of[][]T
is 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
instead ofT[]
. Or for function parameters,min(a int, b int)
instead ofmin(int a, int b)
. Or evenmin(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 build
something 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,
nil
works perfectly well as an empty slice.I've always been a spaces person but Go uses tabs. It didn't bother me one bit.
My impressions of this year's Advent of Code
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.
Conclusions
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.
It's unlikely I'll use Go in my daily work, but I do enjoy the perspective on TypeScript that it gives. As always, TypeScript's relationship to JavaScript (types are erased at runtime) and its ability to infer types stand out. Which language will I try next year? I'm not sure! Suggestions welcome.
If you've made it this far and for some reason want even more, check out the notes and code in my GitHub repo.