This is the second post in the series on how I rewrote my blog to its current state. For context and index, see Rewriting blog is so much fun (1): Background, Objectives, and Index.
In my last post, I went over my motivations of the rewrite and my objectives. Before starting the real dev work, however, I must choose a language.
Escaping JavaScript
It might sound counterintuitive today, but you don’t need JavaScript to build a decent website. One of my takeaways of this rewrite: Try going as far as you can without JavaScript: you’ll end up farther than you had expected—and also happier and more productive.
Am I a JavaScript hater? No and Yes. JavaScript is a great language, yet horribly designed. One mustn’t get the causality wrong: JavaScript’s greatness is retroactively defined and maintained by its popularity, not the other way around! It became the workhorse of the web mostly thanks to first-mover advantage and lack of serious competitors, and it stayed only because of the large number of patches people applied to fix or hide its problems. The tremendous amount of effort put into making JavaScript good could easily make other language great too: imagine an alternative universe where Guido Van Rossum was in Branden Eich’s shoes, and Python would dominate the web just as well.
(I am in good company. Proof by O’reilly)
“But you do agree that JavaScript today is great, right? So what’s the fuss?” The problem, my friend, is that every time when you start a new JavaScript project, you will be forced into a refresher course of JavaScript’s unfathomably dark past. Personally, I feel having this shock wave of 💩 coming right at my face every time, which is too much for a JavaScript non-believer to overcome.
Just think for a moment what a modern JavaScript project takes: an ES5 transpiler, a bundler, a tree-shaker, a minifier, a linter, a formatter, a TypeScript compiler, JSX, TSX, etc. Should they be different programs, or should they be one? Should you configure them separately, or should you trust the default? Better think it through and choose wisely, for changing your mind later on will be painful. I always wondered why I can’t skip these stuff and just code. Turns out that they are necessary safety nets to protect me from accidentally stepping into the 💩 side of JS, sigh.
Don’t other languages suffer similar problems? Absolutely. I had similar struggle setting up new C++ projects. But for other languages usually there’s a good consensus of what to do and you don’t get to make many decisions. Consequently there isn’t much mental burden going through the setup process manually, if it hasn’t been automated already. JavaScript is uniquely infuriating in that its developers are incredibly enthusiastic about solving the same problem multiple times and claiming their solution as the best each time (or blazingly fast!). So as a beginner you are faced with a combinatorially exploding number of choices. Some enjoy the freedom, but making choices happens to be hard for me.
You may say it’s entirely a skill issue (likely true). You may say
that this is because the JavaScript ecosystem is such an umbrella term
that covers so many different use cases (which is true); You may even
say that you should just use Vite and let
npm create vite@latest
do the rest—this is also true today
(but some of my JS experiences predates that). I rationally accept all
these counterarguments, but my experience with starting new JavaScript
projects made me believe that JavaScript easily makes one irrational,
period.
And I haven’t even got to JavaScript frameworks yet. Feels like there are thousands of them with new ones being announced every week.
Sorry if this is an overdose of JavaScript rants for this post. I honestly do not have a strong, rational argument to hate JavaScript, but it has dealt me emotional damage. I’ve had other side projects with it, so not using it this time is fine I guess. Moving on.
The reality was that I made the first rewrite attempt in Next, felt something is off, and then made the second rewrite attempt in Svelte, which did not feel right either. Something did not click, I felt, and lost momentum in this project. That was in Spring 2023. Coursework then took priority and the rewrite was shelved until a year later (blame me for the procrastination).
Rediscovering Go
A year passed and it was the summer of 2024. I started an intense internship and was looking for something to wear-level my brain. Rewriting the blog it is, I thought, and dequeued the job from my (infinitely long) todo list.
Having been tortured by JavaScript twice, I wanted to try something else—something new to me, something fun, and better still, something with good and no-brainer DX so my brain-dead self after a day’s work can still happily work on.
So I chose Go.
Timing was everything in this decision. I’ve kept myself from using Go since I knew it back in middle school, as there’s simply nothing exciting about the language for a teenager looking for fancy languages to flex and fulfill his superiority complex. It was a dumb language that didn’t even do generics, after all. Rust was a better choice and I’ve been firmly siding with Rust for a long time when people compare the two. My opinion only started changing in the past two years as I saw and wrote more engineering (💩) code. Coincidentally, in the Spring of 2024, I took MIT’s graduate distributed systems class which mandated the use of Go for lab assignments. Russ Cox gave a great guest lecture there and I wrote a whole distributed consensus protocol in Go. That experience convinced me that Go is a great language. The language decision for the rewrite came right after my honeymoon with Go started.
I apologize for the misleading title that sounds like I am to compare Go against JavaScript. No. My emotional disappointment with JavaScript merely provided an opportunity to try out Go in a side project. What I want to compare Go with is Rust, which I did many more side projects with and a strong contender in my language decision for the rewrite. Admittedly, Go vs. Rust is not a new topic and many who did such comparisons are more credible or are better writers, but it does not stop me from offering my own take.
When mud wins over diamond
Go reminds me of another class I took in freshman Spring on a completely different subject—Large Scale Symbolic Systems. Ordinary people don’t touch symbolic systems and the Scheme language used to teach the class is pretty much dead. You think taking this class was another superiority complex move of mine? Correct, but the class proved to be far more than that. It’s the kind of the class where you had no idea what the professor was talking about but end up hearing echoes of his words throughout the rest of your career. The professor is Jerry Sussman, who authored the well-known Structure and Interpretation of Computer Programs (SICP) and invented Scheme. The guy was a true legend who programs since 1968, and he had much wise words to say about programming.
And I’ll borrow some of his wisdom by quoting (emphasis mine):
… result in beautiful diamond-like systems. This is sometimes the right idea, and we will see it arise again, but it is very hard to add to a diamond. If a system is built as a ball of mud, it is easy to add more mud. —Software Design for Flexibility, p.12211Jerry said that the diamond-vs-mud metaphor was not his invention. He quoted Joel Moses who allegedly said this first on the APL 79 conference, where APL was the diamond and Lisp was the mud. But Joel Moses denied to have said this, so I attribute this to Jerry. See footnote 11, Section 3, Software Design for Flexibility..
When it comes to Rust and Go. Rust is the diamond and Go is the mud22I remark that there’s no absolute scale of diamond-ness or mud-ness. Rust is the diamond when compared with Go but can be the mud in other cases—for example, when compared with Zig..
Rust rewards meticulous design ahead of time. You
carefully spec out the requirements, define the components of your
system, think through where every byte should be in the memory, and you
write beautiful Rust code with the right trait bounds and lifetimes that
works first-time and can’t be any faster. The long build time of Rust
and its static analysis both encourage planning over trials and errors.
Rust very much penalizes the “go ahead and figure things out along the
way” mindset: you will be hard-pushed to refactor the code every so
often or the entire language will rebel against you. At the end, you
either write good code—but thinking ahead would have taken you there
faster—or litter your code with Rc<RefCell<>>
and .clone()
, in which case both you and the running time
suffer. Rust offers abstractions to save you brain cells as users, but
often you design the abstraction first—and it’s hard. Writing Rust is
indeed like cleaving a diamond: the cost of making a wrong cut is high.
This is the price to pay if you want both security and performance; This
is part of the superiority complex; This makes an incredible learning
experience—what you internalize writing Rust makes you better at any
language; But this also means Rust is not good for everything.
I am unsure how my blog will evolve moving forward and have already lost steam once in the rewriting, so the brain fry of Rust is a bit too much. Sorry Rust—you have some of the fastest web frameworks, but my laziness wins.
Go, by contrast, is more lenient toward imperfections and hacks.
There are many perspectives to this claim. I’ll pick just one:
Go encourages the practice of silly, minor repetition over
clever, magical abstraction. Want to filter an array? Just
write a for
loop. Want to handle an error? Just write
if err != nil {}
. No need for a fancy
.iter().filter().collect()
or syntactic sugars. Instead, Go
wants you to program these simple, explicit patterns into your reflexes
and once you do that, Go programs with such repetitions are no less
readable than Rust programs with good abstraction while being
significantly easier to write. And it’s good for hacks too! Modifying a
piece of repetitive code is easy, because the full code is just there,
and nothing elsewhere will break. I can’t recall how many times I
eagerly refactored duplicate Rust code into helper functions (sometimes
generic) only to find later that a call site needs to be treated
slightly differently—huge pain. I don’t run into these cases in Go as
frequently because I feel less guilty leaving copies of Go code here and
there. This is much like making things with mud—you simply put two balls
of mud together to get a larger ball of mud. Of course, repetition is
painful when it comes to refactoring, but Go’s simplistic design allows
easily writing code to refactor programmatically, not to mention that Go
just eliminated a whole class of refactoring necessitated by premature
abstraction. The price to pay here is performance and the occasional
sense of peril that my Rust-cultured brain is slowly degenerating due to
lack of use—but it’s all acceptable for many projects, this rewrite
included.
When programming meets time
Early into the Distributed Systems class, the course staff invited Russ Cox, the Tech Lead33Russ Cox stepped down as Go Tech Lead in Sep 2024. at Google’s Go team, to give a guest lecture. Hearing such first-hand account of the rationales of Go’s many design decisions as a Go beginner is just fantastic. My biggest takeaway was not Go-specific, though, but rather:
Software Engineering is what happens to programming when you add time and other programmers. This has to be the most concise and beautiful way to say something anyone into programming for several years would vaguely touch upon. I was blown away.
And that’s what us students are under-exposed to—and hence under-appreciate. At school we learn programming, and after we graduate we work as Software Engineers. That change is not a mere word play. That’s an abrupt transition we don’t have much time to digest until we inevitably swim in 💩 code (and add to it ourselves). And indeed, how do you teach that? A three-week lab isn’t that long of a time, and a three-people team isn’t that many other programmers. MIT’s “software engineering class” is 6.03144Actually, the real “software engineering class” at MIT is compilers, where you build an optimizing compiler from scratch over three months with a team. I wrote ours in Rust without thinking it through—and I unsurprisingly suffered. That experience contributed much to my discussion on Rust in this post., where the instructor keeps reiterating ETU (easy to understand), SFB (safe from bugs), and RFC (ready to change). We were tested on them (but you can always ace tests), asked to evoke them in our code reviews (which eventually became a formality), yet for every assignment we always started from a clean slate. Now that I think of it, it would make better sense to have all homeworks be interdependent, and have someone’s next homework build on top of someone else’s previous submission through a shuffle process after code review55Someone taking 031 now please bring this up to RCM. Your classmates will thank you. Also don’t say I came up with this idea.. This way everyone gets a fair chance to ruin everyone else’s day weeks down the line with their legacy code 😋. That’s engineering education in its perfection.
Back to rewriting. I see maintaining a personal website as closer to engineering than most side projects I did. It certainly has that time element, since a blog can live however long. Moreover, I argue that it has a bit of the “other programmer” element too. I can foresee that my programming effort on my blog will be scarcely scattered throughout my spare time—so scarce that myself revisiting my blog’s code months from now is almost “another programmer” as far as familiarity is concerned. Hence, I should aim to write my blog such that maintaining it will have as low friction as possible to my future self.
Go is the right tool for this goal. Recall I said Go encourages minor repetition over premature abstraction? It is easier to trust a team of programmers to repeat consistently over a long time than to trust them to consistently pull off clever tricks. Diamond-like designs risk having its crystalline idea lost or distorted in time and communication. Muddy designs do not. Go recognizes that code in an engineering environment goes through lossy channels of time and space, where simplicity prevails.
There’s no silver bullet
In Rust’s defense: complex systems are complex anyhow. Go moves the complexity out of the core language but it does not magically go away. Architecting complex programs in Go is harder than in Rust, because compiler checks are automated and enforced, whereas human designs are not. At a large scale, Go likely requires more design patterns, testing and developer self-discipline than Rust. Graphically,
(And the same holds true if you change the x-axis to performance)
And luckily the rewrite of my blog is way to the left of the intersection—hence using Go is not a bad idea for now. We’ll see.
Conclusion
So this is how I ditched JavaScript for Go.
Or is it?
This post turns out to be surprisingly more philosophical than I’d expected, and contained a fair amount of references. Did I really think through all of them to arrive at the conclusion to use Go back then? Likely not, but I believe my mind did vaguely touched upon the key points here. I see blogging as a great way to flesh out and materialize immature ideas. Doing so retroactively after a decision is still useful. It gives me solid confidence that I can make similar decisions moving forward. As for you—I hope I’ve contributed some novel thoughts. I’ve always wanted to write a “language A vs B” post. This is my first try at it.
Choosing Go over JavaScript is not a beginner move in today’s web development landscape. The JS ecosystem, once setup, still has arguably the best DX (like HMR)—how did I make up for that in Go? Go is only for backend, so what frontend did I choose—did I ditch JavaScript completely? There isn’t room for these low-level execution details in this post. I’ll write about them in the next one.
Jerry said that the diamond-vs-mud metaphor was not his invention. He quoted Joel Moses who allegedly said this first on the APL 79 conference, where APL was the diamond and Lisp was the mud. But Joel Moses denied to have said this, so I attribute this to Jerry. See footnote 11, Section 3, Software Design for Flexibility.↩︎
I remark that there’s no absolute scale of diamond-ness or mud-ness. Rust is the diamond when compared with Go but can be the mud in other cases—for example, when compared with Zig.↩︎
Russ Cox stepped down as Go Tech Lead in Sep 2024.↩︎
Actually, the real “software engineering class” at MIT is compilers, where you build an optimizing compiler from scratch over three months with a team. I wrote ours in Rust without thinking it through—and I unsurprisingly suffered. That experience contributed much to my discussion on Rust in this post.↩︎
Someone taking 031 now please bring this up to RCM. Your classmates will thank you. Also don’t say I came up with this idea.↩︎