詳解Go 依賴管理 go mod tidy

前言

go mod tidy的作用是把項目所需要的依賴添加到go.mod,並刪除go.mod中,沒有被項目使用的依賴。

Tidy makes sure go.mod matches the source code in the module.
It adds any missing modules necessary to build the current module's
packages and dependencies, and it removes unused modules that
don't provide any relevant packages. It also adds any missing entries
to go.sum and removes any unnecessary ones.

接下來我們將深入源碼研究go mod tidy的執行過程

  • 版本 go 1.18
  • 編輯器 vscode

Debug準備

源碼的位置

輸入命令行go env,找到GOROOT這一項(go的安裝路徑)

路徑${GOROOT}/src/cmd/go/internal/modcmd就是go mod命令相關的源碼瞭。其程序入口${GOROOT/src/cmd/go/main.go}進入該目錄(其實也可以不進,但是待會看源碼時還是得進去)執行以下命令go build -o ./godebug.exe -gcflags all="-N -l" -mod=mod .得到以下程序。

註:可以直接調試main.go這個文件,但是 go mod tidy這個命令是根據執行命令時的工作路徑查找go.mod文件,這無形指定瞭工作路徑為:${GOROOT}/src/cmd

debug 配置文件

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "GO debug",
            "type": "go",
            "request": "attach",
            "mode": "remote",
            "host": "127.0.0.1",
            "port": 2345,
        }
    ]
}

dlv啟動

隨便進一個go項目中,執行命令dlv exec --headless --listen :2345 --api-version=2 D:/go/src/cmd/go/godebug.exe -- mod tidy,這裡的程序是我們上面剛剛編譯出來的,路徑自己CV就行。

現在dlv服務端已經ok瞭,現在回到源碼那邊,啟動客戶端就行瞭。

如果走的exec,好像是沒法vscode的restart按鈕貌似不起作用,每次都需要重復執行以上兩個動作

執行過程

入口${GOOROOT}/src/cmd/go/internal/modcmd/tidy.go,該方法隻是做瞭一些參數配置,主要邏輯在modload.LoadPackages

func runTidy(ctx context.Context, cmd *base.Command, args []string) {
        ...
	modload.LoadPackages(ctx, modload.PackageOpts{
		GoVersion:                tidyGo.String(),
		Tags:                     imports.AnyTags(),
		Tidy:                     true,
		TidyCompatibleVersion:    tidyCompat.String(),
		VendorModulesInGOROOTSrc: true,
		ResolveMissingImports:    true,
		LoadTests:                true,
		AllowErrors:              tidyE,
		SilenceMissingStdImports: true,
	}, "all")
}
  • 加載項目go.mod的文件內容
  • 構建整個項目的依賴關系
  • 更新go.mod文件
// {GOROOT}/src/cmd/go/internal/modload/load.go
func LoadPackages(參數省略)(參數省略) {
	...
        //  加載項目go.mod的文件內容
	initialRS := LoadModFile(ctx)
	...
}

加載go.mod文件

1.根據執行go mod tidy時所在的工作路徑,向上查找最先找到的go.mod文件,讀取並解析該文件內容。

// ${GOROOT}/src/cmd/go/internal/modload/init.go
func LoadModFile(ctx context.Context) *Requirements {
	...
	// 做一些初始化的設置,獲取當前項目的go.mod路徑
	// 執行go mod tidy 是的工作路徑往上一層層尋找,找到的第一個路徑即為目標路徑
        // 查找路徑的調用棧`Init() => findModuleRoot(base.Cwd())`
	Init()
	...
	// 讀取go.mod文件並解析該文件內容;modRoots的長度為1,大於1的情況我沒有遇到過
	for _, modroot := range modRoots {
		gomod := modFilePath(modroot)
		data, f, err := ReadModFile(gomod, fixVersion(ctx, &fixed))
	}
	...
	// 隻獲取go.mod文件中的require列表,並記錄每個依賴的最高版本號
	rs := requirementsFromModFiles(ctx, modFiles)
	...
	// 如果發現當前的go.mod文件有重復的依賴路徑
        // 這裡會先對當前項目的go.mod文件進行一次依賴項的計算
	if rs.hasRedundantRoot() {
        // If any module path appears more than once in the roots, we know that the
        // go.mod file needs to be updated even though we have not yet loaded any
        // transitive dependencies.
		...
	}
	...
}

加載依賴

// {GOROOT}/src/cmd/go/internal/modload/load.go
func LoadPackages(...) (...) {
    // 找出項目的所有依賴,有個全局變量負責最後的存儲的
    ld := loadFromRoots(ctx, loaderParams{
        PackageOpts:  opts,
        requirements: initialRS,
        allPatternIsRoot: allPatternIsRoot,
        listRoots: func(rs *Requirements) (roots []string) {
            // 實際上調用的是 matchPackages() 方法
            updateMatches(rs, nil)
            // 這裡的matches長度也是1個
            for _, m := range matches {
                roots = append(roots, m.Pkgs...)
            }
            return roots
        },
    })
}
  • 獲取遍歷樹的根節點(當前項目的所有滿足條件的文件夾路徑)loadFromRoots()=> listRoots() => matchPackages()
// ${GOROOT}/src/cmd/go/internal/modload/search.go
func matchPackages(...) {
    // 遍歷項目根路徑
    walkPkgs := func(root, importPathRoot string, prune pruning) {
        ...
        // 這裡的root為go.mod所在的目錄,importPathRoot為go.mod定義的module
        err := fsys.Walk(root, func(path string, fi fs.FileInfo, err error) error {
            // 一大堆判斷過濾
            ...
            // 包名 = moduleName + 相對路徑
            name := importPathRoot + filepath.ToSlash(path[len(root):])
            if _, _, err := scanDir(path, tags); err != imports.ErrNoGo {
                m.Pkgs = append(m.Pkgs, name)
            }
            return nil
        })
    }
    // 同樣的這裡modules也隻有1個,多個的沒遇到過
    for _, mod := range modules {
        walkPkgs(root, modPrefix, prune)
    }
    return
}
  • 從項目跟路徑出發構建依賴關系
// {GOROOT}/src/cmd/go/internal/modload/load.go
func loadFromRoots(ctx context.Context, params loaderParams) *loader {
    ...
    // 註這裡是多次循環的過程
    // a=>b,隻有當b加載後才能知道是否有b=>c,b=>d。
    // 所以這裡會不斷的重復這個過程,直至所有的依賴關系構建完畢
    for {
        ld.reset()
        ...
        // 找出項目下的文件夾路徑,這裡的rootPkgs每次循環都是一樣的
        rootPkgs := ld.listRoots(ld.requirements)
        ...
        // 從根路徑出發,遍歷全部的文件,獲取依賴關系
        // 在加載依賴A的同時,會根據依賴A裡面的go.mod繼續去找依賴B
        // 如果發現項目中有直接引用依賴A,但是當前項目的go.mod沒有(前面加載過,存放在ld.requirements),
        // 則會給該pkg一個err(這裡不是module,是module裡面的某個包,例如 A/xxxx,A/yyy),
        // 這裡會交由ld.resolveMissingImports去處理
        for _, path := range rootPkgs {
            // 這裡是並發加載,速度還是比較快的
            // 主要的邏輯在在ld.load方法上
            root := ld.pkg(ctx, path, pkgIsRoot)
            if !inRoots[root] {
                ld.roots = append(ld.roots, root)
                inRoots[root] = true
            }
        }
        // 這個隻是將依賴樹給平鋪瞭存放在 ld.pkgs
        ld.buildStacks()
        ...
        // 某種程度上, 可以認為這裡下載的是缺失的直接依賴,即go.mod裡面沒聲明,但是項目卻使用到瞭的
        // 如果發現沒有缺失的直接依賴瞭,即可認為依賴關系已經構建完畢。
        // 因為上述過程會自動構建依賴關系,這裡隻是添加缺失的直接依賴,然後由上面的循環來構建依賴關系
        modAddedBy := ld.resolveMissingImports(ctx)
        if len(modAddedBy) == 0 {
            break
        }
    }
    ...
}
// 因為構建的是整個依賴關系,所以上述過程完成後,項目中不需要的依賴也已經自動剔除瞭
  • 加載一個單獨的pkg
// {GOROOT}/src/cmd/go/internal/modload/load.go
func (ld *loader) load(ctx context.Context, pkg *loadPkg) {
	... 
	// 找出pkg的module及其所在目錄
	pkg.mod, pkg.dir, pkg.altMods, pkg.err = importFromModules(ctx, pkg.path, ld.requirements, mg)
	if pkg.dir == "" {
		return
	}
	...
	// 掃描文件獲取所有的import
	// 這裡是一個pkg的所有import
	/* 
	例如: 
		A/B/xxx.go 
			import "11111"
		A/B/yyy.go
			import "22222"
	則 pkg
		import "1111"
		import "2222"
	*/ 
	imports, testImports, err = scanDir(pkg.dir, ld.Tags)
	...
	// 遞歸執行 ld.pkg 組裝下數據結構,又回來繼續調用 ld.load
	for _, path := range imports {
		pkg.imports = append(pkg.imports, ld.pkg(ctx, path, importFlags))
	}
}

更新go.mod文件

這裡就比較簡單瞭,就是單純的寫文件而已。在第二過程的時候已經將依賴關系構建完成瞭,其結果存放在一個全局變量裡面MainModules,這裡就是單純校驗寫文件瞭

func LoadPackages(...) (...) {
	...
	if err := commitRequirements(ctx); err != nil {
		base.Fatalf("go: %v", err)
	}
	...
}

以上就是詳解Go 依賴管理 go mod tidy的詳細內容,更多關於Go 依賴管理 go mod tidy的資料請關註WalkonNet其它相關文章!

推薦閱讀: