Skip to Content
Course content

210: Serving Embedded Static Assets

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you first encounter the embed package, there is a very common temptation to treat it as a simple "file-to-string" converter. I see this all the time: a developer embeds a CSS file into a string variable and then manually writes an HTTP handler that sets the Content-Type and writes that string to the response. While that works for one file, it's a tedious way to build a real application.

Thinking the 'embed' package just loads files into variables

The mistake is thinking that //go:embed is just a shortcut for os.ReadFile during the build process. You might try something like this for your project's stylesheets:

type App struct {
    StyleSheet string
}

//go:embed static/style.css
var styleData string

func (a *App) handleCSS(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/css")
    fmt.Fprint(w, styleData)
}

This approach is a nightmare to maintain. If you add a second CSS file, or a folder of images, or a few JavaScript bundles, you'll end up with a dozen global variables and a mess of manual handlers. You're fighting the language instead of using the tool. You aren't just loading data; you're defining a filesystem that lives inside your binary.

Mapping a Virtual Filesystem to your HTTP Handler

The real power of the embed package comes from embed.FS. Instead of loading a file into a string, you load a whole directory into a read-only filesystem. The magic happens when you combine this with http.FS, which converts that embedded filesystem into something the http package knows how to serve.

Let's say we're building a "System Health Dashboard" and we have a folder called web containing index.html and a style.css. Here is how I actually implement this in production:

package main

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

//go:embed web/*
var webAssets embed.FS

func main() {
    // We don't want the URL to be /web/style.css, we want /style.css.
    // fs.Sub strips the "web" prefix from the embedded paths.
    content, err := fs.Sub(webAssets, "web")
    if err != nil {
        panic(err)
    }

    // http.FS converts our embedded FS into an http.FileSystem
    fileServer := http.FileServer(http.FS(content))

    // Now we serve everything in that folder under the root path
    http.Handle("/", fileServer)

    http.ListenAndServe(":8080", nil)
}

I personally love this pattern because it makes your deployment a dream. You ship a single binary file to your server, and you don't have to worry about scp-ing a /static folder or worrying about relative paths breaking because the binary is running from a different working directory. The assets are literally baked into the executable.

One detail to watch out for: fs.Sub is critical. Without it, your assets are nested under the folder name you embedded. If you embed web/*, your browser would have to request /web/style.css. By using fs.Sub, you "jail" the filesystem to that folder, making the contents of web/ appear as the root of your server.




📋 Practical Task

Build a Self-Contained Documentation Portal

Your goal is to create a single-binary Go application that serves a small documentation site. You must ensure that the assets are embedded and served without the folder name appearing in the URL.

Requirements:

  • Create a directory named docs in your project root.
  • Inside docs, create two files: index.html and help.html.
  • Use the embed package to include the entire docs directory.
  • Use fs.Sub to ensure that http://localhost:8080/index.html serves the docs/index.html file (and NOT http://localhost:8080/docs/index.html).
  • Use http.FileServer to handle the routing.

Verification: When you run the binary, you should be able to navigate to both HTML pages in your browser without seeing the word "docs" in the URL path.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.