Serving Embedded Static File Inside Subdirectory Using Go

tech · Dec 30, 2021 · ~3 min
Photo by @iammrcup on Unsplash
Photo by @iammrcup on Unsplash

Introduction

As embed package released on Golang v1.16, now you can include any of your files into your golang binary. You can also serve the embedded file as a static site. But when it comes to subdirectory, you need to handle it a bit different.

In this article, you will learn how to serve embedded files as a static site that stored inside a subdirectory.

Directory Structure

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ tree .
.
├── frontend
│   └── public
│       ├── assets
│       │   ├── css
│       │   │   └── style.css
│       │   └── js
│       │       └── index.js
│       └── index.html
├── go.mod
└── main.go

HTML Content

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
    <script src="/assets/js/index.js"></script>
</body>
</html>

To test if the assets can be imported using absolute and relative URL, you can write the link rel and script src like the snippet above. You can put anything inside your style.css and index.js just to make sure it is loaded.

The Problem

Before embed package is released, you used to use http.Dir to serve a static site using Go like below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import (
    "net/http"
)

func dirHandler() http.Handler {
    return http.FileServer(http.Dir("frontend/public"))
}

func main() {
    http.ListenAndServe(":8000", dirHandler())
}

And yes, it works like a charm. But, when it comes to embed:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
    "embed"
    "net/http"
)

//go:embed frontend/public
var public embed.FS

func fsHandler() http.Handler {
    return http.FileServer(http.FS(public))
}

func main() {
    http.ListenAndServe(":8000", fsHandler())
}

It doesn’t work because you didn’t specified which subdirectory will be served. http.FS will serve the embedded files from root, that’s why instead of http://localhost:8000/, you need to access it using http://localhost:8000/frontend/public/.

But the other problem is, it makes your absolute import not working. /assets/js/index.js will not work because it accesses the file from the root.

The Solution

To solve those problems, you can use fs.Sub to get the subtree of the embedded files. So you can start get the frontend/public directory as a root.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//go:embed frontend/public
var public embed.FS

func fsHandler() http.Handler {
    sub, err := fs.Sub(public, "frontend/public")
    if err != nil {
        panic(err)
    }

    return http.FileServer(http.FS(sub))
}

Now, you can access the site using http://localhost:8000/ and both your relative and absolute import works again!

Thank you for reading!

· · ·

Love This Content?

Any kind of supports is greatly appreciated! Kindly support me via Bitcoin, Ko-fi, Trakteer, or just continue to read another content. You can write a response via Webmention and let me know the URL via Telegraph.

Drop Your Comment Below