Go Project Setup
Field Manual Book

Setting up a Go Project

So, go mod init [module-name] - what's so hard about that? Why do you need to write an entire article about building a Go project? In the most technical sense, that is in fact how you start a Go project, and if you are building demo or throwaway code, that could be enough. But if you intend this to be a long-lived project that you will eventually deploy somewhere, you ought to establish more from the get-go.

For one, how are you going to build your project? No, hitting the play button in your IDE doesn't count. You may not be ready to go the full Github Actions or have a complete CI/CD pipeline, but you should be able to build the project from the terminal.

Then, how are you going to deploy it? If you plan to containerize it in some way, perhaps through Docker or Kubernetes, why don't you set that up from the start?

What about configuration? Are you just using a bunch of constants in your code - or worse - magic values throughout your code that you will eventually change in the future?

By answering these questions early in the project lifetime, you will head off a myriad of problems that you will encounter when you try to deploy your project. Nothing is worse than when you are finally ready to deploy your project and you realize there are an overwhelming number of things you didn't think of ahead of time, and you are in crunch time trying to tie up all of the loose ends - and there are a lot of them that need to be tied.

This article represents my opinion on the minimum you should have in place when you first start a project.

Creating a Repository

Before you do anything else, create a repository for your project. Yes, go to now - get over to GitHub and create your repository before you write a single line of code. Not using GitHub? Fine, then get over to wherever you are keeping your code and create that repository. At the very least, create a new directory and git init inside it - but wouldn't it be nice to ensure that if your hard drive fails, your code is safe somewhere else?

Github Repository

Part 1 source code can be found in a branch on this Github Repository

Name and Description

In GitHub, when creating a new repository, be sure to pick a reasonable name since this will also be the name of your Go module. Also, add a good description for your project, so that other people will know the point of your repository, and by other people, I mean you in 6 months.

Visibility

Be sure to set the visibility of your project. I often set it to private even if I intend to make it public in the future. That way, I can control when it becomes visible to the world. But, you do you.

Readme

Yes, let GitHub create the README.md file for you. It is helpful to have this in place since anyone browsing your project will see this. Later, we will include helpful information in this file; the default is fine for now.

Include the .gitignore

Choose Go for the Add .gitignore option, as it will give you a starting point. We can add to it later, but this at least helps you avoid accidentally committing a bunch of files you don't need.

Do the deed

Finally, click the Create Repository button, and you have a fresh new repository where your latest project can live.

In your terminal (yes, learn to use it, no cheating), you can use git clone to clone your repository:

Assumptions

This article assumes you are using GitHub and that you have set up SSH keys for your account. Also, it assumes you are using a Unix-like terminal. If you are using Windows, you can use WSL2 or Git Bash to get a similar experience.

git clone git@github.com:ymiseddy/go-getting-started.git 

I hope I don't need to say it, but put the URL of your repository in there, not mine.

Initialize your Go project

Now you want to initialize your project using the URL where the repository lives. If you didn't skip the last section, this is easy - see?

go mod init github.com/ymiseddy/go-getting-started.git

Then change into that directory:

cd go-getting-started

Create a basic Hello World

Yes, use this old trope at first. We want to make sure we have everything in place for this application - of course, we will expand on it in the future.

Since, in Go, it is the convention to put your commands under a cmd folder, let's create a folder for our app:

mkdir -p cmd/app

Then, inside that new directory, create a main.go file:

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

Now save your work and test it:

go run cmd/app/main.go

If all went well, you should see a familiar message:

Hello, World!

Now is a good time to commit your work:

git add .
git commit -m "task: initialize the project and add the main.go file."

Create a Makefile

The default build system for Go projects is the old standby Makefile. So, let's create a basic Makefile to build our project directory:

Makefile Tabs

Makefiles are very picky about using tabs instead of spaces. Be sure to use tabs when indenting commands in your Makefile.


.PHONY: build
build:
	go build -o build/app cmd/app/*.go

Now, let's make sure it works:

make
First Rule in Makefile

Make uses the first rule in the file to run if there are no arguments on the command line. So, in this case, make will run the build rule. You can specify other rules by typing make [rule-name].

If you get any errors from running Make, be sure to fix them.

Make a Docker image

Now that we have a basic build system in place, we want to consider how we will eventually deploy this application. It is common to containerize your application using Docker. If you will eventually deploy to Kubernetes, you will need a Docker image anyway. Even if you are deploying to a virtual host, using Docker can help you avoid dependency hell on the host.

For deploying Go applications, I recommend using either an Alpine base image or a Debian Slim base image. Both are small and have a small attack surface. Alpine is smaller, but you may run into some issues with C libraries. Debian Slim is a bit larger, but you are less likely to run into problems. For this example, we will use Alpine.

Here is the Dockerfile:

FROM alpine:latest

COPY build/ main/

WORKDIR /main
CMD ["./app"]

Also, we will add a docker-bakefile.hcl so we can use docker buildx bake to build our image:

variable "IMAGE_TAG" {
  type = string
  default = "0.0.0"
}

target "default" {
	context = "."
	dockerFile = "Dockerfile"
	tags = ["seddy.com/go-getting-started:${IMAGE_TAG}"]
}
IMAGE_TAG

We are defining an IMAGE_TAG variable to set the tag for our image. We will set the default to 0.0.1, but when we use our Makefile to build the image, we will override this value based on a git tag.

We could use docker buildx bake to make our image, but wouldn't it be more fun and better in the long run to add it to our Makefile?

First, we need to get the latest git tag. We can do this using git describe --tags --abbrev=0 - add this to the beginning of your Makefile:

export TAG := $(shell git describe --tags --always)

Add the following rule after the build rule in your Makefile:

.phony: docker-build
docker-build: build
	IMAGE_TAG=$(TAG) docker buildx bake
Makefile Dependencies

Note that the docker-build rule depends on the build rule. Doing this ensures that the make builds the project before building the Docker image.

Before we commit our work, let's remove the build directory if it exists:

rm -rf build/

Later, we will add a clean rule to our Makefile to remove this directory.

Now, let's commit our work so far:

git add .
git commit -m "chore: add Makefile and Dockerfile to build the project."

Since we are using a git tag to set the image tag, let's create a tag:

git tag v0.0.1 

Now, let's try it out:

make docker-build

If all goes well, you should see Docker building your image. You can verify it was built by running:

docker images

You should see your new image in the list:

REPOSITORY                     TAG                 IMAGE ID       CREATED          SIZE
seddy.com/go-getting-started   v0.0.1              11ef917d0fbb   14 minutes ago   10.5MB

We can even run our image to make sure it works:

docker run --rm seddy.com/go-getting-started:v0.0.1

Export the Docker image

Often at this point, you will want to push your image to a registry. However, if you prefer to deploy it to a server or another machine manually, we can export the image to a tar file that you can copy and deploy there.

Add the following rule to your Makefile:

.phony: image
image: docker-build
	mkdir -p image/
	docker save seddy.com/go-getting-started:$(TAG) -o image/go-getting-started-$(TAG).tar

Now, try it out:

make image

If everything works, you will now have an image directory with a tar file inside it:

❯ ls image/
go-getting-started-v0.0.1.tar

You can copy this file to another machine and load it there using:

docker load -i go-getting-started-v0.0.1.tar

Cleaning up after ourselves

Finally, it is a good idea to have a clean rule in your Makefile to clean up any build artifacts:

.PHONY: clean
clean:
	rm -rf build/
	rm -rf image/

This rule will remove the build and image directories when you run make clean.

Makefile .PHONY

Be sure to include .PHONY for each rule that is not a file. Including this prevents Make from getting confused if a file with the same name as the rule exists.

Ignoring build artifacts

It is considered bad form to commit build artifacts to your repository. So, be sure to add the following lines to your .gitignore file:

# Build artifacts
/build/
/image/

Update your README.md

Finally, it is a good idea to update your README.md file to include instructions on how to build and run your project.

Conclusion

It may seem like a lot of work, but taking the time to set up your project correctly from the start will save you a lot of headaches in the long run. You will be thankful you did this when you are ready to deploy your application, having a solid foundation to build upon.

In the next part of this series, we will look at adding configuration to our application so that we can avoid magic values in our code.