Как визуализировать зависимости модуля Go

Go

эта статьяНачалось вмой блог, если вы найдете это полезным, пожалуйста, поставьте лайк и соберите его, чтобы больше друзей могли его увидеть.Похожие видео(первая запись)

Недавно я разработал очень простой небольшой инструмент с менее чем 200 строками кода. Сегодня я кратко представлю его. Что это за инструмент? Это инструмент для визуального отображения зависимостей модуля Go.

Зачем разрабатывать

Почему вы задумали разработать этот инструмент? Есть две основные причины:

Во-первых, в последнее время я часто вижу, как люди обсуждают модули Go в сообществе. Итак, я также потратил некоторое время на его изучение. В этот период возникло требование, как четко идентифицировать взаимосвязь между зависимостями в модуле. После некоторых исследований я обнаружилgo mod graph.

Эффект следующий:

$ 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

Формат каждой строки模块 依赖模块, который в принципе может соответствовать требованиям, но мне все же кажется, что он не такой уж интуитивный.

Во-вторых, у меня раньше был проект в руках, и dep использовался для управления пакетами. Итак, я тоже узнал об этом и внимательно прочитал официальную документацию. один из нихглаваВводит метод визуального отображения зависимостей.

Схема пакета, приведенная в документации:

Когда я увидел эту картинку, мои глаза мгновенно загорелись, графика была превосходной, а взаимосвязь между различными зависимостями была понятна с первого взгляда. Разве это не то, чего я хочу? 666, нравится.

Но... проблема в том, что мод Go не имеет такой возможности. что делать?

Как добиться

Посмотрите, сделал ли кто-нибудь это уже. Искал в интернете и не нашел. Можно ли добиться этого самостоятельно? Должна же быть возможность учиться у депа по идее, верно?

Вот как визуализируются зависимости dep:

# 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

Вот как использовать его в трех основных системах, каждая из которых устанавливает программный пакет graphviz. Судя по названию, это должна быть программа для визуализации, то есть для рисования. Это правда, вы можете это видетьОфициальный сайт.

Глядя на его использование еще раз, обнаруживается, что все они объединены через конвейерные команды, а предыдущие части в основном одинаковы, обеdep status -dot | dot -T png. Последняя часть отличается в разных системах, Linuxdisplay, MacOS естьopen -f -a /Applications/Preview.app, Окно естьstart status.png.

Проведя небольшой анализ, вы поймете, что передняя — для генерации картинок, а задняя — для отображения картинок. Поскольку команды отображения изображений в разных системах различаются, следующие части также различаются.

Теперь основное внимание уделяется фронту, а именноdep status -dot | dot -T pngЧто он делает и как именно реализует рисунок? Грубо говоря, точка -T png — это изображение, сгенерированное данными, предоставленными dep status -dot. Продолжай читать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;
}

На первый взгляд, вывод представляет собой фрагмент кода, который, кажется, не знает, что это такое, который должен быть языком, используемым graphviz для рисования графиков. Все еще учится? Конечно нет, его очень просто использовать здесь, просто примените его напрямую.

Попробуйте его проанализировать, первые две строчки можно пропустить, это должен быть особый способ написания графвиза, указывающий, какой именно график рисовать. Нашей главной заботой является то, как предоставить данные в правильной форме.

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;

Как вы можете видеть с первого взгляда, здесь есть две структуры, а именно связывание идентификаторов для зависимостей и по идентификатору и->Представляет отношения между зависимостями.

В соответствии с приведенной выше гипотезой мы можем попытаться нарисовать простую диаграмму, показывающую, что модуль а зависит от модуля b. Выполните команду следующим образом, передайте код рисования черезeachподведен кdotЗаказ.

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

Эффект следующий:

Нарисовать граф зависимостей очень просто.

Увидев это, вы обнаружили, что задача стала очень простой. мы просто положилиgo mod graphВывод можно визуализировать, преобразовав его в аналогичную структуру.

Введение в процесс разработки

Далее разработайте этот апплет, я назову этот апплет какmodv, что означает, что модуль виден. Исходный код проекта находится вpoloxue/modv.

получать входные данные из конвейера

Сначала проверьте, нормально ли работает конвейер ввода данных.

Наша цель — использовать что-то вродеdepв способе рисования,go mod graphПередать данные через канал вmodv. Поэтому сначала проверьтеos.Stdin, то есть проверьте, нормальный ли статус стандартного ввода и передается ли он по конвейеру.

Ниже приведен код основной функции, расположенной вmain.goсередина.

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

	// 是否是管道传输
	if info.Mode()&os.ModeNamedPipe == 0 {
		fmt.Println("command err: command is intended to work with pipes.")
		PrintUsage()
		os.Exit(1)
	}

Как только будет подтверждено, что устройство ввода нормальное, мы можем начать процесс чтения, анализа и рендеринга данных.

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

Далее давайте подробно рассмотрим, как реализовать поток обработки данных.

абстрактная структура реализации

Сначала определите структуру и примерно опишите весь процесс.

type ModGraph struct {
	Reader io.Reader  // 读取数据流
}

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

// 执行数据的处理转化
func (m *ModGraph) Parse() error {}

// 结果渲染与输出
func (m *ModGraph) Render(w io.Writer) error {}

посмотри снова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
...

Структура каждой строки模块 依赖项. Теперь цель состоит в том, чтобы разобрать его в следующую структуру:

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;
}

Как упоминалось ранее, здесь есть две разные структуры, а именно отношения между модулями и идентификаторами, и идентификаторы модулей представляют зависимости между модулями. заModGraphСтруктура добавляет два члена для их представления.

type ModGraph struct {
	r io.Reader  // 数据流读取实例,这里即 os.Stdin
 
	// 每一项名称与 ID 的映射
	Mods         map[string]int
	// ID 和依赖 ID 关系映射,一个 ID 可能依赖多个项
	Dependencies map[int][]int
}

Следует отметить, что после добавления двух элементов карты не забудьтеNewModGraphИнициализируйте их в .

парсинг вывода графа мода

Как его разобрать?

На данный момент цель ясна. состоит в том, чтобы разобрать входные данные наModsиDependenciesВ обоих членах код реализации находится вParseметод.

Чтобы облегчить чтение данных, во-первых, мы используемbufioна основеreaderсоздать новыйbufReader,

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

Чтобы упростить анализ данных построчно, мы передаем bufReader'sReadBytes()Цикл метода считывает данные в os.Stdin построчно. Затем разделите каждую строку данных пробелами, чтобы получить два элемента зависимостей. код показывает, как показано ниже:

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]))

    ...
}

Следующим шагом является организация проанализированных зависимостей вModsиDependenciesиз двух членов. Идентификатор модуля — это самый простой способ создания правил, и он увеличивается с 1. Код реализации выглядит следующим образом:

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}
}

На этом работа по разбору заканчивается.

Отрисовка результата парсинга

Этому гаджету осталось сделать последний шаг — отобразить проанализированные данные в соответствии сgraphvizТребования к чертежу инструмента. Код реализацииRenderчасть:

Сначала определите шаблон для создания выходного формата, соответствующего вашим требованиям.

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 -}}
}
`

В этом произведении особо нечего вводить, главное знать Gotext/templateСпецификация синтаксиса для шаблонов. Чтобы показать дружелюбие, здесь-Реализовать удаление новых строк, что не влияет на чтение в целом.

Далее см.RenderРеализация метода, предварительно разобранныйModsиDependenciesПоместить в шаблон для рендеринга.

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
}

Теперь вся работа сделана. Наконец, интегрируйте этот процесс в основную функцию. Следующим шагом является его использование.

Используйте опыт

Начать. Кроме того, я тестировал использование этого инструмента только под Mac.Если у вас есть какие-либо вопросы, пожалуйста, не стесняйтесь спрашивать.

Сначала установите егоgraphviz, метод установки был представлен в начале этой статьи, выберите метод установки вашей системы.

Затем установитеmodv, команда выглядит следующим образом:

$ go get github.com/poloxue/modv

Установка завершена! Просто проверьте его использование.

Возьмем, к примеру, MacOS. Сначала загрузите тестовую библиотеку, github.com/poloxue/testmod. Войдите в каталог testmod и выполните команду:

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

Если выполнение прошло успешно, вы увидите следующие эффекты:

Он отлично показывает зависимости между различными модулями.

некоторые размышления

Эта статья представляет собой практическую статью, которая проходит путь от простой идеи до успешного представления работающего инструмента. Хотя разработать его несложно, от разработки до завершения прошло всего час или два. Но мне кажется, что это действительно полезный инструмент.

Есть еще некоторые идеи, которые не были реализованы и проверены, например, удобно ли отображать дерево зависимостей указанного узла вместо всего проекта, когда проект большой. Кроме того, есть ли какая-либо ценность в этом маленьком инструменте, когда другие проекты переходят на модули Go.


Добро пожаловать в мой публичный аккаунт WeChat.