How to visualize the dependency of Go Module

This paper First appeared in My blog , if you think it's useful, please like the collection and let more friends see it.

Recently, I developed a very simple tool, the total code is less than 200 lines. Today, briefly introduce it. What tool is this? It is a tool for visualizing Go Module dependencies.

Why development

Why do you want to develop this tool? There are two main reasons:

One is that we often see you discussing Go Module in the community recently. So I spent some time studying it. During this process, we met a requirement to clearly identify the relationship between the dependencies in the module. After some understanding, I found the go mod graph.

The results are as follows:

$ go mod graph
github.com/poloxue/testmod golang.org/x/text@v0.3.2
github.com/poloxue/testmod rsc.io/quote/v3@v3.1.0
github.com/poloxue/testmod rsc.io/sampler@v1.3.1
golang.org/x/text@v0.3.2 golang.org/x/tools@v0.0.0-20180917221912-90fa682c2a6e
rsc.io/quote/v3@v3.1.0 rsc.io/sampler@v1.3.0
rsc.io/sampler@v1.3.1 golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c
rsc.io/sampler@v1.3.0 golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c

The format of each line is module dependent, which can basically meet the requirements, but it is still not so intuitive.

Second, I have a project in my hand before, and the package management has always used dep. So I got to know it and read the official documents carefully. Among them A chapter This paper introduces the method of dependency visualization.

Package diagram given in the document:

When you see this picture, your eyes will be bright in an instant. Graphing is excellence. The relationship between different dependencies is clear at a glance. Isn't that what I want? 666, like it.

But... The following problem is that go mod does not have this ability. What should I do?

How to achieve

Let's see if someone has already done it. I searched the Internet, but I couldn't find it. Can it be realized by itself? Should we learn from the idea of dep?

The following is how dep relies on Visualization:

# linux
$ sudo apt-get install graphviz
$ dep status -dot | dot -T png | display

# macOS
$ brew install graphviz
$ dep status -dot | dot -T png | open -f -a /Applications/Preview.app

# Windows
> choco install graphviz.portable
> dep status -dot | dot -T png -o status.png; start status.png

Here we show the usage of the three systems. They all have a software package, graphviz. In terms of name, this should be a software for visualization, that is, for drawing. It's the same thing. You can see it. Official website.

Looking at its use, we find that it is all combined by pipeline commands, and the previous parts are basically the same. They are dep status -dot | dot -T png. The latter part is different in different systems. Linux is display, Mac OS is open-f-a / applications / preview.app, and Window is start status.png.

After a little analysis, it will be clear that the front is to generate pictures, and the back is to display pictures. Because the image display commands of different systems are different, the following parts are also different.

Now the focus is on what dep status -dot | dot -T png has done, and how does it realize drawing? Presumably, dot-t PNG is the data provided by DEP status-dot to generate pictures. Continue to see the execution effect of dep status -dot.

$ dep status -dot
digraph {
    node [shape=box];
    2609291568 [label="github.com/poloxue/hellodep"];
    953278068 [label="rsc.io/quote\nv3.1.0"];
    3852693168 [label="rsc.io/sampler\nv1.0.0"];
    2609291568 -> 953278068;
    953278068 -> 3852693168;
}

At first glance, the output is a piece of code that doesn't seem to know what it is. This should be the language graphviz uses to draw charts. Is that still learning? Of course not. It's very simple here. Just apply it directly.

Try to analyze it. The first two lines don't need to be concerned. This should be graphviz specific writing method, indicating what kind of picture to draw. Our main concern is how to provide data in the right form.

2609291568 [label="github.com/poloxue/hellodep"];
953278068 [label="rsc.io/quote\nv3.1.0"];
3852693168 [label="rsc.io/sampler\nv1.0.0"];
2609291568 -> 953278068;
953278068 -> 3852693168;

At first glance, we know that there are two kinds of structures, one is to associate ID for dependency, and the other is to represent the relationship between dependencies through ID and - >.

According to the above conjecture, we can try to draw a simple diagram to show that a module depends on b module. Execute the command as follows to send the drawing code to the dot command through each pipeline.

$ echo 'digraph {
node [shape=box];
1 [label="a"];
2 [label="b"];
1 -> 2;
}' | dot -T png | open -f -a /Applications/Preview.app 

The results are as follows:

It's so easy to draw a dependency graph.

It's very easy to find out the problem here. We only need to transform the output of go mod graph into a similar structure to realize visualization.

Introduction to development process

Next, let's develop this small program. I will name this small program modv, which means module visible. Project source code is located in poloxue/modv.

Receive input from pipeline

First, check whether the data input pipeline is normal.

Our goal is to use a drawing method similar to that in dep, go mod graph will pipe data to modv. Therefore, first check os.Stdin, that is, check whether the standard input status is normal and whether it is pipeline transmission.

Here is the code for the main function, located at main.go Medium.

func main() {
    info, err := os.Stdin.Stat()
    if err != nil {
        fmt.Println("os.Stdin.Stat:", err)
        PrintUsage()
        os.Exit(1)
    }

    // Is it pipeline transmission?
    if info.Mode()&os.ModeNamedPipe == 0 {
        fmt.Println("command err: command is intended to work with pipes.")
        PrintUsage()
        os.Exit(1)
    }

Once the input device is confirmed to be normal, we can enter the process of data reading, parsing and rendering.

    mg := NewModuleGraph(os.Stdin)
    mg.Parse()
    mg.Render(os.Stdout)
}

Next, let's see how to implement the data processing flow.

Abstract implementation structure

First, define a structure, and roughly define the whole process.

type ModGraph struct {
    Reader io.Reader  // Read data stream
}

func NewModGraph(r io.Reader) *ModGraph {
    return &ModGraph{Reader: r}
}

// Perform data processing and transformation
func (m *ModGraph) Parse() error {}

// Result rendering and output
func (m *ModGraph) Render(w io.Writer) error {}

Take a look at the output of go mod graph, as follows:

github.com/poloxue/testmod golang.org/x/text@v0.3.2
github.com/poloxue/testmod rsc.io/quote/v3@v3.1.0
...

The structure of each row is a module dependency. The goal now is to parse it into the following structure:

digraph {
    node [shape=box];
    1 github.com/poloxue/testmod;
    2 golang.org/x/text@v0.3.2;
    3 rsc.io/quote/v3@v3.1.0;
    1 -> 2;
    1 -> 3;
}

As mentioned earlier, there are two different structures, namely module and ID Association, and module ID represents the dependency association between modules. Add two members to the ModGraph structure to represent them.

type ModGraph struct {
    r io.Reader  // Data stream reading instance, which is os.Stdin
 
    // Mapping of each item name and ID
    Mods         map[string]int
    // ID and dependency ID relationship mapping, an ID may depend on multiple items
    Dependencies map[int][]int
}

Note that after adding two map members, remember to initialize them in NewModGraph.

Analysis of mod graph output

How to parse?

Introduced here, the goal has been very clear. To Parse the input data into Mods and Dependencies, the implementation code is in the Parse method.

To facilitate data reading, first of all, we use bufio to create a new buf reader based on the reader.

func (m *ModGraph) Parse() error {
    bufReader := bufio.NewReader(m.Reader)
    ...

To facilitate parsing data by row, we read the data in os.Stdin row by row through the ReadBytes() method of bufReader. Then, each row of data is divided into two spaces to get two items of dependency. The code is as follows:

for {
    relationBytes, err := bufReader.ReadBytes('\n')
    if err != nil {
        if err == io.EOF {
            return nil
        }
        return err
    }

    relation := bytes.Split(relationBytes, []byte(" "))
    // module and dependency
    mod, depMod := strings.TrimSpace(string(relation[0])), strings.TrimSpace(string(relation[1]))

    ...
}

Next, organize the resolved Dependencies into Mods and Dependencies. The module ID is the simplest way to generate rules, which is self increasing from 1. The implementation code is as follows:

modId, ok := m.Mods[mod]
if !ok {
    modId = serialID
    m.Mods[mod] = modId
    serialID += 1
}

depModId, ok := m.Mods[depMod]
if !ok {
    depModId = serialID
    m.Mods[depMod] = depModId
    serialID += 1
}

if _, ok := m.Dependencies[modId]; ok {
    m.Dependencies[modId] = append(m.Dependencies[modId], depModId)
} else {
    m.Dependencies[modId] = []int{depModId}
}

This is the end of parsing.

Render the result of parsing

The last step of the tool is to Render the parsed data to meet the drawing requirements of graphviz tool. The implementation code is the Render part:

First, define a template to generate an output format that meets the requirements.

var graphTemplate = `digraph {
node [shape=box];
{{ range $mod, $modId := .mods -}}
{{ $modId }} [label="{{ $mod }}"];
{{ end -}}
{{- range $modId, $depModIds := .dependencies -}}
{{- range $_, $depModId := $depModIds -}}
{{ $modId }} -> {{ $depModId }};
{{  end -}}
{{- end -}}
}
`

There is nothing to introduce in this section, mainly to be familiar with the syntax specification of the text/template template template in Go. In order to show friendliness, we use - to achieve line breaking removal, which does not affect reading as a whole.

Next, look at the implementation of the Render method. Put the previously parsed Mods and Dependencies into the template for rendering.

func (m *ModuleGraph) Render(w io.Writer) error {
    templ, err := template.New("graph").Parse(graphTemplate)
    if err != nil {
        return fmt.Errorf("templ.Parse: %v", err)
    }

    if err := templ.Execute(w, map[string]interface{}{
        "mods":         m.Mods,
        "dependencies": m.Dependencies,
    }); err != nil {
        return fmt.Errorf("templ.Execute: %v", err)
    }

    return nil
}

Now, all the work is done. Finally, the process is integrated into the main function. The next step is to use it.

User's experience

Let's start. In addition, I only test the use of this tool under Mac now. If you have any questions, please come up with them.

First, you need to install graphviz. The installation method is introduced at the beginning of this article. Choose your system installation method.

Next, install modv. The command is as follows:

$ go get github.com/poloxue/modv

Installation complete! Simply test its use.

Take MacOS for example. Download the test library, github.com/pollox/testmod. Enter testmod directory to execute the command:

$ go mod graph | modv | dot -T png | open -f -a /Applications/Preview.app

If the execution is successful, you will see the following effects:

It perfectly shows the dependencies among the modules.

Some thoughts

This is a practical article, from a simple idea to a successful presentation of a usable tool. Although, it is not difficult to develop. It only takes an hour or two from development to completion. But my feeling is that it's really a valuable tool.

There are still some ideas that have not been implemented and verified, such as whether it is convenient to display the dependency tree of a specific node rather than the whole project once the project is large. What's more, whether the gadget can generate some value when other projects migrate to the Go Module.

Welcome to my wechat account.

Keywords: Go github Linux Mac less

Added by altexis on Wed, 23 Oct 2019 09:06:16 +0300