No regrets though.
Just a few weeks later I was deploying the Go application. Building it was the most fun I had in months, I learned a ton and the end result is a huge improvement over the old application. Better performance, easier deployments and higher test coverage.
The application is a fairly straightforward database driven API & account area where users can log-in to download the product, view their invoices or update their payment method.
While Laravel worked well enough for this, some things always felt overcomplicated to me. And what's with releasing a new "major" version every few months? I'd be fine if the newer versions contained significant improvements, but a lot of times it just felt like minor naming & directory structure changes to me.
Last year I've been moving several services over to Go, so I wasn't completely new to the language. As a developer selling WordPress based products, part of my job is working in an ancient tech stack that is mostly focused on the end user.
If I weren't self-employed, I would simply apply for a new job to make up for this lack of sexy tech. Being my own boss, I owe it to myself to make my day-to-day work fun and not just chase more immediate $$$. If revenue allows (and it does), why not have a little fun?
It's a joy to write Go code, the tooling is amazing and it's not only fast to develop in, the end result is usually crazy fast too. Just reading about the purpose of the Go project sold me on the language.
Porting the codebase
Migrating the code to Go consisted mostly about getting the database interaction right & porting the Blade templates to something we could use in Go.
ORM's are one thing that always end up getting in my way, so I went for a mockable data access layer and plain SQL queries. Meddler was used to get rid of some of the boilerplate for scanning query results into structs.
To support hierarchical templates and partials I open-sourced grender, a tiny wrapper around Go's standard html/template package. This allowed me to port the Blade template files to Go with relative ease, since I could use the same hierarchical structure and partial templates.
For integrating with Stripe there is the official stripe-go package. For Braintree there is the unofficial braintree-go package, which was neglected for a little while but received renewed attention lately. Since there was no Go package to manage invoices in Moneybird yet, I built and open-sourced moneybird-go.
Comparing apples vs oranges
Since Go is a compiled language with a much better standard library than PHP, it is not really fair to compare the two languages like I am about to. That said, I thought it would be fun to share some numbers.
wrk was used to perform some simple HTTP benchmarks for both applications returning the HTML for the login page.
|Concurrency||Avg. latency||Req / sec||Transfer / sec|
Unfortunately, the Laravel application (or PHP-FPM socket) kept falling over once I increased the number of concurrent "users" past 100.
NetData provided the following graphs to see how the server was holding up under all this load.
Please note that I ran the benchmark from the same machine as the applications were running on, so this heavily influences both graphs.
Lines of code
Let's compare the lines of code in both applications, including all vendored dependencies.
$ find . -name '*.php' | xargs wc -l 156289 total
The Laravel version consists of just over 156.000 lines of code. This is excluding development dependencies which, with Laravel, are needed to run tests etc.
$ find . -name '*.go' | xargs wc -l 33624 total
The Go version on the other hand consists of 33.000 lines of code. That's one fifth of the code for exactly the same functionality.
Let's exclude external dependencies in the Laravel application so we know how much lines were actually written by me.
$ find . -name '*.php' -not -path "./vendor/*" | xargs wc -l 13921 total
And for Go.
$ find . -name '*.go' -not -path "./vendor/*" | xargs wc -l 6750 total
The result is slightly more even when just looking at managed lines of code. Still, it's the exact same application with half the amount of code.
Testing is a first class citizen in Go, and test files live right next to the actual source files.
license.go license_test.go subscription.go subscription_test.go
This makes it incredibly convenient to apply test driven development.
In our Laravel application we mostly had integration tests that checked whether the request handlers returned a proper response. Overall test coverage was quite low, mostly due to tight coupling which in turn was mostly my fault. Writing the same application a second time really helps here too.
Did something you should never do: rewrote an application in a different language because I felt like it. Had lots of fun and got a much smaller & faster application in return.
Edited on April 19: 120ms latency
with lots of disk writing for the Laravel benchmark
didn't seem right so I revisited it. Turns out I had
APP_DEBUG set to
templates were recompiled on every request. Oops. The
post has been updated with the correct benchmarking