An overview of Go tooling

Tooling is generally considered one of the stronger aspects of the Go ecosystem. The go command is the gateway to many of the tools that will be discussed in this post.

By learning about each of the tools discussed here, you'll become much more efficient when working on Go projects and perform common tasks quickly and reliably.

Viewing environmental variables

The go env command is used to display information about the current Go environment. Here's a sample of what this command outputs:

GO111MODULE="on"
GOARCH="amd64"
GOBIN="/home/ayo/go/bin"
GOCACHE="/home/ayo/.cache/go-build"
GOENV="/home/ayo/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/ayo/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/lib/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/lib/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/dev/null"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build145204762=/tmp/go-build -gno-record-gcc-switches"

If you want to view the value of a specific variable, you can pass them as arguments to the go env command:

$ go env GOPATH GOROOT GOBIN
/home/ayo/go
/usr/lib/go
/home/ayo/go/bin

The documentation for each of the variables can be accessed using the command below:

go help environmental

Running code with go run

Assuming you have a main.go file with the following code,

package main
import "fmt"
func main() { fmt.Println("Welcome to Go!") }

You can run it using the go run command, as we've already seen several times in this series:

$ go run main.go
Welcome to Go!

The go run command compiles the program, creates an executable in your /tmp directory, and executes this binary in one step. If you want to execute several files at once, you can pass them all as arguments to go run:

$ go run main.go time.go input.go

Or you can use a wildcard:

$ go run *.go

You can also run an entire package at once, as of Go v1.11:

$ go run ./foo # Run the package in the `foo` directory
$ go run .     # Run the package in the current directory

Formatting code with gofmt

If you've been writing Go code for any length of time, you will know that there are strict conventions for how code should be formatted. The gofmt command is what enforces these conventions for all Go code in existence.

The code snippet shown in the previous section was not formatted properly, so let's format it with gofmt, as demonstrated below:

$ gofmt main.go
package main

import "fmt"

func main() { fmt.Println("Welcome to Go!") }

This formats the code in the source file and prints the result to the standard output. If you want to overwrite the source file with the formatted output, you need to add the -w flag.

$ gofmt -w main.go

To format Go source files recursively (current directory and subdirectories), specify a . as the argument to gofmt.

gofmt .

Fixing import statements

Before you can use a package in your code, you need to import it. If you fail to do so, the code will not compile, and an error will be displayed. Given the following code in your main.go file,

package main

func main() {
    fmt.Println("Welcome to Go!")
}

You should see the following error if you attempt to execute the program:

$ go run main.go
# command-line-arguments
./main.go:4:2: undefined: fmt

Before the code can compile, the fmt package must be imported. You can add the necessary code manually or use the goimports command, which adds the necessary import statements for you.

$ goimports main.go
package main

import "fmt"

func main() {
    fmt.Println("Welcome to Go!")
}

The command also removes any imported packages that are no longer referenced and formats the code in the same style as gofmt. So, you can also think of goimports as a replacement for gofmt.

The value of goimports becomes apparent if you set your editor to run it on saving a Go source file. That way, you won't need to worry about importing a package before using it or importing statements that are no longer needed. It'll be done automatically for you as soon as you save the file. Most code editors have some sort of plugin or setting that should help with this.

Building your project

To build an executable binary for your program, use the go build command. This will output a single binary in the current directory:

$ go build
$ ./demo
Welcome to Go!

The binary produced with go build is specific to your operating system architecture, and it contains everything you need to run the program. Therefore, you can transfer it to another machine with the same architecture, and it'll run in the same manner even if Go is not installed.

If you want to cross-compile a binary for an architecture other than your own, all you need to do is change the values of the GOOS and GOARCH environmental variables before running the go build command.

For example, the following command can be used to produce a binary for a 64-bit Windows machine:

$ GOOS=windows GOARCH=amd64 go build

To compile for Linux, macOS, ARM, Web Assembly, or other targets, please refer to the Go docs to see the combinations of GOOS and GOARCH that are available to you.

Installing Go binaries

The go install command is an alternative to go build if you want to be able to run the program from outside its source directory.

Assuming your main.go file is in a directory called demo, the following command will create a demo binary in your $GOPATH/bin directory.

$ go install

The $GOPATH should be $HOME/go on most computers. You can check it with the go env command:

$ go env GOPATH
/home/ayo/go

If you list the contents of $GOPATH/bin, you should see a demo binary:

$ ls $GOPATH/bin
demo

This binary can be executed by running the demo command from any location on your filesystem. This only works as long as the $GOPATH/bin directory has been added to your $PATH.

$ demo
Welcome to Go!

Listing package information

The default invocation of go list returns the name of the import path for the directory you are currently in or the provided package path:

$ go list
github.com/ayoisaiah/demo
$ go list github.com/joho/godotenv
github.com/joho/godotenv

We can customize the output of the go list command using the -f flag, which allows you to execute a Go template that has access to the internal data structures of the go tool. For example, you can list the name of the fmt using the command below:

$ go list -f "{{ .Name }}" fmt
fmt

That's not very interesting on its own, but there's more. You can print all the dependencies of a package using the {{ .Imports }} template. Here's the output for the fmt package:

$ go list -f "{{ .Imports }}" fmt
[errors internal/fmtsort io math os reflect strconv sync unicode/utf8]

Or, you can list the complete set of transitive dependencies for a package:

$ go list -f "{{ .Deps  }}" fmt
[errors internal/bytealg internal/cpu internal/fmtsort internal/oserror internal/poll internal/race internal/reflectlite internal/syscall/execenv internal/syscall/unix internal/testlog io math math/bits os reflect runtime runtime/internal/atomic runtime/internal/math runtime/internal/sys sort strconv sync sync/atomic syscall time unicode unicode/utf8 unsafe]

You can also use the go list command to check for updates to dependencies and subdependencies:

$ go list -m -u all

Or, check for updates to a specific dependency:

$ go list -m -u go.mongodb.org/mongo-driver
go.mongodb.org/mongo-driver v1.1.1 [v1.4.0]

There's a lot more you can do with the go list command. Make sure to check the documentation for flags and other template variables that may be used with the command.

Displaying documentation for a package or symbol

The go doc command prints the documentation comments associated with the item identified by its arguments. It accepts zero, one, or two arguments.

To display the package documentation for the package in the current directory, use the command without any arguments:

$ go doc

If the package is a command (the main package), documentation for exported symbols are omitted in the output, except when the -cmd flag is provided.

We can use the go doc command to view the docs for any package by passing the import path for the package as an argument to the command:

$ go doc encoding/json
package json // import "encoding/json"

Package json implements encoding and decoding of JSON as defined in RFC
7159. The mapping between JSON and Go values is described in the
documentation for the Marshal and Unmarshal functions.
[truncated for brevity]

If you want to view the documentation for a specific method in a package, simply pass it as a second argument to go doc:

$ go doc encoding/json Marshal

A second command, godoc, presents the documentation for all Go packages (including any third-party dependencies you have downloaded) as a webpage. Running the command will start a web server on port 6060 by default, but you can change the address with the -http flag.

$ godoc -http=:6060

Local Go package documentation for fmt in Chrome

Performing static analysis

The go vet command is one that helps with detecting suspicious constructs in your code that may not be caught by the compiler. These are things that may not necessarily prevent your code from compiling but will affect code quality, such as unreachable code, unnecessary assignments, and malformed format string arguments.

$ go vet main.go # Run go vet on the `main.go` file
$ go vet .       # Run go vet on all the files in the current directory
$ go vet ./...   # Run go vet recursively on all the files in the current directory

This command is composed of several analyzer tools, which are listed here, with each one performing a different check on the file.

All checks are performed by default when the go vet command is executed. If you only want to perform specific checks (ignoring all the others), include the name of the analyzer as a flag and set it to true. Here's an example that runs the printf check alone:

$ go vet -printf=true ./...

However, passing the -printf=false flag to go vet will run all checks except printf.

$ go vet -printf=false ./...

Adding dependencies to your project

Assuming you have Go modules enabled, go run, go build, or go install will download any external dependencies needed to fulfill the import statements in your program. By default, the latest tagged release will be downloaded or the latest commit if no tagged releases are available.

If you need to download a specific version of a dependency other than the one Go fetches by default, you can use the go get command. You can target a specific version or commit hash:

$ go get github.com/joho/godotenv@v1.2.0
$ go get github.com/joho/godotenv@d6ee687

This method may be used to upgrade or downgrade dependencies as needed. Any downloaded dependencies are stored in the module cache located at $GOPATH/pkg/mod. You can use the go clean command to clear the module cache for all projects in one go:

$ go clean -modcache

Working with Go modules

We covered Go modules in detail in part 2 of this series. Here's a summary of the commands you need to know to work with modules effectively:

  • go mod init will initialize modules in your project.
  • go mod tidy cleans up unused dependencies or adds missing ones. Make sure to run this command before committing to any changes to your code.
  • go mod download will download all modules to the local cache.
  • go mod vendor copies all third-party dependencies to a vendor folder in your project root.
  • go mod edit can be used to replace a dependency in your go.mod file with a local or forked version. For example, if you need to use a fork until a patch is merged upstream, use the following code:
go mod edit -replace=github.com/gizak/termui=github.com/ayoisaiah/termui

Testing and benchmarking your code

Go has a built-in testing command called go test and a testing package, which can be combined to provide a simple but complete unit testing experience. The test tool also includes benchmarking and code coverage options to help you profile your code even more.

Let's write a simple test to demonstrate some of the capabilities of the test tool. Modify the code in your main.go file, as shown below:

package main

import "fmt"

func welcome() string {
    return "Welcome!"
}

func main() {
    fmt.Println(welcome())
}

Then, add the test in a separate main_test.go file in the same directory:

package main

import "testing"

func TestWelcome(t *testing.T) {
    expected := "Welcome to Go!"
    str := welcome()
    if str != expected {
        t.Errorf("String was incorrect, got: %s, want: %s.", str, expected)
    }
}

If you run go test in the terminal, it should fail:

$ go test
--- FAIL: TestSum (0.00s)
    main_test.go:9: String was incorrect, got: Welcome!, want: Welcome to Go!.
FAIL
exit status 1
FAIL    github.com/ayoisaiah/demo   0.002s

We can make the test pass by modifying the return value of the welcome function in the main.go file.

func welcome() string {
    return "Welcome to Go!"
}

Now, the test should pass successfully:

$ go test
PASS
ok      github.com/ayoisaiah/demo   0.002s

If you have lots of test files with test functions but only want to selectively run a few of them, you can use the -run flag. It accepts a regular expression string to match the test functions that you want to run:

$ go test -run=^TestWelcome$ . # Run top-level tests matching "TestWelcome"
$ go test -run= String .       # Run top-level tests matching "String" such as "TestStringConcatenation"

You should also know the following test flags, which often come in handy when testing Go programs:

  • The -v flag enables the verbose mode so that the names of the tests are printed in the output.
  • The -short flag skips long-running tests.
  • The -failfast flag stops testing after the first failed test.
  • The -count flag runs a test multiple times in succession, which is useful if you want to check for intermittent failures.

Code coverage

To view your code coverage status, use the -cover flag, as shown below:

$ go test -cover
PASS
coverage: 50.0% of statements
ok      github.com/ayoisaiah/demo   0.002s

You can also generate a coverage profile using the -coverprofile flag. This allows you to study the code coverage results in more detail:

$ go test -coverprofile=coverage.out

You will find a coverage.out file in the current directory after running the above command. The information contained in this file can be used to output an HTML file containing the exact lines of code that have been covered by existing tests.

$ go tool cover -html=coverage.out

When this command is run, a browser window pops up showing the covered lines in green and uncovered lines in red.

Screenshot of code coverage information in Chrome

Benchmarking

The benchmarking tool in Go is widely accepted as a reliable way to measure the performance of Go code. Benchmarks are placed inside _test.go files, just like tests. Here's an example that compares the performance of different string concatenation methods in Go:

// main_test.go
package main

import (
    "bytes"
    "strings"
    "testing"
)

var s1 = "random"

const LIMIT = 1000

func BenchmarkConcatenationOperator(b *testing.B) {
    var q string
    for i := 0; i < b.N; i++ {
        for j := 0; j < LIMIT; j++ {
            q = q + s1
        }
        q = ""
    }
    b.ReportAllocs()
}

func BenchmarkStringBuilder(b *testing.B) {
    var q strings.Builder
    for i := 0; i < b.N; i++ {
        for j := 0; j < LIMIT; j++ {
            q.WriteString(s1)
        }
        q.Reset()
    }
    b.ReportAllocs()
}

func BenchmarkBytesBuffer(b *testing.B) {
    var q bytes.Buffer
    for i := 0; i < b.N; i++ {
        for j := 0; j < LIMIT; j++ {
            q.WriteString(s1)
        }
        q.Reset()
    }
    b.ReportAllocs()
}

func BenchmarkByteSlice(b *testing.B) {
    var q []byte
    for i := 0; i < b.N; i++ {
        for j := 0; j < LIMIT; j++ {
            q = append(q, s1...)
        }
        q = nil
    }
    b.ReportAllocs()
}

This benchmark can be invoked using the go test -bench=. command:

$ go test -bench=.
goos: linux
goarch: amd64
pkg: github.com/ayoisaiah/demo
BenchmarkConcatenationOperator-4        1718        655509 ns/op     3212609 B/op        999 allocs/op
BenchmarkStringBuilder-4              105122         11625 ns/op       21240 B/op         13 allocs/op
BenchmarkBytesBuffer-4                121896          9230 ns/op           0 B/op          0 allocs/op
BenchmarkByteSlice-4                  131947          9903 ns/op       21240 B/op         13 allocs/op
PASS
ok      github.com/ayoisaiah/demo   5.166s

As you can see, the concatenation operator was the slowest of the bunch at 655509 nanoseconds per operation, while bytes.Buffer was the fastest at 9230 nanoseconds per operation. Writing benchmarks in this manner is the best way to determine performance improvements or regressions in a reproducible way.

Detecting race conditions

A race detector is included with the go tool and can be activated with the -race option. This is useful for finding problems in concurrent systems, which may lead to crashes and memory corruption.

$ go test -race ./...
$ go run -race main.go
$ go build -race ./...
$ go install -race ./...

Wrapping up

In this article, we covered several tools included in each Go environment and provided short descriptions of how they can be used. This is only the tip of the iceberg, so be sure to check out the full documentation for a deeper explanation of the capabilities of each command.

Thanks for reading, and happy coding!

Get the Honeybadger newsletter

Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo
    Ayooluwa Isaiah

    Ayo is a developer with a keen interest in web tech, security and performance. He also enjoys sports, reading and photography.

    More articles by Ayooluwa Isaiah
    An advertisement for Honeybadger that reads 'Turn your logs into events.'

    "Splunk-like querying without having to sell my kidneys? nice"

    That’s a direct quote from someone who just saw Honeybadger Insights. It’s a bit like Papertrail or DataDog—but with just the good parts and a reasonable price tag.

    Best of all, Insights logging is available on our free tier as part of a comprehensive monitoring suite including error tracking, uptime monitoring, status pages, and more.

    Start logging for FREE
    Simple 5-minute setup — No credit card required