Some Go web dev notes
I spent a lot of time in the past couple of weeks working on a website in Go
that may or may not ever see the light of day, but I learned a couple of things
along the way I wanted to write down. Here they are:
go 1.22 now has better routing
Iâve never felt motivated to learn any of the Go routing libraries
(gorilla/mux, chi, etc), so Iâve been doing all my routing by hand, like this.
// DELETE /records:
case r.Method == "DELETE" && n == 1 && p[0] == "records":
if !requireLogin(username, r.URL.Path, r, w) {
return
}
deleteAllRecords(ctx, username, rs, w, r)
// POST /records/<ID>
case r.Method == "POST" && n == 2 && p[0] == "records" && len(p[1]) > 0:
if !requireLogin(username, r.URL.Path, r, w) {
return
}
updateRecord(ctx, username, p[1], rs, w, r)
But apparently as of Go 1.22, Go
now has better support for routing in the standard library, so that code can be
rewritten something like this:
mux.HandleFunc("DELETE /records/", app.deleteAllRecords)
mux.HandleFunc("POST /records/{record_id}", app.updateRecord)
Though it would also need a login middleware, so maybe something more like
this, with a requireLogin middleware.
mux.Handle("DELETE /records/", requireLogin(http.HandlerFunc(app.deleteAllRecords)))
a gotcha with the built-in router: redirects with trailing slashes
One annoying gotcha I ran into was: if I make a route for /records/, then a
request for /records will be redirected to /records/.
I ran into an issue with this where sending a POST request to /records
redirected to a GET request for /records/, which broke the POST request
because it removed the request body. Thankfully Xe Iaso wrote a blog post about the exact same issue which made it
easier to debug.
I think the solution to this is just to use API endpoints like POST /records
instead of POST /records/, which seems like a more normal design anyway.
sqlc automatically generates code for my db queries
I got a little bit tired of writing so much boilerplate for my SQL queries, but
I didnât really feel like learning an ORM, because I know what SQL queries I
want to write, and I didnât feel like learning the ORMâs conventions for
translating things into SQL queries.
But then I found sqlc, which will compile a query like this:
-- name: GetVariant :one
SELECT *
FROM variants
WHERE id = ?;
into Go code like this:
const getVariant = `-- name: GetVariant :one
SELECT id, created_at, updated_at, disabled, product_name, variant_name
FROM variants
WHERE id = ?
`
func (q *Queries) GetVariant(ctx context.Context, id int64) (Variant, error) {
row := q.db.QueryRowContext(ctx, getVariant, id)
var i Variant
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Disabled,
&i.ProductName,
&i.VariantName,
)
return i, err
}
What I like about this is that if Iâm ever unsure about what Go code to write
for a given SQL query, I can just write the query I want, read the generated
function and itâll tell me exactly what to do to call it. It feels much easier
to me than trying to dig through the ORMâs documentation to figure out how to
construct the SQL query I want.
Reading Brandurâs sqlc notes from 2024 also gave me some confidence
that this is a workable path for my tiny programs. That post gives a really
helpful example of how to conditionally update fields in a table using CASE
statements (for example if you have a table with 20 columns and you only want
to update 3 of them).
sqlite tips
Someone on Mastodon linked me to this post called Optimizing sqlite for servers. My projects are small and Iâm
not so concerned about performance, but my main takeaways were:
- have a dedicated object for writing to the database, and run
db.SetMaxOpenConns(1)on it. I learned the hard way that if I donât do this
then Iâll getSQLITE_BUSYerrors from two threads trying to write to the db
at the same time. - if I want to make reads faster, I could have 2 separate db objects, one for writing and one for reading
There are a more tips in that post that seem useful (like âCOUNT queries are
slowâ and âUse STRICT tablesâ), but I havenât done those yet.
Also sometimes if I have two tables where I know Iâll never need to do a JOIN
beteween them, Iâll just put them in separate databases so that I can connect
to them independently.
Go 1.19 introduced a way to set a GC memory limit
I run all of my Go projects in VMs with relatively little memory, like 256MB or
512MB. I ran into an issue where my application kept getting OOM killed and it
was confusing â did I have a memory leak? What?
After some Googling, I realized that maybe I didnât have a memory leak, maybe I
just needed to reconfigure the garbage collector! It turns out that by default (according to A Guide to the Go Garbage Collector), Goâs garbage collector will
let the application allocate memory up to 2x the current heap size.
Mess With DNSâs base heap size is around 170MB and
the amount of memory free on the VM is around 160MB right now, so if its memory
doubled, itâll get OOM killed.
In Go 1.19, they added a way to tell Go âhey, if the application starts using
this much memory, run a GCâ. So I set the GC memory limit to 250MB and it seems
to have resulted in the application getting OOM killed less often:
export GOMEMLIMIT=250MiB
some reasons I like making websites in Go
Iâve been making tiny websites (like the nginx playground) in Go on and off for the last 4 years or so and itâs really been working for me. I think I like it because:
- thereâs just 1 static binary, all I need to do to deploy it is copy the binary. If there are static files I can just embed them in the binary with embed.
- thereâs a built-in webserver thatâs okay to use in production, so I donât need to configure WSGI or whatever to get it to work. I can just put it behind Caddy or run it on fly.io or whatever.
- Goâs toolchain is very easy to install, I can just do
apt-get install golang-goor whatever and then ago buildwill build my project - it feels like thereâs very little to remember to start sending HTTP responses
â basically all there is are functions likeServe(w http.ResponseWriter, r *http.Request)which read the request and send a response. If I need to
remember some detail of how exactly thatâs accomplished, I just have to read
the function! - also
net/httpis in the standard library, so you can start making websites
without installing any libraries at all. I really appreciate this one. - Go is a pretty systems-y language, so if I need to run an
ioctlor
something thatâs easy to do
In general everything about it feels like it makes projects easy to work on for
5 days, abandon for 2 years, and then get back into writing code without a lot
of problems.
For contrast, Iâve tried to learn Rails a couple of times and I really want
to love Rails â Iâve made a couple of toy websites in Rails and itâs always
felt like a really magical experience. But ultimately when I come back to those
projects I canât remember how anything works and I just end up giving up. It
feels easier to me to come back to my Go projects that are full of a lot of
repetitive boilerplate, because at least I can read the code and figure out how
it works.
things I havenât figured out yet
some things I havenât done much of yet in Go:
- rendering HTML templates: usually my Go servers are just APIs and I make the
frontend a single-page app with Vue. Iâve usedhtml/templatea lot in Hugo (which Iâve used for this blog for the last 8 years)
but Iâm still not sure how I feel about it. - Iâve never made a real login system, usually my servers donât have users at all.
- Iâve never tried to implement CSRF
In general Iâm not sure how to implement security-sensitive features so I donât
start projects which need login/CSRF/etc. I imagine this is where a framework
would help.
itâs cool to see the new features Go has been adding
Both of the Go features I mentioned in this post (GOMEMLIMIT and the routing)
are new in the last couple of years and I didnât notice when they came out. It
makes me think I should pay closer attention to the release notes for new Go
versions.
