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.
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.