lewisdale.dev/src/blog/posts/2024/5/learning-go-day-14.md
Lewis Dale 7934f5f75b
All checks were successful
Build and copy to prod / build-and-copy (push) Successful in 2m19s
More learning Go: bundling more static and HTML
2024-05-15 07:55:53 +01:00

4.9 KiB

title date tags excerpt
Learning Go: Day Fourteen 2024-05-17T08:00:00.0Z
learning
go
css
Nesting templates, serving static files, and some unexpected (to me) behaviour with request matching

In my last post, I managed to get static files bundled into my binary and deployed to the server, meaning I can easily create some HTML output. Now, I want to make that output actually something worthwhile.

Nesting templates

This HTML templating language is pretty powerful, and works fairly similarly to the Nunjucks templates I'm used to using for this blog. To make some of my HTML more repeatable, I want to be able to split my template files up so I can reuse them.

It turns out this is pretty straightforward, as Go provides a template tag for doing exactly that. So I can extract the content of my head section into head.html, and then just reference it in index.html:

{% raw %}

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
    {{ template "head.html" . }}
    <body>
        <h1>Oopsie uptime monitoring</h1>
    </body>
</html>

<!-- head.html -->
<head>
    <title>Oopsie</title>
</head>

{% endraw %}

And then just add my template to the list of templates passed to ParseFS:

// main.go
t, _ := template.ParseFS(content, "templates/index.html", "templates/head.html")

And that works! The output is the same, but now I can reuse the head tag easily.

Template blocks

I can actually go even further. If I then create a template called base.html, which contains the general structure of one my web pages, I can define a block in there, which will let me just define content in templates that inherit it, like index.html.

{% raw %}

<!-- base.html -->
<!DOCTYPE html>
<html lang="en">
    {{ template "head.html" . }}
    <body>
        {{ block "content" . }}{{ end }}
    </body>
</html>

<!-- index.html -->
{{ template "base.html" . }}

{{ define "content" }}
<h1>Oopsie uptime monitoring</h1>
{{ end }}

{% endraw %}

And of course I then need to add it to my list of parsed templates:

// main.go
t, _ := template.ParseFS(content, "templates/index.html", "templates/base.html", "templates/head.html")

Cool. This has had limited impact so far, because I've got no content, but it'll make creating new pages far easier.

Serving static files

Now I'm going to need some static files. In particular, I want to serve some CSS. For now, I've just added Andy Bell's excellent reset CSS to a newly-created static directory. I can use the embed package to embed the static files in my binary, just like I did with my template. Then if I want to serve them, it turns out it's ludicrously easy:

// main.go

//go:embed static/*
var staticFiles embed.FS

http.Handle("/static/", http.FileServerFS(staticFiles))

The HTTP library has premade handlers for serving static files, including ones that can take an instance of FS, much like the one created by embed. This was too easy, right?

Yep, sort of

When I ran the code, I got this error

panic: pattern "GET /" (registered at /Users/lewis/Development/personal/oopsie/main.go:38) conflicts with pattern "/static/" (registered at /Users/lewis/Development/personal/oopsie/main.go:36):
GET / matches fewer methods than /static/, but has a more general path pattern

goroutine 1 [running]:
net/http.(*ServeMux).register(...)
        /Users/lewis/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.2.darwin-arm64/src/net/http/server.go:2733
net/http.HandleFunc({0x1025590f0?, 0x140000ddf28?}, 0x1026ce3a8?)
        /Users/lewis/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.2.darwin-arm64/src/net/http/server.go:2727 +0x9c
main.main()
        /Users/lewis/Development/personal/oopsie/main.go:38 +0x218
exit status 2

It looks like my GET / pattern is also matching /static/, for some reason? My expectation would be that / matches only the root path, and nothing else1. But apparently not, it'll just match anything. However, the fix is pretty simple, I just need to append {$} to the root patterns:

http.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {

And that limits it to only the root path, and I'm able to successfully run my code. So far Go is really impressing me with how easy it is to build this stuff. I've been so entrenched in the NodeJS/Typescript world over the last few months I forgot how nice it is to have a decent standard library2.

The bundled binaries are fairly large - as of writing it's around 14mb, but that's kind of to be expected given all the stuff it's doing. I'm not going to bother reducing it, it's not like I'm serving the binary to the client - in fact right now the client only receives around 1.8kb of data.


  1. This is why you should always read the docs ↩︎

  2. I'm being unfair, the NodeJS standard library is so much better these days, I'm just not using a lot of it ↩︎