Building a Go Mod CLI to generate dependency trees

So, picture the scene, you have a Go project and then get a security alert saying a vulnerability has been detected. You look at your go.mod to see if you're using it and it's nowhere to be seen, but then you see it in your go.sum. How do you find where it is coming from?

Sound familiar? Well that's the situation I found myself in last week. So obviously, I tried to find a tool that would do it but couldn't find one, so like any programmer would do, I decided to build one.

The tool can be found here, with amd64 binaries for Linux, Windows and Mac available in the releases.

Installation

To install, simply download the appropriate binary for your operating system from the latest release, at the time of writing this, it was v1.2.0. Once downloaded, make sure the binary is executable and moved to be in your PATH, with the name go-tree.

How to use it

The tool only works with go mod projects, and requires the environment variable GOPATH to be set (by default, it will be $HOME/go). You can either run it from within the root directory of your go project (where you go.mod is located), or you can use the -modulePath flag to pass in a relative or absolute path to the project you want to scan.

$ go-tree -modulePath $GOPATH/src/path/to/module/to/scan

You can also use the -maxDepth flag to set the maximum recursion level, i.e. how far down the tree to scan. The options are either -1 or an integer above 0, -1 is to indicate no limit and is the default value.

$ go-tree -maxDepth -1

The final flag you can use is the -find flag, which is the whole reason this tool exists. If you specify this flag with the module you would like to find, it will print the full tree for all of the instances of that package in the dependency tree. Note that if you use -find, the -maxDepth will be ignored.

$ go-tree -find github.com/kapilpau/go-mod-dependency-tree

None of these flags are required.

How it works

As this is a single function cli tool, the whole code is contained within a single file. The code has two main pathways, both of which work in a similar way.

The first is the straight tree dump, i.e. where you don't specify the -find flag. This route recursively searches each dependency's go.mod to find all of the dependencies for that module and prints out the name and version of the dependency. For this, we read in the go.mod file for your project, find all of the modules in the requires section and look for the module in the src or pkg folders, in your GOPATH.

If they have exist and have a go.mod file, we continue through the chains and look for their dependencies. If we can't find a dependency in either location, or it doesn't have a go.mod file, we end that branch there and move on.

The other pathway is for when a user is searching for a specific module in the chain. In this case, it is slightly more complicated as we need to decide what to print out later on. For this, we use a custom tree struct, named dependencyChain. This struct has two fields, module (the name of the module currently being scanned) and children (the dependencies of the current module).

We do a similar recursive search to the one detailed above, however, rather than just simply printing out the values as we find them, we have to perform head recursion, so we can look at the outputs of the later recurssions before deciding what to do. So, if we find the module we're looking for, we end the branch of the tree there, as it would be wasteful to carry on, and we populate the dependencyChain object to pass back. Then, when we have the list of dependencyChains for each module, we check the size of the children field and if it is not empty, we pass it upwards, otherwise we ignore it. The reason we do this check is because we only want to see the branches that end in the module we're looking for.

Once we have completed this head recursive search, we perform a tail recursive print, to loop through the children of each dependencyChain and display it as a tree.

If the module you are looking for does not exist in the chain, or it cannot be found (as it may be in a non-go mod enabled project), then a message is printed out at the end to say so.

Lessons learned

I learned a lot from this project, namely how easy it is to create and build cli binaries in Go, even being able to build for different operating systems and architectures, without having to natively use an instance of them. This is definitely just the first of many more to come.

I got to apply the principles of recursion, that we spent so long learning at uni, to a real-life scenario.

I gained a deeper understanding of how Go stores dependencies, and where to find them when I need them.

21