Go Embed and Angular

Hi, there. Today’s article will be a rather short article. In this article I would like to showcase Go 1.16 new embed package. If you are familiar with Go you might know embedding functionality already from famous other libraries like go-bindata. The problem with go-bindata has been that upstream vanished one day and then multiple forks appeared and every company or person was doing their own thing with embedding assets into Go programs. With the new embed package this shall be changed and embedding files shall be officially supported and more easy in the future.

For this article I have picked a github project: https://github.com/Shpota/go-angular. Sasha’s project is an excellent example on how we can make use of the new embed package.

The project consists of two important directories:

  • server: includes all Go code
  • webapp: includes the Angular App

Getting more into frontend development has been one of my main goals with this article, hence forgive me if I write something wrong about it. The Angular app can be build with the following commands:

$ cd webapp
$ npm install
$ ./node_modules/.bin/ng build --prod

On default, this will produce a dist directory storing the ‘compiled’ Angular project. In Sasha’s version Sasha served this assets via a http.Fileserver:

func (a *App) start() {
	a.db.AutoMigrate(&student{})
	a.r.HandleFunc("/students", a.getAllStudents).Methods("GET")
	a.r.HandleFunc("/students", a.addStudent).Methods("POST")
	a.r.HandleFunc("/students/{id}", a.updateStudent).Methods("PUT")
	a.r.HandleFunc("/students/{id}", a.deleteStudent).Methods("DELETE")
	a.r.PathPrefix("/").Handler(http.FileServer(http.Dir("./webapp/dist/webapp/")))
	log.Fatal(http.ListenAndServe(":8080", a.r))
}

The disadvantage of this approach is that you will end up with a Go binary and a separate, dedicated webapp/dist/webapp directory with all static asset files. This might does not matter if you plan to serve your application in a Docker image anyway, but a single Go binary can be useful sometimes for other deployment scenarios.

Therefore I made a few modifications to use the new Go 1.16 embed package with this project. If you like to skip directly to my changes feel free to check out the following link: https://github.com/shibumi/go-angular.

First of all I have changed the output path for the Angular generated assets in the angular.json file via setting "outputPath": "../server/static". With this change Angular will move the static assets to a new static directory inside of the server directory. Why do we need this? We need this, because the embed package does not support ../, ./ or leading slashes, hence we cannot import data from the webapp (one possible solution is to place a Go file in the webapp directory, but I have not tried this).

The next change I made was a slightly modification of the app.go file:

// first I introduced a new global variable
//go:embed static
var static embed.FS

// .....

// Then I modified the start() function accordingly:

func (a *App) start() {
	a.db.AutoMigrate(&student{})
	a.r.HandleFunc("/students", a.getAllStudents).Methods("GET")
	a.r.HandleFunc("/students", a.addStudent).Methods("POST")
	a.r.HandleFunc("/students/{id}", a.updateStudent).Methods("PUT")
	a.r.HandleFunc("/students/{id}", a.deleteStudent).Methods("DELETE")
	// We need to strip the static directory from our path
	// for serving files in the index folder via the http.Fileserver()
	webapp, err := fs.Sub(static, "static")
	if err != nil {
		fmt.Println(err)
	}
	// We need to use Gorilla Mux' PathPrefix function here, because the Pathprefix
	// adds a wildcard to the route eg: /*, otherwise we would only route to "/"
	// Hence the error with 404-returning JS files before got thrown, because
	// Gorilla Mux had no route to these JS files.
	a.r.PathPrefix("/").Handler(http.FileServer(http.FS(webapp)))
	log.Fatal(http.ListenAndServe(":8080", a.r))
}

What is happening here? First I introduced a new global variable called static with type embed.FS. The important part about this change is the go preprocessor-like statement before the variable declaration. With //go:embed static we explain the Go compiler to embed the static directory in the current directory via the embed package. Note: the missing space between // and go is important here! The next modification is the start() function. We are now serving content from a directory, for example: static/index.html, thus we need to strip the static directory name from it. This happens via the fs.Sub method. The last change is the use of http.FS instead of http.Dir. We are dealing with a filesystem now, not a local directory anymore. If we now compile the Angular app and compile our Go binary the Angular generated assets will get included into our Go binary and we have a single binary for deployment.

A few other changes I made were replacing the postgres driver against a sqlite (because i was lazy and just wanted a DB) and a new Dockerfile. The new Dockerfile makes use of Google’s distroless docker image. Distroless images are basically like docker scratch images, with the difference that they provide tzdata and ca-certificates and other data applications might need. Everything else (libraries, shells, busybox utils, etc) is missing in these images. The final Dockerfile looks like this:

FROM node:12.11 AS ANGULAR_BUILD
RUN npm install -g @angular/cli@8.3.12
COPY webapp /webapp
WORKDIR webapp
RUN npm install && ng build --prod

FROM golang:1.16 as GO_BUILD
WORKDIR /go/src/app
ADD server /go/src/app
COPY --from=ANGULAR_BUILD /server/static /go/src/app
RUN go build -o /go/bin/app

FROM gcr.io/distroless/base
COPY --from=GO_BUILD /go/bin/app /
CMD ["/app"]