Containerize Your Go Developer Environment – Part 2

This is the second part in a series of posts where we show how to use Docker to define your Go development environment in code. The goal of this is to make sure that you, your team, and the CI are all using the same environment. In part 1, we explained how to start a containerized development environment for local Go development, building an example CLI tool for different platforms and shrinking the build context to speed up builds. Now we are going to go one step further and learn how to add dependencies to make the project more realistic, caching to make the builds faster, and unit tests.

Adding dependencies

The Go program from part 1 is very simple and doesn’t have any dependencies Go dependencies. Let’s add a simple dependency – the commonly used github.com/pkg/errors package:

package mainimport (   “fmt”   “os”   “strings”   “github.com/pkg/errors”)func echo(args []string) error {   if len(args) < 2 {       return errors.New(“no message to echo”)   }   _, err := fmt.Println(strings.Join(args[1:], ” “))   return err}func main() {   if err := echo(os.Args); err != nil {       fmt.Fprintf(os.Stderr, “%+vn”, err)       os.Exit(1)   }}

Our example program is now a simple echo program that writes out the arguments that the user inputs or “no message to echo” and a stack trace if nothing is specified.

We will use Go modules to handle this dependency. Running the following commands will create the go.mod and go.sum files:$ go mod init$ go mod tidy

Now when we run the build, we will see that each time we build, the dependencies are downloaded$ make[+] Building 8.2s (7/9) => [internal] load build definition from Dockerfile…0.0s => [build 3/4] COPY . . 0.1s => [build 4/4] RUN GOOS=darwin GOARCH=amd64 go build -o /out/example . 7.9s => => # go: downloading github.com/pkg/errors v0.9.1

This is clearly inefficient and slows things down. We can fix this by downloading our dependencies as a separate step in our Dockerfile:

FROM –platform=${BUILDPLATFORM} golang:1.14.3-alpine AS buildWORKDIR /srcENV CGO_ENABLED=0COPY go.* .RUN go mod downloadCOPY . .ARG TARGETOSARG TARGETARCHRUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /out/example .FROM scratch AS bin-unixCOPY –from=build /out/example / …

Notice that we’ve added the go.* files and download the modules before adding the rest of the source. This allows Docker to cache the modules as it will only rerun these steps if the go.* files change.

Caching

Separating the downloading of our dependencies from our build is a great improvement but each time we run the build, we are starting the compile from scratch. For small projects this might not be a problem but as your project gets bigger you will want to leverage Go’s compiler cache.

To do this, you will need to use BuildKit’s Dockerfile frontend (https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/experimental.md). Our updated Dockerfile is as follows:

# syntax = docker/dockerfile:1-experimentalFROM –platform=${BUILDPLATFORM} golang:1.14.3-alpine AS buildARG TARGETOSARG TARGETARCHWORKDIR /srcENV CGO_ENABLED=0COPY go.* .RUN go mod downloadCOPY . .RUN –mount=type=cache,target=/root/.cache/go-build GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /out/example .FROM scratch AS bin-unixCOPY –from=build /out/example / …

Notice the # syntax at the top of the Dockerfile that selects the experimental Dockerfile frontend and the –mount option attached to the run command. This mount option means that each time the go build command is run, the container will have the cache mounted to Go’s compiler cache folder.

Benchmarking this change for the example binary on a 2017 MacBook Pro 13”, I see that a small code change takes 11 seconds to build without the cache and less than 2 seconds with it. This is a huge improvement!

Adding unit tests

All projects need tests! We’ll add a simple test for our echo function in a main_test.go file:

package mainimport (     “testing”    “github.com/stretchr/testify/require” )func TestEcho(t *testing.T) {    // Test happy path    err := echo([]string{“bin-name”, “hello”, “world!”})    require.NoError(t, err) }func TestEchoErrorNoArgs(t *testing.T) {    // Test empty arguments    err := echo([]string{})     require.Error(t, err) }

This test ensures that we get an error if the echo function is passed an empty list of arguments.

We will now want another build target for our Dockerfile so that we can run the tests and build the binary separately. This will require a refactor into a base stage and then unit-test and build stages:

# syntax = docker/dockerfile:1-experimentalFROM –platform=${BUILDPLATFORM} golang:1.14.3-alpine AS base WORKDIR /src ENV CGO_ENABLED=0 COPY go.* . RUN go mod download COPY . .FROM base AS build ARG TARGETOS ARG TARGETARCH RUN –mount=type=cache,target=/root/.cache/go-build GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /out/example .FROM base AS unit-test RUN –mount=type=cache,target=/root/.cache/go-build go test -v .FROM scratch AS bin-unix COPY –from=build /out/example / …

Note that Go test uses the same cache as the build so we mount the cache for this stage too. This allows Go to only run tests if there have been code changes which makes the tests run quicker.

We can also update our Makefile to add a test target:

all: bin/example test: lint unit-testPLATFORM=local.PHONY: bin/example bin/example:    @docker build . –target bin     –output bin/     –platform ${PLATFORM}.PHONY: unit-test unit-test:    @docker build . –target unit-test

What’s next?

In this post we have seen how to add Go dependencies efficiently, caching to make the build faster and unit tests to our containerized Go development environment. In the next and final post of the series, we are going to complete our journey and learn how to add a linter, set up a GitHub Actions CI, and some extra build optimizations.

You can find the finalized source this example on my GitHub: https://github.com/chris-crone/containerized-go-dev

You can read more about the experimental Dockerfile syntax here: https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/experimental.md

If you’re interested in build at Docker, take a look at the Buildx repository: https://github.com/docker/buildx

Read the whole blog post series here.
The post Containerize Your Go Developer Environment – Part 2 appeared first on Docker Blog.
Quelle: https://blog.docker.com/feed/

Published by