IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    为什么Canonical Import Path注释在Go中不再必要

    bigwhite发表于 2024-10-02 13:04:59
    love 0

    本文永久链接 – https://tonybai.com/2024/10/02/why-canonical-import-paths-no-longer-necessary-in-go

    Go语言自推出以来,一直以其简洁和高效的包管理系统著称。在Go 1.11版本之前,Canonical Import Path注释曾是一个重要的工具,用于防止包路径的导入冲突。然而,随着Go Modules的引入,这一工具的作用逐渐被淡化。那么Canonical Import Path注释是否还有必要存在呢?在这篇文章中,我就来介绍一下Canonical Import Path的历史及作用,并通过在Go Modules环境下的向后兼容性测试,讨论是否仍有必要继续使用这一注释。

    1. 什么是Canonical Import Path注释?

    Go在1.4版本中增加了Canonical Import Path,Canonical Import Path用于解决同一个包可能被通过多个导入路径导入的问题。比如当代码托管在像github.com这样的服务上时,导入路径会包含托管服务的域名,比如“github.com/rsc/pdf。但是Go开发者也可以为同一个包提供一个“自定义”或“vanity”导入路径,例如rsc.io/pdf。这样就会产生两个有效的导入路径,这会带来以下问题:

    • 同一个程序中可能会通过不同路径导入同一个包,造成不必要的重复。
    • 使用非官方路径时可能会错过包更新,因为路径没有得到正确识别。
    • 将包迁移到另一个托管服务时,可能会中断使用旧路径的客户端。

    为了解决这个问题,Go 1.4引入了Canonical Import Path注释。在包声明中加上注释后,如果通过非Canonical Import Path导入包,Go命令将拒绝编译导入包的程序。

    Canonical Import Path的语法很简单,在包声明的注释部分加上标识。例如,对于rsc.io/pdf包,声明可以写成:

    package pdf // import "rsc.io/pdf"
    

    这样,Go命令就会拒绝编译任何通过github.com/rsc/pdf路径导入的包,确保代码可以在不破坏用户代码的前提下自由迁移。

    2. Go Modules及其对导入路径的影响

    Go 1.11引入Go Modules后,Go通过go.mod文件管理包的依赖关系和版本,极大简化了包的管理过程。通过在go.mod中定义模块的根路径,Go Modules可以自动指示项目中所有包的导入路径,并且是唯一的,这使得Canonical Import Path在Go Modules环境下基本没什么必要性了。

    例如,假设go.mod文件定义了以下模块路径:

    // go.mod
    module rsc.io/pdf
    

    那么位于项目根目录下的包的导入路径将被自动解析为rsc.io/pdf,避免了包路径冲突问题。因此,在Go Modules的支持下,手动设置Canonical Import Path注释变得不再必要。

    Go提供了Go1向后兼容,在Go module下使用Canonical Import Path注释会是什么情况呢?我们接下来来看看。

    3. 在Go Modules下使用Canonical Import Path注释

    虽然Go Modules简化了包管理,很多老项目仍然保留了Canonical Import Path注释。为了验证在Go Modules环境下继续使用这些注释的兼容性,我进行了以下测试(测试环境使用的是包括Go 1.23.0版本在内的多个Go版本)。

    在这个测试中,我们保持项目中的Canonical Import Path注释不变,看看它是否影响在Go Modules环境中的编译和运行。

    这里我们直接使用位于github.com/rsc/pdf中的pdf包,该包在read.go文件中使用了Canonical Import Path注释:

    // https://github.com/rsc/pdf/blob/master/read.go
    package pdf // import "rsc.io/pdf"
    

    我们先用Go 1.11版本之前的Go版本测试一下导入rsc.io/pdf包。由于Go 1.11版本之前依然采用的是GOPATH构建模式,因此需要先将github.com/rsc/pdf下载到\$GOPATH/src的github.com/rsc下,因为GOPATH模式下,go编译器回到\$GOPATH路径下搜寻依赖包。

    接下来,我们建立demo1目录,并直接将github.com/rsc/pdf/pdfpasswd/main.go复制到demo1目录下,该main.go导入了”rsc.io/pdf”,我们将其改为导入”github.com/rsc/pdf”:

    // demo1/main.go
    
    package main
    
    import (
        "flag"
        "fmt"
        "log"
        "os"
    
        "github.com/rsc/pdf"
    )
    
    var (
        alphabet  = flag.String("a", "0123456789", "alphabet")
        maxLength = flag.Int("m", 4, "max length")
    )
    
    func usage() {
        fmt.Fprintf(os.Stderr, "usage: pdfpasswd [-a alphabet] [-m maxlength] file\n")
        os.Exit(2)
    }
    
    func main() {
        log.SetFlags(0)
        log.SetPrefix("pdfpasswd: ")
    
        flag.Usage = usage
        flag.Parse()
        if flag.NArg() != 1 {
            usage()
        }
    
        f, err := os.Open(flag.Arg(0))
        if err != nil {
            log.Fatal(err)
        }
    
        last := ""
        alpha := *alphabet
        ctr := make([]int, *maxLength)
        pw := func() string {
            inc(ctr, len(alpha)+1)
            for !valid(ctr) {
                inc(ctr, len(alpha)+1)
            }
            if done(ctr) {
                return ""
            }
            buf := make([]byte, len(ctr))
            var i int
            for i = 0; i < len(buf); i++ {
                if ctr[i] == 0 {
                    break
                }
                buf[i] = alpha[ctr[i]-1]
            }
            last = string(buf[:i])
            println(last)
            return last
        }
        st, err := f.Stat()
        if err != nil {
            log.Fatal(err)
        }
        _, err = pdf.NewReaderEncrypted(f, st.Size(), pw)
        if err != nil {
            if err == pdf.ErrInvalidPassword {
                log.Fatal("password not found")
            }
            log.Fatal("reading pdf: %v", err)
        }
        fmt.Printf("password: %q\n", last)
    }
    
    func inc(ctr []int, n int) {
        for i := 0; i < len(ctr); i++ {
            ctr[i]++
            if ctr[i] < n {
                break
            }
            ctr[i] = 0
        }
    }
    
    func done(ctr []int) bool {
        for _, x := range ctr {
            if x != 0 {
                return false
            }
        }
        return true
    }
    
    func valid(ctr []int) bool {
        i := len(ctr)
        for i > 0 && ctr[i-1] == 0 {
            i--
        }
        for i--; i >= 0; i-- {
            if ctr[i] == 0 {
                return false
            }
        }
        return true
    }
    

    然后,我们先用Go 1.10.8版本编译该main.go,得到下面结果:

    $go run main.go
    main.go:9:2: code in directory /Users/tonybai/Go/src/github.com/rsc/pdf expects import "rsc.io/pdf"
    

    我们看到go 1.11之前的版本对pdf包声明的Canonical Import Path做了检查,如果实际导入路径(github.com/rsc/pdf)与其不符,Go编译器会报错!

    接下来,我们来看看切换到go module模式后的编译结果,这里我们使用Go 1.12.7版本。我们创建go.mod文件:

    // demo1/go.mod
    module demo1
    
    go 1.12
    

    编译执行main.go:

    $go run main.go
    go: finding github.com/rsc/pdf v0.1.1
    go: downloading github.com/rsc/pdf v0.1.1
    go: extracting github.com/rsc/pdf v0.1.1
    usage: pdfpasswd [-a alphabet] [-m maxlength] file
    exit status 2
    

    我们看到,go 1.12.7可以成功编译并运行main.go,即便后者没有使用Canonical Import Path导入pdf包。

    而用最新的Go 1.23.0编译和运行,也是没问题的:

    $go run main.go
    usage: pdfpasswd [-a alphabet] [-m maxlength] file
    exit status 2
    

    由此可以得出结论:go module模式下,Go编译器已经不再校验导入包的Canonical Import Path了。

    并且,即便main.go同时导入rsc.io/pdf和github.com/rsc/pdf也是没问题的:

    import (
        "flag"
        "fmt"
        "log"
        "os"
    
        "github.com/rsc/pdf"
        _ "rsc.io/pdf"
    )
    

    这是因为github.com/rsc/pdf下没有go.mod,go编译器无法识别github.com/rsc/pdf和rsc.io/pdf是同一个包。我们再看一个uber-go/zap的例子:

    package main
    
    import (
        "fmt"
    
        _ "github.com/uber-go/zap"
        _ "go.uber.org/zap"
    )
    
    func main() {
        fmt.Println("hello, zap!")
    }
    

    针对这个main.go所在的go module进行go mod tidy,我们会得到如下错误结果:

    $go mod tidy
    go: finding module for package go.uber.org/zap
    go: finding module for package github.com/uber-go/zap
    go: downloading go.uber.org/zap v1.27.0
    go: downloading github.com/uber-go/zap v1.27.0
    go: found github.com/uber-go/zap in github.com/uber-go/zap v1.27.0
    go: found go.uber.org/zap in go.uber.org/zap v1.27.0
    go: demo imports
        github.com/uber-go/zap: github.com/uber-go/zap@v1.27.0: parsing go.mod:
        module declares its path as: go.uber.org/zap
                but was required as: github.com/uber-go/zap
    

    我们看到:go命令检测出了github.com/uber-go/zap仓库下的go module是go.uber.org/zap,我们只能使用go.uber.org/zap作为zap包的导入路径。

    4. 是否应移除Canonical Import Path注释?

    在Go Modules已经成为Go项目默认包管理方式的背景下,Canonical Import Path的使用显得冗余。虽然保留这些注释不会导致兼容性问题,但移除它们可以让项目代码更加简洁,减少不必要的历史包袱。

    对于已经迁移到Go Modules的老项目,开发者可以考虑逐步移除Canonical Import Path注释。对于新项目,则是没有必要添加Canonical Import Path注释,Go Modules已经足够强大,能够管理包路径和依赖;如果项目的用户仍依赖旧版Go工具链(GOPATH模式),保留Canonical Import Path注释则可以作为一种保险措施。

    5. 小结

    Canonical Import Path注释在Go 1.4引入时是为了解决包路径冲突和包迁移问题。然而,随着Go Modules的引入,包管理和路径控制功能逐渐被自动化,Canonical Import Path的作用显得不再必要。对于现代Go项目,开发者应考虑移除这一冗余的注释,这不仅是代码简化的一部分,也反映了Go生态系统中包管理方式的演进,并使项目更加符合Go语言的现代开发环境。


    Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

    img{512x368}
    img{512x368}

    img{512x368}
    img{512x368}

    著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

    Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

    我的联系方式:

    • 微博(暂不可用):https://weibo.com/bigwhite20xx
    • 微博2:https://weibo.com/u/6484441286
    • 博客:tonybai.com
    • github: https://github.com/bigwhite
    • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
    • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

    商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

    © 2024, bigwhite. 版权所有.



沪ICP备19023445号-2号
友情链接