Barak Amar
January 26, 2021

The new Golang v1.16 embed directive helps us keep a single binary and bundle out static content. This post will cover how to work with embed directive by applying it to a demo application. 

Why Embed

One of the benefits of using Go is having your application compiled into a single self-contained binary. Having a way to embed files in Go programs is the missing piece that helps us keep a single binary and bundle out static content.

Code, shared libraries, or virtual machines are not required to run the application (some of the reasons containerized solutions became popular).

Having all of our static resources also part of the final binary simplifies the deployment of our solution. As it does not require setting up the same folder structure on the deployed machine, worrying about missing files, or failing to load at runtime. Making it easier and safer to deploy. 

There are many cases where bootstrap information or default settings are required. Embedding these resources gives you quick access to the code without additional deployment requirements.

Currently, several tools embed static assets (ex: packr, statik, fileembed, etc) but having the feature as part of the language simplifies the build process and “standardizes” the way we embed in Go.

How

Embed is a directive used to initialize a variable with content read from a file, or a collection of files with their content. Go v1.16 is required to use this feature – at the time of the blog, still in beta and can be installed side-by-side following the installation instructions on Go’s Download page.

The “embed” directive supports only the following three variable types, and requires importing the “embed” package:

  • String man initialized with the contents of manual.txt
import _ "embed"

//go:embed manual.txt
var man string
  • Bytes slice logo initialized with the contents of logo.png
import _ "embed"

//go:embed logo.png
var logo []byte
  • The third option enables the capture of a collection of files and directories by using the “embed” package’s FS type.

FS implements fs.FS interface, which is the minimum implementation required of the file system. This interface can be used while working with packages like net/http, text/template, and html/template.

import "embed"

//go:embed dist/*
var webapp embed.FS

Embed Demo Application

The demo application that we will make is a simple postcard image generator. Accept text input from the user and generate a postcard with the text.

We will also include the application’s LICENSE content into the application and print it by demand.

Go postcard

License

Including a LICENSE is very straightforward, we just define a variable to hold it and specify the file.

//go:embed LICENSE
var license string

We will use the Go’s standard flag package to parse -license flag and print out the license.

 printLicense := flag.Bool("license", false, "Print license")
 flag.Parse()
 if *printLicense {
     fmt.Println(license)
     return
 }

Generate Postcard Image

The postcard composted from the card itself – postcard.png, stamp randomly selected from the list of stamps and the user text we render above.

//go:embed postcard.png
var postcardPNG []byte

//go:embed stamp*.png
var stampsPNG embed.FS

Second will be the font file that we will use to render the text on our postcard – ClarendonBT.ttf

//go:embed ClarendonBT.ttf
var clarendonBT []byte

Using the font and freetype packages we implement a function that will render the final postcard.

func generatePostcard(lines []string) image.Image {
    postcardImg, _ := png.Decode(bytes.NewReader(postcardPNG))
    fnt, _ := truetype.Parse(clarendonBT)
    stamps, _ := stampsPNG.ReadDir(".")
    stampName := stamps[rand.Intn(len(stamps))].Name()
    stampFile, _ := stampsPNG.Open(stampName)
    stampImg, _ := png.Decode(stampFile)
    b := postcardImg.Bounds()
    postcard := image.NewRGBA(b)
    draw.Draw(postcard, b, postcardImg, image.Point{0, 0}, draw.Src)
    draw.Draw(postcard, stampImg.Bounds().Add(image.Point{835, 45}), stampImg, image.Point{0, 0}, draw.Over)
    const fontSize = 24
    d := &font.Drawer{
        Dst:  postcard,
        Src:  image.Black,
        Face: truetype.NewFace(fnt, &truetype.Options{Size: fontSize}),
    }
    const lineSpacing = fontSize * 2
    d.Dot = fixed.Point26_6{X: fixed.I(50), Y: fixed.I(275 + fontSize)}
    for _, line := range lines {
        d.DrawString(line)
        d.Dot.X = fixed.I(50)
        d.Dot.Y += fixed.I(lineSpacing)
    }
    return postcard
}

Note that error handling was removed.

User Interface

The user-interface accepts the user’s content. Post the information to our server and get back a postcard image. An HTML form with a text area will be good enough for our example.

index.html

<html>
    <head>
        <title>myPostcard</title>
        <link rel="stylesheet" href="styles.css">
    </head>
    <body>
        <p class="title">myPostcard</p>
        <form action="/postcard" method="post">
            <label for="content">Content</label>
            <textarea id="content" rows="7" cols="35" name="postcard" autofocus></textarea>
            <input class="button" type="submit" value="Generate">
        </form>
    </body>
</html>

Throwing some style

styles.css

body { background-color:white; margin:50px 100px; font-family:verdana; }
p { font-size:18; }
input, label { display:block; }
.title { color:teal; font-size:30; }
.button { margin:10px 0px; padding:10px; color:white; border:none; background-color:teal; }

Using the embed.FS we will embed our UI’s html, css and LICENSE files into webUI.

Note that the compiler will include the content of LICENSE only once in our final binary although we used it here and for our license flag option.

//go:embed *.html *.css LICENSE
var webUI embed.FS

And using Go’s http package we will serve the UI static content by passing the webUI to our file server handler.

http.Handle("/", http.FileServer(http.FS(webUI)))

Connecting The Dots

In order to deliver the postcard image with the request text content, we will need to implement a handler to handle form POST requests.

http.HandleFunc("/postcard", postcardHandler)

It should format the input, generate the postcard image, encode it as known format (PNG in this case) and write it back.

func postcardHandler(w http.ResponseWriter, r *http.Request) {
    text := r.FormValue("postcard")
    text = strings.ReplaceAll(text, "\r", "")
    lines := strings.Split(text, "\n")
    postcard := generatePostcard(lines)
    var b bytes.Buffer
    err := png.Encode(&b, postcard)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    w.Header().Set("Content-Type", "image/png")
    w.Header().Set("Content-Length", strconv.Itoa(len(b.Bytes())))
    _, err = w.Write(b.Bytes())
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

All Inside

All the above is inside our single self-contained binary, you can run the service and browse to your http://localhost:8000, post text and hit Generate to render a postcard.

Or you can execute it with -license to get a print of the license we included.
The full source of the demo project and files can be found here.


If you enjoyed this article, check out our Github repo and related posts:

LakeFS

  • Get Started
    Get Started