选择正确的Go Module Path

本文永久链接 – https://tonybai.com/2024/04/09/choose-the-right-go-module-path

最近我在查看项目代码时,注意到有人在go.mod文件中将module path写为com.example.foo了。根据这个写法,相信屏幕前的读者也可以推断出这位开发人员可能是从Java阵营转到Go的。实际开发中可能有很多开发者会使用类似的内容作为module path,但这显然不是Go的推荐写法或惯用法。

在这篇简短的文章中,我就来介绍一下module path对Go源码构建、包导入路径以及开发协作的影响,以及符合惯例的module path应该是什么样子的。

我们先来复习一下什么是Go module path。

1. 什么是module path

在Go语言中,module path(模块路径)是指在Go开发中用来标识和定位模块的唯一字符串,用于指定在远程仓库或本地文件系统中存储模块代码的位置。

module path在go.mod文件中定义,比如下面这个示例:

// go.mod
module github.com/user/module

go 1.21.1

我们看到:一个典型的模块路径是一个URL格式字符串,可能是类似于github.com/user/module的形式,其中github.com/user/module就是module path。

在Go语言中,模块(module)是一种组织和管理代码的方式,也是Go代码版本管理的基本单元,我们可以在模块路径中包含主版本信息,比如:

// go.mod
module github.com/user/module/v2

go 1.21.1

这表明该模块为v2版本,与前面的github.com/user/module是不向后兼容的两个模块。模块的使用者可以同时导入这两个不兼容的模块下的包,比如:

import (
    "github.com/user/module/foo"
    foov2 "github.com/user/module/v2/foo"
)

那么module path的选取和使用,对Go开发有何影响呢?我们继续向下看。

2. module path的影响

2.1 指示Go module网络位置

前面提到过,在Go语言中,我们通常使用模块的存储库地址作为模块路径的基础。这样做的好处是,Go编译器可以直接通过模块路径确定模块在网络上的位置,并从指定的位置下载需要的代码。这使得在使用第三方模块时非常方便,开发者只需要指定模块的路径,Go工具链就能够自动处理依赖关系,下载并编译所需的模块代码。

例如,如果一个模块的路径是github.com/user/module,那么Go工具链(尤其是Go编译器)就会认为该模块的代码存储在GitHub上的user用户下的module仓库中。当Go工具链需要引入该模块时,它会根据这个路径通过goproxy或直接去GitHub上下载相应的代码。

这种基于存储库地址的模块路径设计简化了模块的管理和依赖关系的处理,使得在Go项目中使用第三方模块变得更加方便和可靠。

2.2 对Go包路径的影响

Go module下的包的导入路径为module path+到包所在目录的相对路径,以module path为github.com/user/module的module下的pkg/foo目录下的包为例,foo包的导入路径为github.com/user/module/pkg/foo。

而如果像本文开头那样,使用com.example.foo作为module path,那么foo包的导入路径就变为了com.example.foo/pkg/foo,这显然难以理解,同时,com.example.foo这样的Java模式的字符串也无法指示go module的网络位置。

2.3 对编译的影响

module path对编译的影响体现在两方面:

首先,Go编译时通过module path来查找依赖的模块。如果Go module path不正确或不完整,那么编译可能会失败。非idiomatic的Go module path可能导致编译错误或难以诊断的问题。

其次,module path会影响采用go build默认构建出的二进制文件的名字,比如如果一个module path为github.com/user/mymodule,那么在该module下执行go build(不使用-o命令行标志),默认得到的二进制文件名为mymodule。

但如果module path为com.example.foo,那么得到的二进制文件名就为com.example.foo,这显然不是我们想要的。

2.4 对开发者协作的影响

Go模块路径的命名对开发者之间的协作也有着重要的影响,主要体现在两方面:

  • 唯一性和命名空间

模块路径应当保持唯一,以避免与其他模块产生冲突。通常情况下,使用域名作为模块路径的一部分可以确保全球唯一性。在团队内部,也可以基于公司或组织的名称来命名模块路径,以确保模块的唯一性。

  • 依赖管理

使用清晰、有意义、可以指示位置和版本的模块路径可以帮助开发者更好地管理依赖关系。当其他开发者在引入你的模块时,他们可以通过模块路径来确定正确的依赖版本,以及如何与你的模块进行集成。

3. 如何选择一个好的module path

通过上面的秒数,其实我们已经可以勾勒出一个好的module path的画像了。当然这也是Go社区的最佳实践。

通常情况下,module path应该基于模块的存储库地址,并使用简短、易于理解的路径。

就像前面提到的那样,如果你的module存储在GitHub上并可公开,那么module path一般是github.com/user/module。

如果你的module公司内部,不能公开的,那么可以使用一个私有的存储库地址,例如:company.com/dept/go/module。

无论公开的,还是私有的,你都可以定制module path,这方面的方案可以参考我之前编写的有关定制Go module的拉取方案

如果是仅在本地使用的日常练习项目,那么Go module path的使用可以宽松一些,可以无需在乎其对go module网络位置、开发者协作的影响,可使用像demo这样的单个词的module path,仅注意下其对包路径和编译结果的影响即可。

4. 小结

综上,我们看到:Go module path对Go module网络位置、包路径、编译和开发者协作都有重要影响。遵循Go社区的最佳实践,选择一个好的Go module path可以提高代码可读性和可维护性,并简化多人协作,帮助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://gopherdaily.tonybai.com

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

Go 1.22引入的包级变量初始化次序问题

本文永久链接 – https://tonybai.com/2024/03/29/the-issue-in-pkg-level-var-init-order-in-go-1-22

细心的朋友可能已经注意到,从春节后,我的博客就“停更”了!实际上,这一情况部分是因为工作上的事务繁忙,另一部分则是因为我将工作之外的闲暇时间更多地投入到一本即将于今年中下旬出版的书的撰写了:在之前的积累基础上,我花了两个多月的时间完成了初稿。

当然,我也深切地怀念博客写作所带来的乐趣和与读者的互动。正巧,今天一位学员在《Go语言第一课》专栏留言给了我一个恢复下笔的机会。借此,我也准备恢复一下博客写作的节奏。

另外预告一下:我和我的技术团队合作翻译的一本Go语言入门书最早也将于2024年4月份上市,敬请期待


《Go语言第一课》专栏第8讲中,我曾系统讲解了Go包的初始化次序,以及Go包内包级变量、常量、init函数等的初始化次序。讲这些的初衷就是希望Go初学者能先了解一下Go程序的执行次序,这样在后续阅读和理解Go代码的时候,就好比拥有了“通往宝藏的地图”,可以直接沿着Go代码执行次序这张“地图”去阅读和理解Go代码,而不会在庞大的代码库中迷失了。

相对于早期的Go版本,Go包的初始化次序在Go 1.21版本开始会有所变化,这个可以看我的《Go 1.21中值得关注的几个变化》一文了解详情。

不过除了Go包的初始化次序得以明确之外,Go在1.22版本中的包级变量初始化次序也发生了一些“变化”,但Go 1.22的Release Notes压根没提到Go包内的变量初始化次序会有变化。究竟这些变化是有意为之,还是由于代码变更而引入的新问题呢?我们还得从近期《Go语言第一课》专栏的一位读者提出的问题讲起!

1. Go 1.22的输出结果与专栏文章中不同!

原专栏中的代码较多,为方便起见我又写了一段简化版的代码,可以等价地反映问题。下面的代码用于演示包级变量、常量和init函数的初始化次序:

// initorder.go
package main

import (
    "fmt"
)

var (
    v0 = constInitCheck()
    v1 = variableInit("v1")
    v2 = variableInit("v2")
)

const (
    c1 = "c1"
    c2 = "c2"
)

func constInitCheck() string {
    if c1 != "" {
        fmt.Println("main: const c1 has been initialized")
    }
    if c1 != "" {
        fmt.Println("main: const c2 has been initialized")
    }
    return ""
}

func variableInit(name string) string {
    fmt.Printf("main: var %s has been initialized\n", name)
    return name
}

func init() {
    fmt.Println("main: first init func invoked")
}

func init() {
    fmt.Println("main: second init func invoked")
}

func main() {
    // do nothing
}

使用Go 1.22版本之前的版本,比如Go 1.21版本,运行该程序的输出结果如下:

$go run initorder.go
main: const c1 has been initialized
main: const c2 has been initialized
main: var v1 has been initialized
main: var v2 has been initialized
main: first init func invoked
main: second init func invoked

这个输出结果也是专栏文章中的输出结果,即包级元素的初始化顺序是:常量 -> 变量 -> init函数。三个变量的初始化次序是v0 -> v1 -> v2。

但专栏的一位读者在使用最新Go 1.22版本运行上述程序后,却提出了如下问题:

总结一下这个问题的两个关键点如下:

  • Go 1.22版本运行上述程序的输出结果与文章中的结果不一致
  • 将const声明block搬移到var声明block的前面后,使用Go 1.22版本的输出结果与文章中的一致

我们先来复现一下问题。我使用Go 1.22.0运行上面的initorder.go,得到下面结果:

$go run main.go
main: var v1 has been initialized
main: var v2 has been initialized
main: const c1 has been initialized
main: const c2 has been initialized
main: first init func invoked
main: second init func invoked

该输出结果确如读者所说,与文中的输出顺序不一致了,变量的初始化次序变为了v1 -> v2 -> v0。这会让很多读者误以为包内元素的初始化次序变成了“变量 -> 常量 -> init函数”。是否真的如此了呢?我们下面来初步分析一下。

2. 原因初步分析

Go语言规范中对包内变量初始化次序的说明是这样的(截至2024.03):

Within a package, package-level variable initialization proceeds stepwise, with each step selecting the variable earliest in declaration order which has no dependencies on uninitialized variables. More precisely, a package-level variable is considered ready for initialization if it is not yet initialized and either has no initialization expression or its initialization expression has no dependencies on uninitialized variables. Initialization proceeds by repeatedly initializing the next package-level variable that is earliest in declaration order and ready for initialization, until there are no variables ready for initialization. Multiple variables on the left-hand side of a variable declaration initialized by single (multi-valued) expression on the right-hand side are initialized together: If any of the variables on the left-hand side is initialized, all those variables are initialized in the same step. For the purpose of package initialization, blank variables are treated like any other variables in declarations.

粗略翻译后大致意思如下:

在包内,包级变量初始化逐步进行,每一步都会选择声明顺序中最早的且不依赖于未初始化变量的那个变量。更准确地说,如果包级变量尚未初始化并且没有初始化表达式或其初始化表达式不依赖于未初始化的变量,则认为该变量具备初始化条件。通过重复初始化声明顺序中最早且具备初始化条件的下一个包级变量来进行初始化,直到没有具备初始化条件的变量为止。由右侧单个(多值)表达式初始化的变量声明左侧的多个变量会一起初始化:如果左侧的任何变量被初始化,则所有这些变量都会被初始化在同一步骤中。出于包初始化的目的,空变量也被视为与声明中的任何其他变量一样。

按照Go语言规范的描述,我们来理论推导一下v0、v1和v2的初始化次序:

var (
    v0 = constInitCheck()
    v1 = variableInit("v1")
    v2 = variableInit("v2")
)
  • 第一轮:待初始化的包级变量集合{v0, v1, v2}。在这一轮,我们按声明顺序逐一看一下这三个变量。

v0未初始化,其声明语句的右侧有初始化表达式(initialization expression),且这个初始化表达式式(constInitCheck)不依赖未初始化的变量(仅仅依赖两个常量c1和c2),因此按照Spec描述,v0具备初始化条件,会先进行初始化,于是constInitCheck会被调用。

  • 第二轮:待初始化的包级变量集合{v1, v2}。

按声明顺序,先看v1。和v0一样,其声明语句的右侧有初始化表达式,且这个初始化表达式式(variableInit)不依赖未初始化的变量,因此按照Spec描述,v1具备初始化条件,会进行初始化,于是variableInit会被调用。

  • 第三轮:待初始化的包级变量集合{v2}。

这个没啥可推导的了,初始化v2就是了!

这样,包级变量的声明次序就应该是v0 -> v1 -> v2。这个理论推导结果显然与Go 1.22版之前的输出结果是一致的。但与Go 1.22版本的输出结果有悖。

那么Go 1.22版本为什么没有将v0作为第一个具备初始化条件的变量对其进行初始化呢?v0有初始化表达式constInitCheck,该函数没有依赖任何未初始化的包级变量,但该函数内部依赖了两个常量c1和c2:

func constInitCheck() string {
    if c1 != "" {
        fmt.Println("main: const c1 has been initialized")
    }
    if c1 != "" {
        fmt.Println("main: const c2 has been initialized")
    }
    return ""
}

我们大胆地猜测一下:Go 1.22版本将c1和c2当成了“未初始化的变量”了!还记得读者问题的第二个关键点吗:“将const声明block搬移到var声明block的前面后,使用Go 1.22版本的输出结果便与文章中的一致”。按照Go 1.22的逻辑,将常量声明放到前面后,按顺序常量先被初始化了。这样到v0时,v0具备初始化的条件就成立了,于是v0就可以先被初始化了。

3. “一波三折”的issue

为了证实上述推测,我在github.com/golang/go提了issue 66575,并对上述问题做了阐述,不过该issue被Go团队的年轻成员Sean Liao“闪电”关闭了。

好在几个小时后,Go大神Keith Randall看到了这个issue,并支持了我的猜测!他还闪电般地找出了导致Go 1.22版本出现此问题的commit,并给出了fix方案:cmd/compile: put constants before variables in initialization order。fix方案的思路就是将所有常量的初始化放到变量之前。

fix merge到主干后,Gobot自动关闭了该issue。

但严谨的Keith Randall随后reopen了该issue,并圈了Go语言之父的Robert Griesemer,希望后者确定一下是否需要更新一下Go spec。

目前该issue已经被加入Go 1.23 milestone,并会在Go 1.23 fix。


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

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

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats