Not sure how to structure your Go web application?

My new book guides you through the start-to-finish build of a real world web application in Go — covering topics like how to structure your code, manage dependencies, create dynamic database-driven pages, and how to authenticate and authorize users securely.

Take a look!

How to use go run to manage tool dependencies

Published on:

When you're working on a project it's common for there to be some developer tooling dependencies. These aren't code dependencies, but rather tools that you run as part of the development, testing, build or deployment processes.

For example, you might use golang.org/x/text/cmd/gotext in conjunction with go:generate to generate message catalogs for translation, or honnef.co/go/tools/cmd/staticcheck to perform static analysis on your code before committing a change.

This raises a couple of interesting questions — especially in a team environment. How do you make sure that everyone has the necessary tools installed on their machines? And that the tools they are using are all the same version?

Until Go 1.17, the convention for managing this was to create a tools.go file in your project containing import statements for the different tools and a //go:build tools build constraint. If you're not already familiar with this approach, it's described in the official Go Wiki.

But since Go 1.17 there is an alternative approach you can take. It has pros and cons compared to the tools.go approach, but it's worth knowing about and may be a good fit for some projects.

It hinges on the fact that go run now allows you to execute a specific version of a remote package. From the 1.17 release notes:

go run now accepts arguments with version suffixes (for example, go run example.com/cmd@v1.0.0). This causes go run to build and run packages in module-aware mode, ignoring the go.mod file in the current directory or any parent directory, if there is one.

In other words, you can use go run package@version to execute a remote package when you are outside of a module, or inside of a module even if the package isn't listed in the go.mod file.

It's also useful as a quick way to run an executable package without installing it. Instead of this:

$ go install honnef.co/go/tools/cmd/staticcheck@v0.3.1
$ staticcheck ./...

You can now just do this:

$ go run honnef.co/go/tools/cmd/staticcheck@v0.3.1 ./...

Using with go:generate

Let's take a look at an example where we use the golang.org/x/tools/cmd/stringer tool in conjunction with go:generate to generate String() methods for some iota constants.

If you'd like to follow along, please run the following commands:

$ mkdir tools
$ go mod init example.com/tools
$ touch main.go

And then add the following code to main.go:

File: main.go
package main

import "fmt"

//go:generate go run golang.org/x/tools/cmd/stringer@v0.1.10 -type=Level

type Level int

const (
    Info Level = iota
    Error
    Fatal
)

func main() {
    fmt.Printf("%s: Hello world!\n", Info)
}

The important thing here is the //go:generate line. When you run go generate on this file, it will in turn use go run to execute v0.1.10 of the golang.org/x/tools/cmd/stringer package.

Let's try it out:

$ go generate .
go: downloading golang.org/x/tools v0.1.10
go: downloading golang.org/x/sys v0.0.0-20211019181941-9d821ace8654
go: downloading golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
go: downloading golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3

You should see that the necessary modules are downloaded and then the go:generate command finishes executing successfully — resulting in a new level_string.go file being generated and a working application. Like so:

$ ls 
go.mod  level_string.go  main.go
$ go run .
Info: Hello world!

Using in a Makefile

You can also use the go run package@version pattern to execute tools from your scripts or Makefiles. To illustrate, let's create a Makefile with an audit task that executes a specific version of the staticcheck tool.

$ touch Makefile
File: Makefile
.PHONY: audit
audit:
    go vet ./...
    go run honnef.co/go/tools/cmd/staticcheck@v0.3.1 ./...

If you run make audit, the necessary modules will be downloaded and the staticcheck tool should complete its checks successfully.

$ make audit
go vet ./...
go run honnef.co/go/tools/cmd/staticcheck@v0.3.1 ./...
go: downloading honnef.co/go/tools v0.3.1
go: downloading golang.org/x/tools v0.1.11-0.20220316014157-77aa08bb151a
go: downloading golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e
go: downloading github.com/BurntSushi/toml v0.4.1

If you run it for a second time, you'll see that the module cache is used and it should finish much faster.

$ make audit
go vet ./...
go run honnef.co/go/tools/cmd/staticcheck@v0.3.1 ./...

Pros and cons

In terms of positives, go run package@version has a couple of nice advantages over the tools.go approach:

  • It's simpler to set up and requires less code — no tools.go file is needed, there are no build constraints, and no aliased imports.

  • It avoids polluting your dependency graph with things that your binaries do not actually depend on.

In terms of negatives:

  • If you have the same go run package@version command in multiple places throughout your codebase and want to upgrade to a newer version, then you need to update all of the commands manually (or use sed or find-and-replace). With the tools.go approach you only need to update your go.mod file by running go get package@newversion.

  • With the tools.go approach it's possible to verify that cached code in your module cache hasn't been changed by running go mod verify. I'm not aware of an equivalent check for go run package@version (if you know of a way to do this, please let me know!). From my limited testing, it seems to be possible to edit the cached code in the module cache on your machine, and go run package@version will use this edited code without complaining.

  • If you are working offline, then go run package@version may fail with a dial tcp: lookup proxy.golang.org: Temporary failure in name resolution error because it can't reach the Go module mirror — even if there is a copy already in your local module cache. Similar to this:

    $ make audit
    go vet ./...
    go run honnef.co/go/tools/cmd/staticcheck@v0.3.1 ./...
    go: honnef.co/go/tools/cmd/staticcheck@v0.3.1: honnef.co/go/tools/cmd/staticcheck@v0.3.1: Get "https://proxy.golang.org/honnef.co/go/tools/cmd/staticcheck/@v/v0.3.1.info": dial tcp: lookup proxy.golang.org: Temporary failure in name resolution
    make: *** [Makefile:4: audit] Error 1
    

    As far as I can see this isn't a problem when you use the tools.go approach, although you can work around it fairly easily by setting the GOPROXY environment variable to direct while you are offline. Doing this will force go run to bypass the Go module mirror and use the cached module on your machine straight away.

    $ export GOPROXY=direct
    $ make audit
    go vet ./...
    go run honnef.co/go/tools/cmd/staticcheck@v0.3.1 ./...