关于Go Modules的常见疑惑与解答
关于Go Modules的常见疑惑与解答 以下是从 spf13/cobra 复制的一些代码示例
go.mod
module cobra-demo
go 1.15
require (
github.com/mitchellh/go-homedir v1.1.0
github.com/spf13/cobra v1.1.0
github.com/spf13/viper v1.7.1
)
main.go
package main
import (
"cobra-demo/cmd"
)
func main() {
cmd.Execute()
}
cmd/version.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(versionCmd)
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of Hugo",
Long: `All software has versions. This is Hugo's`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hugo Static Site Generator v0.9 -- HEAD")
},
}
cmd/root.go
package cmd
import (
"fmt"
"os"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
// Used for flags.
cfgFile string
userLicense string
rootCmd = &cobra.Command{
Use: "cobra",
Short: "A generator for Cobra based Applications",
Long: `Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
}
)
func Execute() error {
return rootCmd.Execute()
}
func init() {
cobra.OnInitialize(initConfig)
}
func er(msg interface{}) {
fmt.Println("Error:", msg)
os.Exit(1)
}
func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := homedir.Dir()
if err != nil {
er(err)
}
viper.AddConfigPath(home)
viper.SetConfigName(".cobra")
}
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}
运行 go mod tidy 后,go.sum 中会有一长串列表。
我在其中看到了 cloud.google.com/go,但是当我运行 go mod why cloud.google.com/go 时
$ go mod why cloud.google.com/go
# cloud.google.com/go
(main module does not need package cloud.google.com/go)
经过一番探究,我发现 viper 导入了 github.com/bketelsen/crypt,而 crypt 又导入了 cloud.google.com/go。
那么 github.com/bketelsen/crypt 和 cloud.google.com/go 是 cobra-demo 的间接依赖,对吗?
viper 依赖于 github.com/bketelsen/crypt,而 crypt 依赖于 cloud.google.com/go,那么为什么这个项目不需要 cloud.google.com/go 这个包呢?
运行 go mod vendor 后
λ tree -L 2 vendor
vendor
├── github.com
│ ├── fsnotify
│ ├── hashicorp
│ ├── inconshreveable
│ ├── magiconair
│ ├── mitchellh
│ ├── pelletier
│ ├── spf13
│ └── subosito
├── golang.org
│ └── x
├── gopkg.in
│ ├── ini.v1
│ └── yaml.v2
└── modules.txt
14 directories, 1 file
其中没有 cloud.google.com/go。
这是一个关于Go Modules间接依赖的典型问题。让我来解释一下具体原因。
在您的项目中,cloud.google.com/go确实是间接依赖,但Go Modules在go mod vendor时不会包含它,因为该依赖在当前构建环境下没有被实际使用。这通常发生在依赖包有可选功能或特定构建标签时。
让我们通过代码示例来验证这个情况:
// 查看viper的依赖关系
package main
import (
"fmt"
"golang.org/x/tools/go/packages"
)
func main() {
cfg := &packages.Config{
Mode: packages.NeedImports | packages.NeedDeps,
}
pkgs, err := packages.Load(cfg, "github.com/spf13/viper")
if err != nil {
panic(err)
}
for _, pkg := range pkgs {
fmt.Printf("Package: %s\n", pkg.ID)
for imp := range pkg.Imports {
fmt.Printf(" Import: %s\n", imp)
}
}
}
运行go mod graph可以更清楚地看到依赖关系:
# 查看完整的依赖图
go mod graph | grep cloud.google.com/go
# 或者查看特定包的依赖路径
go mod why -m all | grep cloud.google.com/go
github.com/bketelsen/crypt可能只在特定条件下导入cloud.google.com/go,例如:
// 在crypt包中可能有这样的条件导入
// +build gcp
package crypt
import (
_ "cloud.google.com/go/storage"
)
// 或者通过初始化函数
func init() {
// 只在特定环境变量设置时才注册GCP后端
if os.Getenv("USE_GCP") != "" {
registerGCPBackend()
}
}
这就是为什么go mod why显示不需要这个包,而go mod vendor也没有包含它。Go Modules足够智能,只包含当前构建配置下实际需要的依赖。
您可以通过以下方式验证:
// 创建一个测试文件来检查构建标签
// build_constraints.go
// +build ignore
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Printf("GOOS: %s\n", runtime.GOOS)
fmt.Printf("GOARCH: %s\n", runtime.GOARCH)
// 检查是否有特定构建标签生效
tags := []string{"gcp", "aws", "azure"}
for _, tag := range tags {
// 在实际代码中,这会影响导入
fmt.Printf("Would import for tag %s: %v\n", tag, false)
}
}
在实际构建时,Go工具链会根据当前环境的构建标签和条件编译指令来决定哪些依赖是真正需要的,这就是为什么某些间接依赖不会出现在vendor目录中。

