The go list -json Option

After The go -x Option I wanted to write about another Go flag that I use more than I expected to. The go list command prints the import paths of the packages you give it and most people never go further than that. With the -json flag it prints everything the Go tool knows about each package.

For this post I use a small module called golist with two packages. It lives in /tmp/golist and looks like this:

$ cd /tmp/golist
$ tree
.
├── go.mod
├── greet
│   ├── greet_test.go
│   └── greet.go
└── main.go

2 directories, 4 files

Keep in mind that the /tmp directory is emptied after a reboot on most UNIX systems, so put the module somewhere else if you want to keep it. Here is main.go:

package main

import (
	"fmt"
	"os"

	"golist/greet"
)

func main() {
	name := "world"
	if len(os.Args) > 1 {
		name = os.Args[1]
	}
	fmt.Println(greet.Hello(name))
}

And here is greet/greet.go:

package greet

import "strings"

func Hello(name string) string {
	return "Hello " + strings.ToUpper(name)
}

The greet directory also has a greet_test.go file with a single test. Without any flags, the go list command gives back nothing more than the import paths:

$ go version
go version go1.27.3 darwin/arm64
$ go list ./...
golist
golist/greet

This is what the Go tool knows about the greet package. The real output is longer, since I removed a couple of fields and shortened the Deps list to keep it readable:

$ go list -json ./greet
{
	"Dir": "/tmp/golist/greet",
	"ImportPath": "golist/greet",
	"Name": "greet",
	"Root": "/tmp/golist",
	"Module": {
		"Path": "golist",
		"Main": true,
		"Dir": "/tmp/golist",
		"GoMod": "/tmp/golist/go.mod",
		"GoVersion": "1.27.3"
	},
	"Match": [
		"./greet"
	],
	"GoFiles": [
		"greet.go"
	],
	"Imports": [
		"strings"
	],
	"Deps": [
		"errors",
		"internal/abi",
		...
		"unicode/utf8",
		"unsafe"
	],
	"TestGoFiles": [
		"greet_test.go"
	],
	"TestImports": [
		"testing"
	]
}

The Dir field is where the package lives on disk and the GoFiles field lists the files that go into a normal build. The test files are kept apart in the TestGoFiles field and their imports go to the TestImports field. The Module field tells you which module the package belongs to and where its go.mod file is. The Imports field holds what the package imports directly, while the Deps field holds everything it needs, recursively. For a package that only imports strings the Deps field has 40 entries, most of them internal packages of the runtime.

Most of the time I need one or two of these fields and not the whole thing. You can put the names of the fields you want after -json= and the Go tool prints only those. According to go help list, it may also skip computing the fields you did not ask for, so asking for less can be faster on a big module.

$ go list -json=ImportPath,Imports ./...
{
	"ImportPath": "golist",
	"Imports": [
		"fmt",
		"golist/greet",
		"os"
	]
}
{
	"ImportPath": "golist/greet",
	"Imports": [
		"strings"
	]
}

The output is one JSON object per package, printed one after the other, without commas and without an enclosing array. That happens to be what jq reads by default. The -deps flag makes go list visit every dependency of the packages you name, so the following command prints the packages of the golist program that are not part of the standard library:

$ go list -json -deps . | jq -r 'select(.Standard | not) | .ImportPath'
golist/greet
golist

The golist/greet package comes first because go list -deps prints every package after all of its dependencies. In a real project this list would also include every third party package that your program pulls in.

When I do not need JSON at all, I use the -f flag, which takes a Go template. The template sees the same fields as the JSON output and it is often shorter to type than a jq filter:

$ go list -f '{{.ImportPath}}: {{.Imports}}' ./...
golist: [fmt golist/greet os]
golist/greet: [strings]
$ go list -deps -f '{{if not .Standard}}{{.ImportPath}}{{end}}' .
golist/greet
golist

The second command gives the same answer as the jq example without leaving the Go tool. A package whose template produces nothing does not leave an empty line behind.

The -m flag makes go list work with modules instead of packages. The go list -m -json all command prints every module that takes part in the build and go list -m -u all also tells you which of them have newer versions. The golist module has no dependencies, so there is nothing interesting to show here, but on a real project it is a quick way to see what you depend on before running go get -u.

The last flag I want to show is -e. To see what it does, I break the module on purpose by adding a file with the wrong package name to the greet directory:

$ echo 'package greeting' > greet/extra.go
$ go list ./...
main.go:7:2: found packages greeting (extra.go) and greet (greet.go) in /tmp/golist/greet
$ go list -e -json=ImportPath,Error ./...
{
	"ImportPath": "golist"
}
{
	"ImportPath": "golist/greet",
	"Error": {
		"ImportStack": [
			"golist"
		],
		"Pos": "main.go:7:2",
		"Err": "found packages greeting (extra.go) and greet (greet.go) in /tmp/golist/greet"
	}
}
$ rm greet/extra.go

Without -e the command prints the error and exits with a non-zero status. With -e it reports every package anyway and puts the problem in the Error field of the package that has it. Editors need exactly this, because they have to keep working while your code is broken. The gopls language server and most other Go tools learn about your code through the golang.org/x/tools/go/packages package, which runs go list -e -json behind the scenes.

Happy coding in Go!

You can purchase Practical Systems Programming in Go from amazon.com or Packt Publishing. All code from the book is available in the official GitHub repository.

You can get Mastering Go, 4th edition on Packt, Amazon.com, all other Amazon web sites as well as on other book stores.