Pretty interesting. It is scary to make your "learn a new language" task to port 14,000 lines of code, but with that in mind, this all seems to have gone well. Some random thoughts:

> I had to write my own set-of-int and
set-of-string classes

map[int]struct{}, map[string]struct{}

ints[42] = struct{}{} // insert
delete(ints, 42) // delete
for i := range ints { ... } // iterate
if _, ok := ints[42]; ok { ... } // exists?

> Catchable exceptions require silly contortions

I am not sure why go has panic/recover, but it's not something to use. panic means "programming error", recover means "well, the code is broken but I'm just a humble generic web framework and I guess maybe the next request won't be broken, so let's keep running". It is absolutely not for things like "timeout waiting for webserver" or "no rows in the database" as other languages use exceptions for. For those, you return an error and either wrap it with fmt.Errorf("waiting for webserver: %w", err) or check it with errors.Is and move on. Yup, you have to remember to do that or your program will run weirdly. It's just how it is. There is not something better that maybe with some experimentation you will figure out. You have to just do the tedious, boring, and simple thing.

I have used recover exactly once in my career. I wrote a program that accepted mini-programs (more like rules) via a config file that could be reloaded at runtime. We tried to prove them safe, but recover was there so that we could disable the faulty rule and keep running in the event some sort of null pointer snuck in. (I don't think one ever did!)

> Pass-by-reference vs. pass-by-value

I feel like the author wants []*Type instead of []Type here.

> Absence of sum/discriminated-union types

True. Depending on what your goals are, there are many possibilities:

type IntOrString struct { i int; s string; iok, sok bool }
func (i IntOrString) String() (string, error) { if i.sok { return i.s, nil } else { return "", errors.New("is not a string") }}
func NewInt(x int) IntOrString { return IntOrString{i: x, iok: true} }
...

This uses more memory than interface{}, but it's also very clear what you intend for this thing to be.

I will also point out that switch can bind the value for you:

switch x := foo.(type) {
case int:
return x + 1
case string:
i, err := strconv.Atoi(x)
return i + 1
}

And that you need not name types merely to call methods on them:

if x, ok := foo.(interface { IntValue() int }); ok { return x.IntValue() }

You can also go crazy like the proto compiler does for implementations of "oneof" and have infinite flexibility. It is not very ergonomic, but it is reliable.

> Keyword arguments

type Point struct { X, Y float64 }

func EuclideanDistance(a, b Point) float64 { ... }

EuclideanDistance(Point{X: 1, Y: 2}, Point{3, 4})

> No map over slices

This one is like returning errors. You will press a lot of buttons on your keyboard. It is how it is.

I personally hate typing the average "simple" for loop:

func fooToInterface(foos []foo) []interface{} {
var result []interface{}
for _, f := range foos {
result = append(result, f)
}
return result
}

But it's also not that hard. I used to be a Python readability reviewer at Google. I always had the hardest time reading people's very aggressive list comprehensions. It was like they HAD to get their entire program into one line of code, or people wouldn't think they were smart. The result was that the line became a black box; nobody would read it, it was just assumed to work.

I really like seeing the word "for" twice when you're iterating over two things.