11. Tooling

We become what we behold. We shape our tools, and thereafter our tools shape us. — Marshall McLuhan

Good support for lots of useful tools is another strength of Go. Apart from the built-in tools, there any many other community-built tools. This chapter covers the built-in Go tools and few other external tools.

The built-in Go tools can access through the go command. When you install the Go compiler (gc); the go tool is available in the path. The go tool has many commands. You can use the go tool to compile Go programs, run test cases, and format source files among other things.

11.1. Getting Help

The go tool is self-documented. You can get help about any commands easily. To see the list of all commands, you can run the "help" command. For example, to see help for build command, you can run like this:

go help build

The help command also provides help for specific topics like “buildmode”, “cache”, “filetype”, and “environment” among other topics. To see help for a specific topic, you can run the command like this:

go help environment

11.2. Basic Information

11.2.1. Version

When reporting bugs, it is essential to specify the Go version number and environment details. The Go tool gives access to this information through the following commands.

To get version information, run this command:

go version

The output should look something like this:

go version go1.20.4 linux/amd64

As you can see, it shows the version number followed by operating system and CPU architecture.

11.2.2. Environment

To get environment variables, you can run this command:

go env

The output should display all the environment variables used by the Go tool when running different commands.

A typical output will look like this:

GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/baiju/.cache/go-build"
GOENV="/home/baiju/.config/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/baiju/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/baiju/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="go1.20.4"
GCCGO="gccgo"
GOAMD64="v1"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/dev/null"
GOWORK=""
CGO_CFLAGS="-O2 -g"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-O2 -g"
CGO_FFLAGS="-O2 -g"
CGO_LDFLAGS="-O2 -g"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0
  -fdebug-prefix-map=/tmp/go-build1378738152=/tmp/go-build
  -gno-record-gcc-switches"

You can temporarily set environment variable with -w option. This is an example to set GOPATH in Windows system.

C:\> go env -w GOPATH=C:\mygo

Note: To set an environment variable permenantly in Windows, see the first chapter which explains about Windows installation.

11.2.3. List

The list command provides meta information about packages. Running without any arguments shows the current packages import path. The -f helps to extract more information, and it can specify a format. The text/template package syntax can be used to specify the format.

The struct used to format has many attributes, here is a subset:

  • Dir – directory containing package sources

  • ImportPath – import path of package in dir

  • ImportComment – path in import comment on package statement

  • Name – package name

  • Doc – package documentation string

  • Target – install path

  • GoFiles – list of .go source files

Here is an example usage:

$ go list -f '{{.GoFiles}}' text/template
[doc.go exec.go funcs.go helper.go option.go template.go]

11.3. Building and Running

To compile a program, you can use the build command. To compile a package, first change to the directory where the program is located and run the build command:

go build

You can also compile Go programs without changing the directory. To do that, you are required to specify the package location in the command line. For example, to compile github.com/baijum/introduction package run the command given below:

go build github.com/baijum/introduction

If you want to set the executable binary file name, use the -o option:

go build -o myprog

If you want to build and run at once, you can use the "run" command:

go run program.go

You can specify more that one Go source file in the command line arguments:

go run file1.go file2.go file3.go

Of course, when you specify more than one file names, only one “main” function should be defined among all of the files.

11.3.1. Conditional Compilation

Sometimes you need to write code specific to a particular operating system. In some other case the code for a particular CPU architecture. It could be code optimized for that particular combination. The Go build tool supports conditional compilation using build constraints. The Build constraint is also known as build tag. There is another approach for conditional compilation using a naming convention for files names. This section is going to discuss both these approaches.

The build tag should be given as comments at the top of the source code. The build tag comment should start like this:

// +build

The comment should be before package documentation and there should be a line in between.

The space is OR and comma is AND. The exclamation character is stands for negation.

Here is an example:

// +build linux,386

In the above example, the file will compile on 32-bit x86 Linux system.

// +build linux darwin

The above one compiles on Linux or Darwin (Mac OS).

// +build !linux

The above runs on anything that is not Linux.

The other approach uses file naming convention for conditional compilation. The files are ignore if it doesn’t match the target OS and CPU architecture, if any.

This compiles only on Linux:

stat_linux.go

This one on 64 bit ARM linux:

os_linux_arm64.go

11.4. Running Test

The Go tool has a built-in test runner. To run tests for the current package, run this command:

go test

To demonstrate the remaining commands, consider packages organized like this:

.
|-- hello.go
|-- hello_test.go
|-- sub1
|   |-- sub1.go
|   `-- sub1_test.go
`-- sub2
    |-- sub2.go
    `-- sub2_test.go

If you run go test from the top-level directory, it’s going to run tests in that directory, and not any sub directories. You can specify directories as command line arguments to go test command to run tests under multiple packages simultaneously. In the above listed case, you can run all tests like this:

go test . ./sub1 ./sub2

Instead of listing each packages separates, you can use the ellipsis syntax:

go test ./...

The above command run tests under current directory and its child directories.

By default go test shows very few details about the tests.

$ go test ./...
ok      _/home/baiju/code/mypkg   0.001s
ok      _/home/baiju/code/mypkg/sub1      0.001s
--- FAIL: TestSub (0.00s)
FAIL
FAIL    _/home/baiju/code/mypkg/sub2      0.003s

In the above results, it shows the name of failed test. But details about other passing tests are not available. If you want to see verbose results, use the -v option.

$ go test ./... -v
=== RUN   TestHello
--- PASS: TestHello (0.00s)
PASS
ok      _/home/baiju/code/mypkg   0.001s
=== RUN   TestSub
--- PASS: TestSub (0.00s)
PASS
ok      _/home/baiju/code/mypkg/sub1      0.001s
=== RUN   TestSub
--- FAIL: TestSub (0.00s)
FAIL
FAIL    _/home/baiju/code/mypkg/sub2      0.002s

If you need to filter tests based on the name, you can use the -run option.

$ go test ./... -v -run Sub
testing: warning: no tests to run
PASS
ok      _/home/baiju/code/mypkg   0.001s [no tests to run]
=== RUN   TestSub
--- PASS: TestSub (0.00s)
PASS
ok      _/home/baiju/code/mypkg/sub1      0.001s
=== RUN   TestSub
--- FAIL: TestSub (0.00s)
FAIL
FAIL    _/home/baiju/code/mypkg/sub2      0.002s

As you can see above, the TestHello test was skipped as it doesn’t match “Sub” pattern.

The chapter on testing has more details about writing test cases.

golangci-lint is a handy program to run various lint tools and normalize their output. This program is useful to run through continuous integration. You can download the program from here: https://github.com/golangci/golangci-lint
The supported lint tools include Vet, Golint, Varcheck, Errcheck, Deadcode, Gocyclo among others. golangci-lint allows to enable/disable the lint tools through a configuration file.

11.5. Formatting Code

Go has a recommended source code formatting. To format any Go source file to conform to that format, it’s just a matter of running one command. Normally you can integrate this command with your text editor or IDE. But if you really want to invoke this program from command line, this is how you do it:

go fmt myprogram.go

In the above command, the source file name is explicitly specified. You can also give package name:

go fmt github.com/baijum/introduction

The command will format source files and write it back to the same file. Also it will list the files that is formatted.

11.6. Generating Code

If you have use case to generate Go code from a grammar, you may consider the go generate. In fact, you can add any command to be run before compiling the code. You can add a special comment in your Go code with this syntax:

//go:generate command arguments

For example, if you want to use peg (https://github.com/pointlander/peg), a Parsing Expression Grammar implementation, you can add the command like this:

//go:generate peg -output=parser.peg.go grammar.peg

When you build the program, the parser will be generated and will be part of the code that compiles.

11.7. Embedding Code

Go programs are normally distributed as a single binary. What if your program need some files to run. Go has a feature to embed files in the binary. You can embed any type of files, including text files and binary files. Some of the commonly embedded files are SQL, HTML, CSS, JavaScript, and images. You can embed individual files or diectories including nested sub-directories.

You need to import the embed package and use the //go:embed compiler directive to embed. Here is an example to embed an SQL file:

import _ "embed"

//go:embed database-schema.sql
var dbSchema string

As you can see, the “embed” package is imported with a blank identifier as it is not directly used in the code. This is required to initialize the package to embed files. The variable must be at package level and not at function or method level.

The variable could be slice of bytes. This is useful for binary files. Here is an example:

import _ "embed"

//go:embed logo.jpg
var logo []byte

If you need an entire directory, you can use the embed.FS as the type:

import "embed"

//go:embed static
var content embed.FS

11.8. Displaying Documentation

Go has good support for writing documentation along with source code. You can write documentation for packages, functions and custom defined types. The Go tool can be used to display those documentation.

To see the documentation for the current packages, run this command:

go doc

To see documentation for a specific package:

go doc strings

The above command shows documentation for the “strings” package.

go doc strings

If you want to see documentation for a particular function within that package:

go doc strings.ToLower

or a type:

go doc strings.Reader

Or a method:

go doc strings.Reader.Size

11.9. Find Suspicious Code Using Vet

There is a handy tool named vet to find suspicious code in your program. Your program might compile and run. But some of the results may not be desired output.

Consider this program:

 1package main
 2
 3import (
 4    "fmt"
 5)
 6
 7func main() {
 8    v := 1
 9    fmt.Printf("%#v %s\n", v)
10}

If you compile and run it. It’s going to be give some output. But if you observe the code, there is an unnecessary %s format string.

If you run vet command, you can see the issue:

$ go vet susp.go
# command-line-arguments
./susp.go:9: Printf format %s reads arg #2,
but call has only 1 arg

Note: The vet command is automatically run along with the test command.

11.10. Exercises

Exercise 1: Create a program with function to return “Hello, world!” and write test and run it.

hello.go:

package hello

// SayHello returns a "Hello word!" message
func SayHello() string {
     return "Hello, world!"
}

hello_test.go:

package hello

import "testing"

func TestSayHello(t *testing.T) {
     out := SayHello()
     if out != "Hello, world!" {
        t.Error("Incorrect message", out)
     }
}

To run the test:

go test . -v

11.10.1. Additional Exercises

Answers to these additional exercises are given in the Appendix A.

Problem 1: Write a program with exported type and methods with documentation strings. Then print the documentation using the go doc command.

11.11. Summary

This chapter introduced the Go tool. It explained all the major Go commands in detail and provided practical examples for each command. It covered how to build and run programs, run tests, format code, and display documentation. It also mentioned a few other useful tools.