Using Golang Embed and Build Tags for a Self Contained App

In the recent released go has added some nice developer features. One of my favorites has been embed paired with fs.FS. Using these two together you can build a binary that contains all the required assets as well. This is my go to when developing a web project that needs templates or js/css to be deployed with it. There is just one problem with this though. During develop time, it isn’t the most convenient to use the compiled in templates or js/css.


Mar. 6, 2023 497 words golang ·

In the recent released go has added some nice developer features. One of my favorites has been embed paired with fs.FS. Using these two together you can build a binary that contains all the required assets as well. This is my go to when developing a web project that needs templates or js/css to be deployed with it.

There is just one problem with this though. During develop time, it isn’t the most convenient to use the compiled in templates or js/css. If you make a change you’d like to not have to rebuild your binary to test those updates. With the use of embed, fs.FS, and build tags you can use either the filesystem assets or an embedded set of assets to make either your development time or deployment time easier and more efficient.

The Project

Let’s pretend a project structure to work with, with the command to be build at cmd/{binary}.

With a file structure like this, we can now build a binary that either uses os.DirFS or embed.FS to expose an fs.FS to the rest of our application.

main.go

All our main.go file needs to have is a specific method or interface to call go load the static directory. In our case, it is LoadStatic() fs.FS.

package main

import (
	"log"
)

func main() {
	log.Println("Starting not just the scale")
	staticDir := LoadStatic()
	log.Println(staticDir)
}

staticembed.go

This is our file that will provide an embed.FS (which implements fs.FS) to our main.go file above. Notice two things, the go:embed static comment is expecting a directory that is a sibling of itself, in this case static. The second thing, we use fs.Sub to extract the static directory on to the return. This has to do with how we’re going to load the os.DirFS below, and how the rest of the app is expecting the fs.FS to be rooted.

//go:build release

package main

import (
	"embed"
	"io/fs"
	"log"
)

//go:embed static
var static embed.FS

func LoadStatic() fs.FS {
	staticDir, err := fs.Sub(static, "static")
	if err != nil {
		log.Fatalln("Error loading static embed", err)
	}
	return staticDir
}

static.go

I like working in my project top directory. So you’ll notice here, unlike above, the directory that we’re going to be loading is cmd/{binary}/static. That is because our current working directory sits above the cmd directory. Your project might not be like this, so your mileage may vary, easily tweak the path to fit your use case.

//go:build !release

package main

import (
	"io/fs"
	"os"
)

func LoadStatic() fs.FS {
	return os.DirFS("cmd/{binary}/static")
}

How to build

Now that we have our project structure laid out we can build our project in two main ways.

  1. Build the project for development time:
    • go build|run cmd/{binary}
  2. Build the project for release:
    • go build -tags release cmd/{binary}

You should notice the only real difference is for the release build we’re passing -tags release to our build. This will include our embed.FS version of the LoadStatic call.