One might think that when you’ve learned one programming language you’ve learned them all, or that you’ve learned enough of the fundamentals of programming that you can seamlessly transfer that knowledge to new languages.
If you’re aware of the Dreyfus model of skill acquisition, you might already know that this kind of skill transfer doesn’t necessarily apply. Being an expert engineer doesn’t imply that you will also be an expert engineering manager, and being an expert Ruby programmer doesn’t imply that you’ll be an expert writing Haskell. There may be things in common that make it easier for you to learn, but not enough to completely strip away the need to learn.
Despite putting my 10,000 hours in as a child, I’m still having trouble with adulthood.
Who knew!?
I’ve spent the vast majority of my career in software engineering writing Ruby and JavaScript (JS) - backend, front-end, full stack…thin clients, thick clients…static linking, dynamic linking…time is a flat circle. I’ve always enjoyed dabbling though, just to see what it’s like to use something different. Elisp, Haskell, Elixir, Zig, you name it; I’ll just try to make a little toy to see if I can learn anything from it and apply it to my daily driver.
Go has also been one of my fleeting dalliances, but not really much more than building simplistic APIs or CLI tools; certainly not enough to get a good grasp of the language and what it’s about.
Consider me excited, then, when Go takes over Ruby as my primary workhorse. How hard can it be, after all? I know Ruby intuitively, so I just have to take what I know and do it in Go…right?
These would be my famous last words, if in fact I was famous. I suppose instead they’re just last words.
Going back to the model of skill acquisition, you can imagine my affinity for programming has allowed me to take a few shortcuts in becoming a Go-ficionado, but being the double edged sword that it is the entire back-catalogue of loaded Ruby/JS-based assumptions in my experience has also slowed me down somewhat.
Sounds like a need a good humbling, so enjoy a loose play-by-play of my Sisyphean regression to unconscious-incompetence.
Modules
Good grief, where do I even begin. My last encounter with Go was before the full introduction of modules and workspaces in version 1.18, when a few competing proposals were at play.
“Why can’t I just do
go add dependency-name
? What even is this?”
— Me, 2024
Every commit comes with a whole bunch of random changes to go.mod and go.sum, but everything seems to build. Why are they updating all the time? Why are they all marked as indirect? Am I not actually pinning the versions like I should?
Plainly, I’m not using it right and maybe there’s an element of learned helplessness as the faded memory of setting up a special directory structure for go projects came to the front of my conscious.
Say what you want about about NPM or Bundler or any tool derived from either of those, it’s a known quantity. Not great that you need some kind of server to pull the dependencies from though - that’s a lot of money on infrastructure and bandwidth to support an open source ecosystem.
I think I’ve mostly got the hang of this now but I’m not 100% convinced I’ve grasped it.
JSON
Bane of my life, really. In a dynamic language it’s easy to take the equivalent of a struct
and then manipulate it, perhaps to change the naming convention for keys (camelCase
, snake_case
, for example).
Go’s JSON setup, at least in its current inception, couples the JSON representation of a struct to the struct itself. If you want to do a conversion it seems you have to manipulate the string output (perhaps deserializing it into a map and then serializing it back again after modification).
My struggle with this came from the faulty assumption that the language had functionality to work with structs beyond them being simple data types. Everything that interacts with a struct, beyond simple getting and setting, is done through reflection.
Fair enough, Go is speedy and sits somewhere between C and Java in terms of semantics. This is more on the C side I imagine.
It took a while for me to come to terms with the fact that I just needed to change my thinking, and after much searching it turns out you can do fairly simple type conversions on structs provided that they are structurally identical.
package main
import "fmt"
type FooBarCamel struct {
FooBar string `json:"fooBar"`
}
type FooBarSnake struct {
FooBar string `json:"foo_bar"`
}
func main() {
camel := FooBarCamel{"baz"}
snake := FooBarSnake(camel)
camelJSON, _ := json.Marshal(camel)
snakeJSON, _ := json.Marshal(snake)
fmt.Printf("Camel: %s; Snake: %s", camel, snake)
}
Presumably you can just write all these duplicated types yourself or just use codegen.
Mindset
When you get used to a dynamic language and start to know how it all works under the hood, you start to look at ways to optimise code because you understand the performance characteristics of what you’re writing. Perhaps one function is implemented in C as an optimisation but another is done at runtime and isn’t quite as fast.
You also start to get used to the idea of the code itself having an ‘experience’: you want it to be nice to write, nicely laid out, and awkward implementation details to be kept out of the way. DSLs abound. You don’t need to ‘generate’ code per se because the language likely supports metaprogramming. Ultimately you have a blank slate VM and you are modifying its state and behaviour until it does what you want.
That’s an intensely attractive proposition and is what makes Ruby, Smalltalk and Lisp so nice, dare I say joyful, to write.
Make no mistake, the trade-off here is that it can be difficult to read the source and understand what the code is actually doing, especially when you’re debugging macros or metaprogramming tactics where the code at runtime doesn’t actually exist in your repo. Rails is full of this: ActiveRecord reads your database schema at startup and generates the interface for all the models you’ve defined there and then, at runtime.
There’s something to be said about beauty in simplicity, or at least aesthetics, when using a language like Go. You basically have to spell it all out, but the bargain is that the language itself is not particularly complicated and has a pretty simple mental model and small syntactical footprint. The language gives you tools to abstract to help clean it up, but not so much that the abstraction becomes unintuitive. If you need more, use codegen tools.
Adapting to this mentality helped me work much more effectively with Go as a relative newbie. If nothing else, it helps you remember how the high level algorithms are usually implemented in other languages: once you know reduce
you know almost all of them.
Is that a reference to a generic solution I see?
Generics
Honestly - these were almost a decade in the making and are quite limited except for buliding a catalogue of helper functions.
Duck typing with interfaces still handles the 90% of use-cases. Quack.
Is there more? Probably… Is it enough for now? Yes. Let’s see how this Go-ad trip continues.