最近,我開發了一個很是簡單的小工具,總的代碼量 200 行不到。今天,簡單介紹下它。這是個什麼工具呢?它是一個用於可視化展現 Go Module 依賴關係的工具。node
爲何會想到開發這個工具?主要有兩點緣由:linux
一是最近常常看到你們在社區討論 Go Module。因而,我也花了一些時間研究了下。期間,遇到了一個需求,如何清晰地識別模塊中依賴項之間的關係。一番瞭解後,發現了 go mod graph
。git
效果以下:github
$ 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
複製代碼
每一行的格式是 模塊 依賴模塊
,基本能知足要求,但總以爲仍是不那麼直觀。golang
二是我以前手裏有一個項目,包管理一直用的是 dep。因而,我也瞭解了下它,把官方文檔仔細讀了一遍。其中的某個章節介紹了依賴項可視化展現的方法。小程序
文檔中給出的包關係圖:bash
看到這張圖的時候,眼睛瞬間就亮了,圖形化就是優秀,不一樣依賴之間的關係一目瞭然。這不就是我想要的效果嗎?666,點個贊。微信
但 ...,隨之而來的問題是,go mod 沒這個能力啊。怎麼辦?app
先看看是否是已經有人作了這件事了。網上搜了下,沒找到。那是否是能本身實現?應該能夠借鑑下 dep 的思路吧?
以下是 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
。後面的部分在不一樣的系統就不一樣了,Linux 是 display
,MacOS 是 open -f -a /Applications/Preview.app
,Window 是 start status.png
。
稍微分析下就會明白,前面是生成圖片,後面是顯示圖片。由於不一樣系統的圖片展現命令不一樣,因此後面的部分也就不一樣了。
如今關心的重點在前面,即 dep status -dot | dot -T png
幹了啥,它到底是如何實現繪圖的?大體猜想,dot -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 用於繪製圖表的語言。那是否是還有學習下?固然不用啊,這裏用的很簡單,直接套用就好了。
試着分析一下吧,前面兩行能夠不用關心,這應該是 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;
複製代碼
一看就知道,這裏有兩種結構,分別是爲依賴項關聯 ID ,和經過 ID 和 ->
表示依賴間的關係。
按上面的猜測,咱們能夠試着畫出一個簡單的圖, 用於表示 a 模塊依賴 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
,即 module visible 的意思。項目源碼位於 poloxue/modv。
先要檢查數據輸入管道是否正常。
咱們的目標是使用相似 dep
中做圖的方式,go mod graph
經過管道將數據傳遞給 modv
。所以,要先檢查 os.Stdin
,即檢查標準輸入狀態是否正常, 以及是不是管道傳輸。
下面是 main 函數的代碼,位於 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;
}
複製代碼
前面說過,這裏包含了兩種不一樣的結構,分別是模塊與 ID 關聯關係,以及模塊 ID 表示模塊間的依賴關聯。爲 ModGraph
結構體增長兩個成員表示它們。
type ModGraph struct {
r io.Reader // 數據流讀取實例,這裏即 os.Stdin
// 每一項名稱與 ID 的映射
Mods map[string]int
// ID 和依賴 ID 關係映射,一個 ID 可能依賴多個項
Dependencies map[int][]int
}
複製代碼
要注意的是,增長了兩個 map 成員後,記住要在 NewModGraph
中初始化下它們。
如何進行解析?
介紹到這裏,目標已經很明白了。就是要將輸入數據解析到 Mods
和 Dependencies
兩個成員中,實現代碼都在 Parse
方法中。
爲了方便進行數據讀取,首先,咱們利用 bufio
基於 reader
建立一個新的 bufReader
,
func (m *ModGraph) Parse() error {
bufReader := bufio.NewReader(m.Reader)
...
複製代碼
爲便於按行解析數據,咱們經過 bufReader 的 ReadBytes()
方法循環一行一行地讀取 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
兩個成員中。模塊 ID 是生成規則採用的是最簡單的實現方式,從 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 -}} } `
複製代碼
這一塊沒啥好介紹的,主要是要熟悉 Go 中的 text/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
}
複製代碼
如今,所有工做都完成了。最後,將這個流程整合到 main 函數。接下來就是使用了。
開始體驗下吧。補充一句,這個工具,我如今只測試了 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 Module 遷移的時候,這個小工具是否能產生一些價值。
歡迎關注個人微信公衆號。