Project setup

Setting up a Go project.

Requirements analysis

Functional requirements

Functional requirements is a list of the core functionalities that the system is expected to implement and how the actors (users, other system components or services) interact with it. To establish the functional requirements, you must write user stories.

User stories describe business value and include a list of acceptance criteria that acts as a verification tool that each goal is met.

User story template

As an actor, I need to be able to short requirement, so as to reason/business value.

The acceptance criteria for this user story are as follows:

  • acceptance criteria 1
  • acceptance criteria 2

Non-functional requirements

Non-functional requirements include items like service-level objectives (SLOs) and capacity and scalability requirements.

To

Makefile

Create a Makefile at the base of your project to manage project tasks, builds, and dependencies. A Makefile consists of targets, which are tasks that you can run by entering make target-name.

The following sections describe common Makefile idioms and targets for a basic Go project.

Global variables

Add global variables at the top of the Makefile in all caps. For example, the following variable defines the Go version:

GO_VERSION := 1.19.4

To use this variable in the Makefile, enclose it in parentheses and prepend it with a $:

$(GO_VERSION)

Build

Build the application for your host machine:

build:
    go build -o appname path/to/main.go

Build binaries for multiple architectures, and store them in a bin/ directory at the project root:

build:
	# Linux
	GOOS=linux   GOARCH=amd64 go build -o ./bin/appname_linux_amd64   .path/to/main.go
	# macOS
	GOOS=darwin  GOARCH=amd64 go build -o ./bin/appname_darwin_amd64  .path/to/main.go
	# windows
	GOOS=windows GOARCH=amd64 go build -o ./bin/appname_win_amd64.exe .path/to/main.go

Cross-compilation

You need to know the GOOS and GOOARCH values to compile the correct binaries.

Next, you can cross compile for multiple operating systems with a Makefile. Create a make target that compiles multiple binaries and places them in the /bin directory:

compile:
	# Linux
	GOOS=linux GOARCH=amd64 go build -o ./bin/hit_linux_amd64 ./cmd/hit
	# macOS
	GOOS=darwin GOARCH=amd64 go build -o ./bin/hit_darwin_amd64 ./cmd/hit
	# windows
	GOOS=windows GOARCH=amd64 go build -o ./bin/hit_win_amd64.exe ./cmd/hit

Make sure that you add the /bin directory to the .gitignore file.

Test

Run go tests:

test:
    go test ./... -coverprofile=coverage.out

Code coverage

View how much of the source code is adequetly tested:

What does this do?

coverage:
	go tool cover -func coverage.out | grep "total:" | \
	awk '{print ((int($$3) > 80) != 1) }'

Generate coverage report

Runs the Go coverage tool to generate an HTML page that describes what code is covered by tests:

report:
	go tool cover -html=coverage.out -o cover.html

Format your source code

This is not an issue in VSCode, but you can add a target that verifies the Go source code is formatted correctly:

check-format:
	test -z $$(go fmt ./...)

Static linters

golangci-lint is a Go linters aggregator–it provides multiple linters for Go source code.

Install

install-lint: 
	sudo curl -sSfL \
	https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh \
 	| sh -s -- -b $$(go env GOPATH)/bin v1.51.1

Run the linter

static-check:
	golangci-lint run ./...

Github Actions

Modules

A Go code repository comprises of exactly one module. The module includes packages, and these packages include source files. To create a module, go to the top-level directory of the project and enter the following command:

go mod init <project-name>

The preceding command creates a go.mod file in the top-level of your project that lists your project name at the top.

Module names should be unique within the Go community. This prevents conflicts with other public libraries. A common pattern is to use a URL that you own, such a project-name.example.com. Another very common pattern is to use the path where the project exists, minus the scheme. For example, github.com/username/projectname.

Packages

Packages are directories in a Go project. A package name should describe what it provides, not what it does. The name of the directory is the package name. For example, source files in the go-src/stocks/ package are in the stocks package. At the top of the file, declare package names with package <package-name>, and import packages with the import <package-name> statement.

import statements use the fully-qualified package name. This begins with the module name containing the package. For example, import go-src/<package-name>

Prepend any imported package code with the package name, or an alias for the package: alias package/name. For example, s go-src/stocks allows you to prepend any code with s., such as s.Investment.

main: any program that has to run as an application must be in the main package.

When you write external tests, use a _test suffix. For example, a package that contains external tests for the url package is url_test.

Import external packages

When you import an external package, you list the module name in the go.mod file, followed by the path to the specific library from the project root. For example, if the go.mod file contains the following:

module url
...

Then you import the module as follows:

import "url/path/to/library"

Commonly, packages are publically available in repositories, and the module name is the path to the root of the repository:

module github.com/rjs/url-parser
...

In this case, the import statement for the parser package within this repo is as follows:

import "github.com/rjs/url-parser/parser"

CLI tools

.
├── cmd 
│   └── todo
│       ├── main.go         # config, parse, switch {} flags
│       └── main_test.go    # integration tests (user interaction)
├── go.mod
├── todo.go                 # API logic for flags
└── todo_test.go            # unit tests

/internal directory is special because other projects cannot import anything in this directory. This is a good place for domain code. If an entity needs to be accessible to all domain code, place its file in the /internal directory. Its package name is the project package. Each subdirectory in /internal is a domain.

You create a tool that the user interacts with and is responsible for the following:

  • Parses the flags
  • Validates flags
  • Calls the business logic library

Go uses the cmd directory for executables (the entry point) such as CLI tools. Within each cmd/subdirectory, you can name the entry point main.go or the name of the package, such as hit.go. Regardless of the file name, it must be in the main package because that package is what makes a file executable.

Next, you have to create the tool library that contains the business logic. This is a standalone package, so use the name of the library that you are building.

The following is a simple directory structure for the hit tool:

hit-tool
├── cmd         # Executable directory
│   └── hit     # CLI tool directory
├── go.mod
└── hit         # Library directory

Web apps

Web app structure separates the Go code and the web assets to simplify building and deploying.

.
├── cmd
│   └── web
│       ├── handlers.go
│       └── main.go
├── go.mod
├── internal
├── README.md
└── ui
    ├── html
    └── static
/cmd
Application-specific code for executables.
/internal
Non-application-specific code, including reusable code like validation helpers and SQL database models. Code in the /internal directory cannot be imported by external projects.
/ui
User-interface assets for the web app, including templates and static files (CSS, Javascript).

Configuration with CLI flags

Add flags to manage environment configurations. CLI flags are easier to manage, have default values, and have built-in help.

If you plan to use environment variables, you can pass the env vars to the flag:

$ export ENV_VAR="value"
$ go run ./example -addr=$ENV_VAR

This prevents you from relying on env vars in your code and needing to convert the values from string to the flag type. The flag package handles those conversions themselves.

Dependency injection

Definition here

In web applications, handlers need access to multiple dependencies. The easiest way to do that is to inject the dependencies with structs:

type application struct {
	logger: *log.logger
	db:     *sql.DB
	cache:  *cache
}

Then, you can initialize your app in the main method, and inject any dependencies in the new application object at runtime:

func main() {
	// instantiate myLogger
	// mySQLHandle db config
	// instantiate myCache

	app := &application {
		logger: myLogger,
		db:     mySQLHandle,
		cache:  myCache,
	}
}

Web app checklist

Plan your routes:

MethodPatternHandlerAction