Building and Packaging 101 in Go

Shashank KS
Towards Dev
Published in
3 min readDec 19, 2021

--

Source: Google images

Packaging is probably as important as writing the code itself. If you get as confused as me looking at a Go project with a Makefile that has a ton of Latin commands inside it and a DockerFile using that Makefile, this might help.

Building Go files

If you’re new to Go, you might be running your Go application using go run . . Although this is not the recommended way to run it in production or for that matter anywhere other than your local. You should build your application using go build which creates executables that you can run later on. Typically a Makefile is used to do all these. ‘make’ is a program that runs a Makefile. ‘make’ handles builds efficiently. Using ‘make’, you can ensure that only the files that have been modified since the last build and those which are dependent on the changed files are compiled. ‘make’ uses the timestamp of the file to check if the file has been modified and whether it has to be recompiled or not.

The makefile of my project looked like this (explanations are followed by #):

When we run make or make all on the CLI, two executables — app1exec and app2exec will be created in the current working directory.

PWD := $(shell pwd) #variable declarations
GOPATH := $(shell go env GOPATH)
source := $(wildcard *.go **/*.go */*/*.go)
IMAGES:=imageapp1 imageapp2
OTHER_FLAGS:=-tags musl
all: app1 app2 #these targets are run if you run `make` or `make #all` from the CLIdeps: #dependencies I'm downloading gobin and a linter here
@GO111MODULE=off go get -u github.com/myitcv/gobin
@$(GOPATH)/bin/gobin github.com/golangci/golangci-lint/cmd/golangci-lint@v1.24.0
test: #all tests are run if I run `make run` on the CLI
go test ./...
build: #this is more like a check whether everything can be built #because go build ./... discards the results after running
go build ./...
run: #to run locally
go run github.com/shashankks0987/app1 & \
go run github.com/shashankks0987/app2
app1: $(source) #the $(source) tells Makefile the dependent #project directories. If no file has changed in these, it will not
#run this job
@echo "Building app1 to $(PWD)/app1..."
@CGO_ENABLED=1 GO111MODULE=on go build $(OTHER_FLAGS) -o app1exec github.com/shashankks0987/app1
app2: $(source)
@echo "Building app2 to $(PWD)/app2..."
@CGO_ENABLED=1 GO111MODULE=on go build $(OTHER_FLAGS) -o app2exec github.com/shashankks0987/app2
docker: #this is what you'll use to dockerize your code
@echo "Making docker image"
for image in $(IMAGES); do\
echo $$image;\
docker build . --target $$image -t $$image;\
done

CGO_ENABLED=1 leads to faster, smaller builds & runtimes - as it can dynamically load the host OS's native libraries (e.g. glibc, DNS resolver, etc.) - when running on the build OS. This is ideal for local rapid development.
This is a good resource to decide whether you should enable it or not.

There are various ways to ship your code — binaries, executables, docker images etc.., . Using docker images might be the most versatile of them all as docker makes sure your code runs the same way irrespective of the OS and other such system specifics.

Although you can draw some ideas from this DockerFile, you should know what works best for your project:

FROM golang:1.17-alpine AS build_base
RUN apk add make git gcc musl-dev
# Set the Current Working Directory inside the container
WORKDIR /tmp/app
COPY go.mod ./go.mod
RUN go mod download
COPY . .
RUN make deps
RUN make
FROM alpine:3.9 as app1
LABEL name=app1
RUN apk add ca-certificates
COPY --from=build_base /tmp/app/app1exec /app/app1
RUN adduser -D -H -u 1801 user1
USER 1801
CMD ["/app/app1"]
FROM alpine:3.9 as app2
LABEL name=app2
RUN apk add ca-certificates
COPY --from=build_base /tmp/app/app2exec /app/app2
RUN adduser -D -H -u 1801 user1
USER 1801
CMD ["/app/app2"]

What is happening in the above DockerFile:

1) We build a base container that copies source code, downloads dependencies, and uses the makefile to create the required executables.

2) The other two images that follow just copy the executable from the base image.

3) app1 and app2 are now dockerized and ready to run!

I hope this read gives you a fair idea of how to build and package files in Go and also not panic (like I did) the next time you see these on a codebase.

--

--