Kyle Banks

Visualize Go Dependency Trees with Depth

Written by @kylewbanks on Mar 4, 2017.

Nobody likes a big dependency tree, particularly on external third-party packages that need to be maintained, debugged, vendored, etc. While working on Commuter I was experimenting with the new dependency management tool, ‘dep’ and began to wonder exactly how many packages I’m pulling in and how they’re being used.

As great as the standard Go tooling is, I was surprised to find no built-in way of visualizing the dependency tree of a package. A simple go list ./… does print the basics, but you can’t see which packages are actually being used and by which packages. go list also omits standard library packages which can be nice to see as well. This is where depth comes in!

Introducing Depth

depth is a command-line application to visualize the dependency tree of a particular package or a set of packages, working on wither your own packages, third-party libraries, or the Go standard library. Simply execute depth with the name of the package(s) to visualize:

# Any of your own packages or third-party libraries:
$ depth github.com/KyleBanks/depth/cmd/depth
github.com/KyleBanks/depth/cmd/depth
  ├ encoding/json
  ├ flag
  ├ fmt
  ├ io
  ├ log
  ├ os
  ├ strings
  └ github.com/KyleBanks/depth
    ├ fmt
    ├ go/build
    ├ path
    ├ sort
    └ strings

# Standard library:
$ depth strings
strings
  ├ errors
  ├ io
  ├ unicode
  └ unicode/utf8
  
# Or both:
$ depth strings github.com/KyleBanks/depth
strings
  ├ errors
  ├ io
  ├ unicode
  └ unicode/utf8
github.com/KyleBanks/depth
  ├ fmt
  ├ go/build
  ├ path
  ├ sort
  └ strings

There are a few arguments you can provide to get richer or simplified output, including test-only dependencies, limiting the depth of the tree, resolving internal standard library dependencies, JSON output, and more. For more on that, be sure to check depth out on GitHub.

depth is also written in a way that makes it simple to pull into your own projects and resolve the dependency tree of any package through code:

import "github.com/KyleBanks/depth"

var t depth.Tree
if err := t.Resolve("strings"); err != nil {
    log.Fatal(err)
}

// Output: "'strings' has 4 dependencies."
log.Printf("'%v' has %v dependencies.", t.Root.Name, len(t.Root.Deps)) 

Again, check the project out on GitHub for more on that.

Fun with Recursion

depth was a lot of fun to develop, mostly because it was all about recursion. The Tree starts with a root Pkg, which in turn has dependencies represented as a []Pkg - each of these has their own dependencies, and so on and so forth.

type Tree struct {
	Root *Pkg
	
	...
}

type Pkg struct {
	Tree   *Tree
	Parent *Pkg 
	Deps   []Pkg
	
	...
}

Resolving dependencies for each Pkg requires finding all the packages it imports (the dependencies or Deps), which must each be resolved themselves. You can find all the code on GitHub of course, but here’s a simplified version of the dependency resolution logic:

func (p *Pkg) Resolve(i Importer, resolveImports bool) error {   
    pkg, err := i.Import(name, p.SrcDir, importMode)
    if err != nil {
        return err
    }
    
    for _, imp := range pkg.Imports {
        if err := p.addDep(i, imp, pkg.Dir); err != nil {
            return err
        }
    }
    
    return nil
}

func (p *Pkg) addDep(i Importer, name string, srcDir string) error {
    dep := Pkg{
        Name:   name,
        SrcDir: srcDir,
        Tree:   p.Tree,
        Parent: p,
    }
    
    if err := dep.Resolve(i, resolveImports); err != nil {
        return err
    }
    p.Deps = append(p.Deps, dep)
    
    return nil
}

There’s a number of things missing from this simplified version, such as depth limiting, unique checks, handling internal versus third-party packages, etc. However, it should demonstrate the recursion at play in depth. Starting with Resolve I use an Importer type to determine the imports required by the current Pkg. For each import (pkg.Imports) I then call addImport, which in turn must use Resolve on the newly created Pkg to determine its dependencies.

The whole project basically wound up with recursive functions like this, performing logic such as calculating the depth of a particular Pkg in the Tree, seeing if a particular Pkg is ever a parent of another, and even indenting the output of the depth command-line application.

Let me know if this post was helpful on Twitter @kylewbanks or down below!