Serving Static Files from Go

How to easily serve static files in a Go app.

I have included this in so many apps and even in blog posts, but never front and center. I think this is a very handy pattern and should be easy to discover, so here we are.

Here’s how I usually start. The use of go:embed means that the assets are directly built into the Go binary, which is conventient for deployment. The use of fs.Sub in run() means that a file at path assets/x.txt could be accessed at http://localhost:8080/x.txt. This is the detail I forget the most often and what motivated me to write this post down.

package main

import (
	"fmt"
	"embed"
	"io/fs"
	"net/http"
	"os"
)

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

//go:embed assets/*
var assets embed.FS

func run() error {
	mux := http.NewServeMux()
	sub, err := fs.Sub(assets, "assets")
	if err != nil {
		return err
	}
	mux.Handle("/", http.FileServer(http.FS(sub)))
	return http.ListenAndServe(":8080", mux)
}

The drawback to the above is that the assets, being embedded directly into the binary, will not change when you modify the files. I think that for deployed code that’s perfectly fine, but during development it can be annoying. Here’s a more complicated version. This takes advantage of build tags, though you could just as easily make this a commandline flag:

main.go:

package main

import (
	"fmt"
	"net/http"
	"os"
)

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func run() error {
	mux := http.NewServeMux()
	mux.Handle("/", http.FileServer(http.FS(assets())))
	return http.ListenAndServe(":8080", mux)
}

main_prod.go:

//go:build !dev
// +build !dev

package main

import (
	"embed"
	"io/fs"
)

//go:embed assets/*
var _assets embed.FS

func assets() fs.FS {
	sub, err := fs.Sub(_assets, "assets")
	if err != nil {
		panic(err)
	}
	return sub
}

main_dev.go:

//go:build dev
// +build dev

package main

import (
	"io/fs"
	"os"
)

func assets() fs.FS {
	return os.DirFS("assets")
}

The above code, when run or built normally will function just like the first version. But if you run it with go run -tags dev . or build it with go build -tags dev . you’ll get a binary that serves whatever is in assets like a normal file serving http server.

Posted Tue, Mar 7, 2023

If you're interested in being notified when new posts are published, you can subscribe here; you'll get an email once a week at the most.