标签 gomodule 下的文章

Go 1.23中值得关注的几个变化

本文永久链接 – https://tonybai.com/2024/08/19/some-changes-in-go-1-23

距离上一次Go 1.22版本发布又过去六个月了,我们如期迎来了Go 1.23版本的发布

对于Go项目乃至整个Go社区而言,这个版本还有一点额外的意义,那就是这是Russ Cox作为Tech lead,领导Go团队发布的最后一个Go版本了。

8月2日,Russ Cox在golang-dev google group上发文,在领导了Go项目12年后,决定辞去Tech Lead,并将这一角色“传位”给Austin Clements,后者是现任Go core领域(围绕编译器工具链、运行时和发布)的Leader。Cherry Mui将递补,成为Go core领域的新Leader。

注:除了Go core外,Go项目下面还有两个领域的子团队,分别是由Roland Shoemaker领导的Go安全团队(Go Security)和由Rob Findley、Hana Kim共同领导的Go工具链和IDE支持团队。

注:Austin曾在Google担任实习生,在攻读博士学位期间参与了Go项目的早期工作。后来(2014年),他加入了Go 团队,与Rick Hudson合作完成了Go的并发垃圾回收。他还曾参与了当前的抢占式调度器和链接器的开发工作。现在,他领导着Go的编译器/运行时团队。– 来自golang.design

长相有些神似“马特达蒙”的Russ Cox经常活动于GopherCon之类的技术大会上,照片和视频比较多,但Austin和Cherry似乎都很神秘,很少出镜。Cherry Mui居然还是一个巾帼女汉子。如果你和我一样,不是很了解Austin,可以看看这个Austin在GopherCon 2020上的这个视频

在Russ Cox的领导下,Go如今已经成为云原生领域的基石语言,在我的《都2024年了,当初那个“Go,互联网时代的C语言”的预言成真了吗?》那篇文章中,我谈到Go建立了云原生时代的整体技术栈,地位媲美单机时代的C语言。Go在各大编程语言排行榜的位次也一直在提升,今年Go在TIOBE上最高已经冲到了第七名。在语法特性和工具链方面,Russ Cox带领Go团队先后实现了Go module、Go泛型等重要变化的落地。Go已经证明了自己的成功。

但俗话说:“船大难掉头”!随着Go语言的成熟,用户的不断增多,生态的不断扩大,如何把控好Go这艘大船,持续在正确的方向上航行,便逐渐成为了摆在Go团队面前的一个极具挑战性的问题。另外,在Go演进的过程中,质疑声也从来就没有中断过,尤其是在Go module、Go泛型等提案落地的过程中。Go 1.23引入的自定义函数iterator也曾一度将Go抛上风口浪尖,一些人批评Go忘记了简单的原则,正在走向错误的演进方向上。甚至还出现了Go已经过了流行的顶峰的观点:

这些也同样是Russ Cox留给Austin Clements等新一代决策层的“课题”。

言归正传!让我们来看看Go 1.23版本都有哪些重要的变化吧!

注:在两个多月,我曾写了一篇《Go 1.23新特性前瞻》,如果当时的新变化的实现与如今Go 1.23正式版是一致的,在本文中我就不会再详细说明了,大家可以移步那篇文章了解。

1. 语言变化

Go 1.23中最大的语言变化就是将Go 1.22中引入的试验特性:range-over-func变为了正式特性。我么就先从这个变化说起。

1.1 自定义函数迭代器

一旦你接受了泛型,迭代器就会不可避免地出现 — https://changelog.com/gotime/325

迭代器(iterator)是一个用于遍历集合类型的基本语言构造,例如切片、数组、map等。它是一种获取集合中的下一个item的机制,并会检查集合中是否还有其他内容,如果没有了,它会停止继续迭代。这种语言构造并非Go专属的,我们在许多语言中都能找到它,比如:Python、Java等。

Go 1.18版本加入了泛型支持,有了泛型后,各种使用泛型实现的集合类型便如“雨后春笋”般出现了。但Go的for range原生并不支持对这些集合类型的迭代,于是对自定义函数迭代器类型的需求便自然而然的出现了。

Go 1.23支持自定义迭代器后,for range的语法规格变为如下形式:

我们看到:for range继Go 1.22增加对整型表达式的支持后,在Go 1.23中又增加了对三种形式的自定义函数迭代器的支持。下面是Go spec中关于带有单个参数(fibo)和带有两个参数的函数迭代器(Walk)的示例:

// fibo generates the Fibonacci sequence
fibo := func(yield func(x int) bool) {
    f0, f1 := 0, 1
    for yield(f0) {
        f0, f1 = f1, f0+f1
    }
}

// print the Fibonacci numbers below 1000:
for x := range fibo {
    if x >= 1000 {
        break
    }
    fmt.Printf("%d ", x)
}
// output: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

// iteration support for a recursive tree data structure
type Tree[K cmp.Ordered, V any] struct {
    left, right *Tree[K, V]
    key         K
    value       V
}

func (t *Tree[K, V]) walk(yield func(key K, val V) bool) bool {
    return t == nil || t.left.walk(yield) && yield(t.key, t.value) && t.right.walk(yield)
}

func (t *Tree[K, V]) Walk(yield func(key K, val V) bool) {
    t.walk(yield)
}

// walk tree t in-order
var t Tree[string, int]
for k, v := range t.Walk {
    // process k, v
}

初看这个示例,for range的形式很简洁,且循环体内部对获得的item的处理也没有受到任何影响。函数迭代器的复杂性更多放在了提供迭代器的集合类型的作者那里了。作为要提供自定义迭代器的集合类型作者,你需要弄清楚迭代器的运作原理,尤其要记住要满足何种函数签名,才能更好地提供迭代器的实现,这的确会带来一些复杂性,并且初期编写时,你可能会反复参考Go Spec文档。至于迭代器的运作原理和典型使用方法,在不久前写的一篇《Go 1.23中的自定义迭代器与iter包》中,我对Go 1.23新增的迭代器做了一个系统的梳理,感兴趣的童鞋可以移步那篇文章阅读,这里就不另花笔墨了。

1.2 别名中增加泛型参数

但凡涉及type alias的提案或多或少都会有一定的争议,这次也不例外。Matthew Dempsky于2021年提出的issue: spec: generics: permit type parameters on aliases历经多年,几百次的讨论,才最终在Go 1.23中作为实验性特性引入。也许也正是这种缓慢而稳定的方法才是Go标准库和Go社区发展过程中真正令人印象深刻的地方。不过,目前除了这个issue中的内容,尚未有类似experimental wiki之类的资料可循。

那什么是带有类型参数的type alias呢?我们看看下面这个示例:

// go1.23-examples/lang/generic_type_alias.go 

package main

import "fmt"

type MySlice[T any] = []T

func main() {
    // 使用int类型实例化MySlice
    intSlice := MySlice[int]{1, 2, 3, 4, 5}
    fmt.Println("Int Slice:", intSlice)

    // 使用string类型实例化MySlice
    stringSlice := MySlice[string]{"hello", "world"}
    fmt.Println("String Slice:", stringSlice)

    // 使用自定义类型实例化MySlice
    type Person struct {
        Name string
        Age  int
    }

    personSlice := MySlice[Person]{
        {Name: "Alice", Age: 30},
        {Name: "Bob", Age: 25},
    }

    fmt.Println("Person Slice:", personSlice)
}

我们需要Go 1.23.0及以上版本可以编译该程序,并且还需要在命令前加上:GOEXPERIMENT=aliastypeparams。

执行上述程序的结果如下:

$GOEXPERIMENT=aliastypeparams go build generic_type_alias.go
$./generic_type_alias
Int Slice: [1 2 3 4 5]
String Slice: [hello world]
Person Slice: [{Alice 30} {Bob 25}]

怎么理解带有类型参数的类型别名呢?参考Russ Cox在issue的comment给出的理解,我们可以将其看成是一种“类型宏”(类似c中的#define),以该示例为例:

type MySlice[T any] = []T

就是在任何出现MySlice[T]的地方,将其换成[]T。我们再看一个复杂的例子:

// go1.23-examples/lang/pairs.go 

package main

import "fmt"

// 使用多个类型参数的类型别名
type Pair[T, U any] = struct {
    First  T
    Second U
}

// 使用Pair类型别名
func MakePair[T, U any](first T, second U) Pair[T, U] {
    return Pair[T, U]{First: first, Second: second}
}

// 交换Pair中的元素
func SwapPair[T, U any](p Pair[T, U]) Pair[U, T] {
    return Pair[U, T]{First: p.Second, Second: p.First}
}

func main() {
    // 创建一个int和string的Pair
    intStringPair := MakePair(42, "Answer")
    fmt.Printf("Int-String Pair: %+v\n", intStringPair)

    // 创建一个float64和bool的Pair
    floatBoolPair := Pair[float64, bool]{First: 3.14, Second: true}
    fmt.Printf("Float-Bool Pair: %+v\n", floatBoolPair)

    // 使用自定义类型
    type Person struct {
        Name string
        Age  int
    }
    personStringPair := MakePair(Person{Name: "Alice", Age: 30}, "Developer")
    fmt.Printf("Person-String Pair: %+v\n", personStringPair)

    // 交换Pair中的元素
    swappedPair := SwapPair(intStringPair)
    fmt.Printf("Swapped Int-String Pair: %+v\n", swappedPair)

    // 使用类型推断
    inferredPair := MakePair("Hello", 123)
    fmt.Printf("Inferred Pair: %+v\n", inferredPair)
}

我们可以在任何出现Pair[T, U any]的地方将其换为

struct {
    First  T
    Second U
}

编译运行上述代码,可得到如下结果:

$GOEXPERIMENT=aliastypeparams go run pairs.go
Int-String Pair: {First:42 Second:Answer}
Float-Bool Pair: {First:3.14 Second:true}
Person-String Pair: {First:{Name:Alice Age:30} Second:Developer}
Swapped Int-String Pair: {First:Answer Second:42}
Inferred Pair: {First:Hello Second:123}

Russ Cox还提到,利用该aliastypeparams机制,还可以用于缩短命名,比如:

type lexer[T any] = func(string, int) (T, int, bool)

当然Go 1.9引入type alias是为了重构,而aliastypeparams机制也可以很好的帮助重构,比如下面这个定义:

type T1[X, Y any] = T2[X, Y, defaultZ]

即如果已经有了T1[X, Y],然后意识到需要另一个参数,并将其泛型化为T2[X, Y, Z],这时使用上面的语句可以保持旧代码的正常运行。

此外,如果类型别名的约束更严格呢,比如下面的类型别名定义:

type MySlice[T any] = []T
type YourSlice[T comparable] = MySlice[T]

这里YourSlice的类型参数约束要求是comparable,比MySlice的any更严格,我们来看一下这个comparable会有效么?

// go1.23-examples/lang/strict_alias.go 

package main

import "fmt"

type MySlice[T any] = []T
type YourSlice[T comparable] = MySlice[T]

func main() {
    // 使用int类型实例化MySlice
    intSlice := YourSlice[int]{1, 2, 3, 4, 5}
    fmt.Println("Int Slice:", intSlice)

    intsliceSlice := YourSlice[[]int]{
        []int{1, 2, 3},
        []int{4, 5, 6},
    }
    fmt.Println("IntSlice Slice:", intsliceSlice)
}

我们知道int切片类型是不满足comparable的,但这个示例代码在目前Go 1.23.0版本是可以正常编译运行的。

最后,该aliastypeparameter实验特性会将类型别名定义局限在同一个包中,尚不支持跨多个包使用。

2. 工具链

在工具链方面,Go 1.23的变化都很实用!我们逐一挑重点变化来看一下。

2.1 Telemetry(遥测)

Go Telemetry是一个用于Go工具链程序收集性能和使用数据的系统。它适用于Go团队维护的开发者工具,如go cmd、gopls和govulncheck。

Russ Cox关于Go telemetry的构思始于2023年2月,他先是在个人博客发表一系列关于Go telemetry的思路和设计方案,然后又在Go项目建立disscusion和社区探讨这个idea。

在2023年GopherCon大会上,Russ Cox代表Go团队做了名为“Go Changes”的主题演讲,明确了Go的演进将是基于数据驱动的,而数据来源除了来自官方的年度用户调查、用户交谈、对已发布的go module的代码阅读分析之外,Go团队计划在Go工具链中加入Telemetry。telemetry可以帮助Go团队改进Go语言和工具,了解Go工具链使用情况和问题并提供比GitHub问题或年度用户调查更详细、及时的数据。

随着Go 1.23的发布,telemetry作为go cmd的sub command正式落地。

Go Telemetry由telemetry模式控制,有三种可能的值:

  • local(默认): 收集数据并存储在本地计算机上,但不上传;
  • on: 收集数据,并可能根据采样上传;
  • off:不收集也不上传数据。

你可以通过go env GOTELEMETRY查看当前模式。你可以通过go telemetry on|off|local来选择使用哪种模式。

Go Telemetry使用计数器来收集数据,它主要有两类计数器:

  • 基本计数器:记录命名事件的次数
  • 栈计数器:记录事件次数和发生时的调用栈

计数器数据写入本地文件系统(存储路径可通过go env GOTELEMETRYDIR查看)的内存映射文件中:

// 在我的macOS上
$go env GOTELEMETRYDIR
/Users/tonybai/Library/Application Support/go/telemetry

大约每周一次,计数器数据会被汇总成报告,存储在本地目录中。如果启用了上传(on),只有经过批准的计数器子集会被上传到telemetry.go.dev。

访问telemetry.go.dev网站可以查看由公开上传数据合并的报告和生成的图表。这些报告和图表可以帮助Go团队了解工具的使用情况、性能表现,从而进行有针对性的改进。

为了Go演进路线的精准,这里也呼吁大家多多支持。当下载Go 1.23版本后,简单地执行“go telemetry on”,你就可以为Go做贡献了:

$go telemetry on
Telemetry uploading is now enabled and data will be periodically sent to
https://telemetry.go.dev/. Uploaded data is used to help improve the Go
toolchain and related tools, and it will be published as part of a public
dataset.

For more details, see https://telemetry.go.dev/privacy.
This data is collected in accordance with the Google Privacy Policy
(https://policies.google.com/privacy).

To disable telemetry uploading, but keep local data collection, run
“go telemetry local”.
To disable both collection and uploading, run “go telemetry off”.

2.2 实用的go命令变化

  • go env -changed

go env子命令增加一个-changed命令行选项,可以用于查看当前Go环境中设置的Go环境变量值与默认值有差异的项的值,包括使用go env -w写入的,或是通过系统环境变量设置的。

在我的环境中,我可以看到下面的几个go环境变量的值的自定义设定:

$go env -changed
GONOPROXY='xxxxx'
GONOSUMDB='xxxxx'
GOPRIVATE='xxxxx'
GOPROXY='https://goproxy.cn'
GOSUMDB='off'
  • go mod tidy -diff

go mod tidy增加了一个“dry-run”方式,通过-diff命令行选项,可以使得go mod tidy只打印(以unified diff的格式)更新信息,但不做实际的更新(即修改go.mod和go.sum),比如在我的以前的一个代码目录下执行该命令:

$go mod tidy -diff
go: downloading google.golang.org/protobuf v1.25.0
go: downloading github.com/golang/protobuf v1.4.3
go: downloading google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98
go: downloading golang.org/x/net v0.0.0-20201021035429-f5854403a974
go: downloading golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f
go: downloading github.com/google/go-cmp v0.5.6
go: downloading golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
go: downloading golang.org/x/text v0.3.3
go: downloading github.com/golang/protobuf v1.5.2
go: downloading google.golang.org/protobuf v1.27.1
go: downloading golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4
diff current/go.mod tidy/go.mod
--- current/go.mod
+++ tidy/go.mod
@@ -8,12 +8,12 @@
 )

 require (
-   github.com/golang/protobuf v1.4.3 // indirect
+   github.com/golang/protobuf v1.5.2 // indirect
    golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect
-   golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f // indirect
+   golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect
    golang.org/x/text v0.3.3 // indirect
    google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98 // indirect
-   google.golang.org/protobuf v1.25.0 // indirect
+   google.golang.org/protobuf v1.27.1 // indirect
 )

 replace google.golang.org/grpc v1.40.0 => /Users/tonybai/Go/src/github.com/grpc/grpc-go

diff current/go.sum tidy/go.sum
--- current/go.sum
+++ tidy/go.sum
@@ -7,13 +7,16 @@
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
+github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
+github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -28,14 +31,18 @@
 github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
 github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -43,6 +50,7 @@
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -69,8 +77,9 @@
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -105,10 +114,14 @@
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
-google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

$echo $?
1

我们看到,如果有更新的包,该命令还会返回一个非0值(echo $?)以作为提示

  • go.mod中增加godebug指示符(directive)

从Go 1.23版本开始,你可以在go.mod/go.work文件中使用godebug指示符,其语法格式如下(包括单行和块状):

godebug default=go1.21

godebug (
    panicnil=1
    asynctimerchan=0
)

default: 是一个特殊的键,用于指定未明确设置的GODEBUG值应该采用哪个Go版本的默认值,例如: default=go1.21。除default键之外的其他键值对则用于明确设置特定的GODEBUG选项。

Go支持多种方式设置GODEBUG,包括在使用go命令时伴随使用GODEBUG环境变量、使用go.mod中的godebug以及在源文件中使用//go:debug指示符。它们之间的优先级关系是:go.mod中的设置优先于Go工具链的默认值,但可以被源文件中的//go:debug指令覆盖

更多关于godebug机制的内容,大家可以查看godebug的官方参考文档

3. 编译器与运行时

  • 开启PGO情况下,编译速度的提升

Go从1.20版本引入PGO优化技术,到目前PGO已经得到了进一步的优化,但PGO的引入也带来了编译时间的显著开销,对于一些大型项目,在开启PGO的情况下,编译时间甚至增加了100%。Go 1.23版本针对PGO的构建成本做了大幅优化,使得PGO带来的编译开销仅仅相对于非PGO增加个位数级百分比的变化。

  • 限制对linkname的使用

在Go语言中,//go:linkname指令可以用来链接到标准库或其他包中的未导出符号。比如我们想访问runtime包中的一个未导出函数,例如runtime.nanotime。这个函数返回当前时间的纳秒数。我们可以通过//go:linkname指令链接到这个符号。下面我用一个示例来演示一下这点:

// go1.23-examples/compiler/golinkname/main.go
package main

import (
    "fmt"
    _ "unsafe" // 必须导入 unsafe 包以使用 //go:linkname
)

// 声明符号链接
//
//go:linkname nanotime runtime.nanotime
func nanotime() int64

func main() {
    // 调用未导出的 runtime.nanotime 函数
    fmt.Println("Current time in nanoseconds:", nanotime())
}

运行该示例:

$go run main.go
Current time in nanoseconds: 397501409223055

这种做法一般不推荐,因为它可能导致程序不稳定,并且未来版本的Go可能会改变内部实现(比如nanotime被改名或被删除),破坏你的代码。

Go团队意识到了这种不规范的行为,在Go 1.23中,Go团队明确了//go:linkname的使用规范。

Go 1.23链接器现在禁止使用//go:linkname指令来引用标准库中未标记有//go:linkname的内部符号,并且链接器也禁止从汇编代码中引用这些符号。

不过,为了向后兼容,在一些大型开源代码库中发现的存量//go:linkname用法仍然受支持,为此,Go在标准库和runtime库中为支持linkname的函数增加了//go:linkname标记,以上面示例中的runtime.nanotime为例,在Go 1.23中其源码注释如下:

// runtime/time_nofake.go

// Exported via linkname for use by time and internal/poll.
//
// Many external packages also linkname nanotime for a fast monotonic time.
// Such code should be updated to use:
//
//  var start = time.Now() // at init time
//
// and then replace nanotime() with time.Since(start), which is equally fast.
//
// However, all the code linknaming nanotime is never going to go away.
// Do not remove or change the type signature.
// See go.dev/issue/67401.
//
//go:linkname nanotime
//go:nosplit
func nanotime() int64 {
    return nanotime1()
}

对于没有标记//go:linkname的标准库内部符号,要在外部通过go:linkname引用默认都将被禁止。不过,考虑到调试和实验目的,你也可以通过使用-checklinkname=0这个链接器命令行选项来禁用这个检查:

$go env -w GOFLAGS=-ldflags=-checklinkname=0 // 全局生效

4. 标准库

标准库的变化永远是大头儿,这里仅列出重要的变化。

4.1 Timer/Ticker变化

timer/ticker的stop/reset问题一直困扰Go团队,Go 1.23的两个重要fix期望能从根本上解决这个问题:

程序不再引用的Timer和Ticker将立即有资格进行垃圾回收,即使它们的Stop方法尚未被调用。Go的早期版本直到触发后才会收集未停止的Timer,并
且从未收集未停止的Ticker。

  • Timer/Ticker的Stop/Reset后不再接收旧值(issue 37196)

与Timer或Ticker关联的计时器channel现在改为无缓冲的了,即容量为0 。此更改的主要效果是Go现在保证任何对Reset或Stop方法的调用,调用之前不会发送或接收任何陈旧值。 Go的早期版本使用带有缓冲区的channel,因此很难正确使用Reset和Stop。此更改的一个明显效果是计时器channel的len和cap现在返回0而不是1,这可能会影响轮询长度以确定是否在计时器channel上接收的程序。通过GODEBUG设置asynctimerchan=1可恢复异步通道行为。

4.2 structs包

Go语言的结构体布局实际上受到平台布局和对齐规则的严格限制,这种限制可能导致在某些平台上出现权衡或潜在问题,同时阻碍了可以节省内存和提高垃圾回收性能的字段重排优化。

为了解决某些平台(如WASM和ppc64le)的特殊对齐需求,提高跨平台兼容性,并为未来的结构体优化留下空间,David Chase提案增加HostLayout指示符类型

具体来说就是引入一个新的包,包含一个零size的类型HostLayout:

// $GOROOT/src/structs/hostlayout.go

type HostLayout struct {
    _ hostLayout // prevent accidental conversion with plain struct{}
}

该类型可用作结构体字段来控制编译器对结构体类型的布局方式。被标记为HostLayout的结构体字段将按照主机的C ABI期望的方式进行内存布局。HostLayout不会影响包含它的结构体内部其他结构体类型字段的布局,也不会影响包含它的结构体所在的更上层结构体的布局。按照惯例,HostLayout应该作为一个名为”_”的字段类型,放在结构体定义的开始位置,比如:

type T struct {
    _ structs.HostLayout
    x, y int
}

注:关于结构体对齐,可以参考《Go语言第一课》专栏的第17讲:复合数据类型:用结构体建立对真实世界的抽象

4.3 新增unique包、iter包、函数迭代器相关函数

Go标准库还新增了unique包,并在maps、slices中增加了函数迭代器的实用函数,具体内容大家可以参考我之前的文章《Go 1.23新特性前瞻》。

至于关于新增的iter包的用法,可以参考《Go 1.23中的自定义迭代器与iter包》一文,这里就不赘述了!

5. 小结

Go 1.23版本在Russ Cox的带领下取得了丰硕的成果,为开发者带来了众多令人瞩目的语言特性、工具链优化以及编译器和运行时改进。

然而,随着Russ Cox的卸任,从Go 1.24版本开始,我们将迎来新一代Go决策层的领导。他们将如何引领Go语言的未来发展?是否会带来新的方向和变化?让我们拭目以待,共同见证Go语言的持续进化。

不过这里还要提醒各位Go开发者,在升级Go 1.23版本时务必注意潜在的向后兼容性问题,尤其是//go:linkname、time.Timer/Ticker的变化可能带来的影响。

本文涉及的源码可以在这里下载。

6. 参考资料


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

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

Gopher的Rust第一课:Rust的依赖管理

本文永久链接 – https://tonybai.com/2024/06/16/gopher-rust-first-lesson-managing-deps

在上一章《Gopher的Rust第一课:Rust代码组织》中,我们了解了Rust的代码组织形式,知道了基于Cargo构建项目以及Rust代码组织是目前的标准方式,同时Cargo也是管理项目外部依赖的标准方法,而项目内部的代码组织则由Rust module来完成。

在这一章中,我们将聚焦Rust的依赖管理,即Cargo对外部crate依赖的管理操作。我将先介绍几种依赖的来源类型(来自crates.io或其他Package Registries、来自某个git仓库以及来自本地的crate等),然后说说Cargo依赖的常见操作,包括依赖的添加、升降版本和删除;最后,聊一下如何处理依赖同一个依赖项的不同版本。

作为Gopher,我们先来简略回顾一下Go的依赖管理要点,大家可以在学习Cargo依赖管理后自己做个简单的对比,看看各自的优缺点是什么。

5.1 Go依赖管理回顾

Go 1.11版本开始,Go引入了Go Modules以替代旧的GOPATH方式进行依赖管理。

我们可以使用go mod init命令初始化一个新的Go模块。go mod init会创建一个go.mod文件,该文件记录了当前项目的模块路径,并通过require directive记录了当前模块的依赖项以及版本:

require github.com/some/module v1.2.3

在开发过程中,我们也可以使用replace替换某个模块的路径,例如将依赖指向本地代码库进行调试:

replace example.com/some/module => ../local/module

或是通过replace将依赖指向某个特定版本的包。Go 1.18引入的Go工作区模式让依赖本地包的动作更为便利丝滑。

Go Modules支持语义版本控制(semver),版本号格式为vX.Y.Z(其中X是major,Y为minor,Z为patch)。当发生不兼容变化时X编号需要+1。Go创新性地使用了语义版本导入机制,通过在包导入路径上使用vX来支持导入同一个包的不同major版本:

import (
    "github.com/some/module"
    v2 "github.com/some/module/v2"
)

无论是Go代码中引入新依赖,还是通过go mod edit命令手工修改依赖(升级、更新版本或降级版本),通过go mod tidy这个万能命令都可以自动清理和整理依赖。 go module还支持使用go.sum文件来记录每个依赖项的精确版本和校验和,确保依赖的完整性和安全性。go.sum文件应当提交到版本控制系统中。

此外,go mod vendor支持将依赖项副本存储在本地,这可以使你的项目在没有网络连接的情况下构建,并且可以避免依赖项版本冲突。

Go并没有采用像Rust、Js那样的中心module registry,而是采用了分布式go proxy来实现依赖发现与获取,默认的goproxy为proxy.golang.org,国内Gopher可以使用goproxy.cn、goproxy.io以及几个大厂提供的GOPROXY。

注:更多关于Go module依赖管理的系统且详细的内容,可以看看我在极客时间“Go语言第一课”专栏中的两讲:06|构建模式:Go是怎么解决包依赖管理问题的?07|构建模式:Go Module的6类常规操作

接下来,我们正式进入Rust的依赖管理环节,我们先来看看Cargo依赖的来源。

5.2 Cargo依赖的来源

Rust的依赖管理系统中,Rust项目主要有以下几种依赖来源:

  1. 来自crates.io的依赖:这是Rust官方的crate registry,包含了大量开源的Rust库。
  2. 来自某个git仓库的依赖:可以从任何git仓库添加依赖,特别是在开发阶段或使用未发布的版本时非常有用。
  3. 来自本地的crate依赖:可以添加本地文件系统中的crate,便于在开发过程中引用本地代码。

接下来,我们就来逐一看看在一个Cargo项目中如何配置这三种不同来源的依赖。

5.2.1 来自crates.io的依赖

在Rust中,最常见的依赖来源是crates.io,这也是Rust官方维护的中心crate registry,我们可以通过cargo命令或手工修改Cargo.toml文件来添加这些依赖。我们用一个示例来说明一下如何为当前项目添加来自crates.io的依赖。

我们先用cargo创建一个名为hello_world的binary项目:

$cargo new hello_world --bin
     Created binary (application) `hello_world` package

$cat Cargo.toml
[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

//managing-deps/hello_world/src/main.rs
fn main() {
    println!("Hello, world!");
}

构建该项目,这与我们在《Gopher的Rust第一课:第一个Rust程序》一文中描述的别无二致:

$cargo build
   Compiling hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/hello_world)
    Finished dev [unoptimized + debuginfo] target(s) in 1.07s

$./target/debug/hello_world
Hello, world!

现在我们改造一下main.rs代码,添加点“实用”代码(改自serde的example):

//managing-deps/hello_world/src/main.rs
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    println!("Hello, world!");
    let point = Point { x: 1, y: 2 };

    // Convert the Point to a JSON string.
    let serialized = serde_json::to_string(&point).unwrap();

    // Prints serialized = {"x":1,"y":2}
    println!("serialized = {}", serialized);

    // Convert the JSON string back to a Point.
    let deserialized: Point = serde_json::from_str(&serialized).unwrap();

    // Prints deserialized = Point { x: 1, y: 2 }
    println!("deserialized = {:?}", deserialized);
}

然后我们通过cargo check命令检查一下源码是否可以编译通过:

$cargo check
    Checking hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/hello_world)
error[E0432]: unresolved import `serde`
 --> src/main.rs:1:5
  |
1 | use serde::{Deserialize, Serialize};
  |     ^^^^^ use of undeclared crate or module `serde`

error[E0433]: failed to resolve: use of undeclared crate or module `serde_json`
  --> src/main.rs:14:22
   |
14 |     let serialized = serde_json::to_string(&point).unwrap();
   |                      ^^^^^^^^^^ use of undeclared crate or module `serde_json`

error[E0433]: failed to resolve: use of undeclared crate or module `serde_json`
  --> src/main.rs:20:31
   |
20 |     let deserialized: Point = serde_json::from_str(&serialized).unwrap();
   |                               ^^^^^^^^^^ use of undeclared crate or module `serde_json`

Some errors have detailed explanations: E0432, E0433.
For more information about an error, try `rustc --explain E0432`.
error: could not compile `hello_world` (bin "hello_world") due to 3 previous errors

cargo check提示找不到serde、serde_json两个crate。并且,cargo check执行后,多出一个Cargo.lock文件。由于此时尚未在Cargo.toml中添加依赖(虽然代码中明确了对serde和serde_json的依赖),Cargo.lock中还没有依赖package的具体信息:

$cat Cargo.lock
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3

[[package]]
name = "hello_world"
version = "0.1.0"

Rust是否可以像go module那样通过go mod tidy自动扫描源码并在Cargo.toml中补全依赖信息呢?然而并没有。Rust添加依赖的操作还是需要手动完成。

我们的rust源码依赖serde和serde_json,接下来,我们就需要在Cargo.toml中手工添加serde、serde_json依赖,当然最标准的方法还是通过cargo add命令:

$cargo add serde serde_json
      Adding serde v1.0.202 to dependencies.
             Features:
             + std
             - alloc
             - derive
             - rc
             - serde_derive
             - unstable
      Adding serde_json v1.0.117 to dependencies.
             Features:
             + std
             - alloc
             - arbitrary_precision
             - float_roundtrip
             - indexmap
             - preserve_order
             - raw_value
             - unbounded_depth

我们查看一下cargo add执行后的Cargo.toml:

$cat Cargo.toml
[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
serde = "1.0.202"
serde_json = "1.0.117"

我们看到在dependencies下新增了两个直接依赖信息:serde和serde_json以及它们的版本信息。

关于依赖版本,Cargo定义了的兼容性规则如下:

针对在1.0版本之前的版本,比如0.x.y,语义版本规范认为是处于初始开发阶段,公共API是不稳定的,因此没有明确兼容性语义。但Cargo对待这样的版本的规则是:0.x.y与0.x.z是兼容的,如果x > 0且y >=z。比如:0.1.10是兼容0.1.1的。而在1.0版本之后,Cargo参考语义版本规范确定版本兼容性。

基于上述的兼容性规则,在Cargo.toml中指定依赖版本的形式与语义有如下几种情况:

some_crate = "1.2.3" => 版本范围[1.2.3, 2.0.0)。
some_crate = "1.2" => 版本范围[1.2.0, 2.0.0)。
some_crate = "1" => 版本范围[1.0.0, 2.0.0)。

some_crate = "0.2.3" => 版本范围[0.2.3, 0.3.0)。
some_crate = "0.2" => 版本范围[0.2.0, 0.3.0)。
some_crate = "0" => 版本范围[0.0.0, 1.0.0)。

some_crate = "0.0" => 版本范围[0.0.0, 0.1.0)。
some_crate = "0.0.3" => 版本范围[0.0.3, 0.0.4)。

some_crate = "^1.2.3" => 版本范围[1.2.3]。

some_crate = "~1.2.3" => 版本范围[1.2.3, 1.3.0)。
some_crate = "~1.2" => 版本范围[1.2.0, 1.3.0)。
some_crate = "~1" => 版本范围[1.0.0, 2.0.0)。

Cargo还支持一些带有通配符的版本需求形式:

some_crate = "*" => 版本范围[0.0.0, )。
some_crate = "1.*" => 版本范围[1.0.0, 2.0.0)。
some_crate = "1.2.*" => 版本范围[1.2.0, 1.3.0)。

如果要限制最高版本范围,可以用带有多版本的需求形式:

some_crate = ">=1.2, < 1.5" => 版本范围[1.2.0, 1.5.0)。

有了版本范围后,Cargo初始就会使用该范围内的当前最大版本号版本作为依赖的最终版本。比如some_crate = "1.2.3",但当前some_crate的最高版本为1.3.5,那么Cargo会选择1.3.5的some_crate作为当前项目的依赖。

如果一个项目有两个依赖项同时依赖另外一个共同的依赖,比如(例子来自Cargo book):

# Package A
[dependencies]
bitflags = "1.0"

# Package B
[dependencies]
bitflags = "1.1"

那么A依赖bitflags的范围在[1.0.0, 2.0.0),B依赖bitflags的范围在[1.1.0, 2.0.0),这样如果当前bitflags的最新版本为1.2.1,那么Cargo会选择1.2.1作为bitflags的最终版本。这点与Go的最小版本选择(mvs)是不一样的,在这个示例情况下,Go会选择bitflags的1.1.0版本,即满足A和B的bitflags的最小版本即可。

后续当依赖的版本有更新时,可以执行cargo update升级依赖的版本到一个兼容的、更高的版本(体现在Cargo.lock文件中依赖的版本更新)。

Cargo.lock是锁定Cargo最终采用的依赖的版本的描述文件,这个文件由cargo管理,不要手动修改,这时的Cargo.lock文件如下:

$cat Cargo.lock
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3

[[package]]
name = “hello_world”
version = “0.1.0″
dependencies = [
 "serde",
 "serde_json",
]

[[package]]
name = “itoa”
version = “1.0.11″
source = “registry+https://github.com/rust-lang/crates.io-index”
checksum = “49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b”

[[package]]
name = “proc-macro2″
version = “1.0.83″
source = “registry+https://github.com/rust-lang/crates.io-index”
checksum = “0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43″
dependencies = [
 "unicode-ident",
]

[[package]]
name = “quote”
version = “1.0.36″
source = “registry+https://github.com/rust-lang/crates.io-index”
checksum = “0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7″
dependencies = [
 "proc-macro2",
]

[[package]]
name = “ryu”
version = “1.0.18″
source = “registry+https://github.com/rust-lang/crates.io-index”
checksum = “f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f”

[[package]]
name = “serde”
version = “1.0.202″
source = “registry+https://github.com/rust-lang/crates.io-index”
checksum = “226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395″
dependencies = [
 "serde_derive",
]

[[package]]
name = “serde_derive”
version = “1.0.202″
source = “registry+https://github.com/rust-lang/crates.io-index”
checksum = “6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838″
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = “serde_json”
version = “1.0.117″
source = “registry+https://github.com/rust-lang/crates.io-index”
checksum = “455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3″
dependencies = [
 "itoa",
 "ryu",
 "serde",
]

[[package]]
name = “syn”
version = “2.0.65″
source = “registry+https://github.com/rust-lang/crates.io-index”
checksum = “d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106″
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-ident",
]

[[package]]
name = “unicode-ident”
version = “1.0.12″
source = “registry+https://github.com/rust-lang/crates.io-index”
checksum = “3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b”

和go.sum类似(但go.sum并不指示依赖项采用的具体版本), Cargo.lock中对于每个依赖项都包括名字、具体某个版本、来源与校验和。

我们再用cargo check一下该项目是否可以编译成功:

$cargo check
   Compiling serde v1.0.202
   Compiling serde_json v1.0.117
    Checking ryu v1.0.18
    Checking itoa v1.0.11
    Checking hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/hello_world)
error: cannot find derive macro `Serialize` in this scope
 --> src/main.rs:3:10
  |
3 | #[derive(Serialize, Deserialize, Debug)]
  |          ^^^^^^^^^
  |
note: `Serialize` is imported here, but it is only a trait, without a derive macro
 --> src/main.rs:1:26
  |
1 | use serde::{Deserialize, Serialize};
  |                          ^^^^^^^^^

error: cannot find derive macro `Deserialize` in this scope
 --> src/main.rs:3:21
  |
3 | #[derive(Serialize, Deserialize, Debug)]
  |                     ^^^^^^^^^^^
  |
note: `Deserialize` is imported here, but it is only a trait, without a derive macro
 --> src/main.rs:1:13
  |
1 | use serde::{Deserialize, Serialize};
  |             ^^^^^^^^^^^

error[E0277]: the trait bound `Point: Serialize` is not satisfied
    --> src/main.rs:14:44
     |
14   |     let serialized = serde_json::to_string(&point).unwrap();
     |                      --------------------- ^^^^^^ the trait `Serialize` is not implemented for `Point`
     |                      |
     |                      required by a bound introduced by this call
     |
     = help: the following other types implement trait `Serialize`:
               bool
               char
               isize
               i8
               i16
               i32
               i64
               i128
             and 131 others
note: required by a bound in `serde_json::to_string`
    --> /Users/tonybai/.cargo/registry/src/rsproxy.cn-8f6827c7555bfaf8/serde_json-1.0.117/src/ser.rs:2209:17
     |
2207 | pub fn to_string<T>(value: &T) -> Result<String>
     |        --------- required by a bound in this function
2208 | where
2209 |     T: ?Sized + Serialize,
     |                 ^^^^^^^^^ required by this bound in `to_string`

error[E0277]: the trait bound `Point: Deserialize<'_>` is not satisfied
    --> src/main.rs:20:31
     |
20   |     let deserialized: Point = serde_json::from_str(&serialized).unwrap();
     |                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Deserialize<'_>` is not implemented for `Point`
     |
     = help: the following other types implement trait `Deserialize<'de>`:
               bool
               char
               isize
               i8
               i16
               i32
               i64
               i128
             and 142 others
note: required by a bound in `serde_json::from_str`
    --> /Users/tonybai/.cargo/registry/src/rsproxy.cn-8f6827c7555bfaf8/serde_json-1.0.117/src/de.rs:2676:8
     |
2674 | pub fn from_str<'a, T>(s: &'a str) -> Result<T>
     |        -------- required by a bound in this function
2675 | where
2676 |     T: de::Deserialize<'a>,
     |        ^^^^^^^^^^^^^^^^^^^ required by this bound in `from_str`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `hello_world` (bin "hello_world") due to 4 previous errors

似乎是依赖包缺少某个feature。我们重新add一下serde依赖,这次带着必要的feature:

$cargo add serde --features derive,serde_derive
      Adding serde v1.0.202 to dependencies.
             Features:
             + derive
             + serde_derive
             + std
             - alloc
             - rc
             - unstable

然后再执行check:

$cargo check
   Compiling proc-macro2 v1.0.83
   Compiling unicode-ident v1.0.12
   Compiling serde v1.0.202
   Compiling quote v1.0.36
   Compiling syn v2.0.65
   Compiling serde_derive v1.0.202
    Checking serde_json v1.0.117
    Checking hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/hello_world)
    Finished dev [unoptimized + debuginfo] target(s) in 8.50s

我们看到,当开启serde的derive和serde_derive feature后,项目代码就可以正常编译和运行了,下面是运行结果:

$cargo run
   Compiling itoa v1.0.11
   Compiling ryu v1.0.18
   Compiling serde v1.0.202
   Compiling serde_json v1.0.117
   Compiling hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/hello_world)
    Finished dev [unoptimized + debuginfo] target(s) in 4.16s
     Running `target/debug/hello_world`
Hello, world!
serialized = {"x":1,"y":2}
deserialized = Point { x: 1, y: 2 }

注:feature是cargo提供的一种条件编译和选项依赖的机制,有些类似于Go build constraints,但表达能力和控制精细度要远超go build constraints,但其复杂度也远超go build constraints。在本章中,我们不对feature进行展开说明,更多关于feature的详细说明,请参见cargo feature参考手册

除了官方的crates.io,Cargo还支持来自其他非官方的Registry的依赖,比如使用企业私有crate registry,这个不在本章内容范围内,后续会考虑用专题的形式说明。

考虑crates.io在海外,国内Rustaceans可以考虑使用国内的crate源,比如使用rsproxy源的配置如下:

// ~/.cargo/config
[source.crates-io]
replace-with = 'rsproxy'

[source.rsproxy]
registry = "https://rsproxy.cn/crates.io-index"

[source.rsproxy-sparse]
registry = "sparse+https://rsproxy.cn/index/"

[registries.rsproxy]
index = "https://rsproxy.cn/crates.io-index"

[net]
git-fetch-with-cli = true

git-fetch-with-cli = true表示使用本地git命令去获取registry index,否则使用内置的git库来获取。

5.2.2 来自git仓库的依赖

有时候,我们可能需要依赖一个尚未发布到crates.io上的库,这时可以通过git仓库来添加依赖。当然,这一方式也非常适合一些企业内的私有git仓库上的依赖。在Go中,如果没有一些额外的IT设置支持,便很难拉取私有仓库上的go module

下面我们使用下面命令将Cargo.toml中的serde依赖改为从git repo获取:

$cargo add serde --features derive,serde_derive  --git https://github.com/serde-rs/serde.git
    Updating git repository `https://github.com/serde-rs/serde.git`
      Adding serde (git) to dependencies.
             Features:
             + derive
             + serde_derive
             + std
             - alloc
             - rc
             - unstable

更新后的Cargo.toml依赖列表变为了:

[dependencies]
serde = { git = "https://github.com/serde-rs/serde.git", version = "1.0.202", features = ["derive", "serde_derive"] }
serde_json = "1.0.117"

不过当我执行cargo check时报如下错误:

$cargo check
    Updating git repository `https://github.com/serde-rs/serde.git`
remote: Enumerating objects: 28491, done.
remote: Counting objects: 100% (6879/6879), done.
remote: Compressing objects: 100% (763/763), done.
remote: Total 28491 (delta 6255), reused 6560 (delta 6111), pack-reused 21612
Receiving objects: 100% (28491/28491), 7.97 MiB | 205.00 KiB/s, done.
Resolving deltas: 100% (20065/20065), done.
From https://github.com/serde-rs/serde
 * [new ref]                    -> origin/HEAD
 * [new tag]           v0.2.0     -> v0.2.0
 * [new tag]           v0.2.1     -> v0.2.1
 * [new tag]           v0.3.0     -> v0.3.0
 * [new tag]           v0.3.1     -> v0.3.1
 ... ...
 * [new tag]           v1.0.98    -> v1.0.98
 * [new tag]           v1.0.99    -> v1.0.99
   Compiling serde v1.0.202
   Compiling serde_derive v1.0.202 (https://github.com/serde-rs/serde.git#37618545)
   Compiling serde v1.0.202 (https://github.com/serde-rs/serde.git#37618545)
    Checking serde_json v1.0.117
    Checking hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/hello_world)
error[E0277]: the trait bound `Point: serde::ser::Serialize` is not satisfied
    --> src/main.rs:14:44
... ...

在serde的github issue中,这个问题似乎已经修正,但在我的环境下不知何故依旧存在。

在使用git来源时,我们也可以指定一个特定的分支、tag或者commit:

[dependencies]
serde = { git = "https://github.com/serde-rs/serde.git", branch = "next" }
# 或者
serde = { git = "https://github.com/serde-rs/serde.git", tag = "v1.0.104" }
# 或者
serde = { git = "https://github.com/serde-rs/serde.git", rev = "a1b2c3d4" }

5.2.3 来自本地的crate依赖

在开发过程中,我们还可能需要引用本地文件系统中的crate。在Go中,我们可以使用go mod的replace或者Go workspace来解决该问题。在Rust中,我们也可以通过下面方式来添加本地依赖:

$cargo add serde --features derive,serde_derive  --path ../serde/serde
      Adding serde (local) to dependencies.
             Features:
             + derive
             + serde_derive
             + std
             - alloc
             - rc
             - unstable

// Cargo.toml
[dependencies]
serde = { version = "1.0.202", features = ["derive", "serde_derive"], path = "../serde/serde" }

不过,和来自git一样,基于来自本地的crate依赖,cargo check也报和基于git的crate依赖同样的错误。

5.3 Cargo依赖常见操作

下面简要说说依赖的常见操作,以来自crates.io的依赖为例。

5.3.1 添加依赖

正如上面示例中我们演示的那样,我们可以通过cargo add来添加一个依赖,或者可以通过手工编辑Cargo.toml文件添加对应的配置。例如,添加一个源自crates.io的新依赖rand库:

[dependencies]
rand = "0.8"

5.3.2 升降版本

要升级某个依赖到兼容的最新版本,可以使用cargo update;如果升级到不兼容版本,需要先修改Cargo.toml中的版本需求。例如,将rand库升级到2.x版本:

[dependencies]
rand = "2.0"

然后运行cargo update,Cargo会根据新的版本号需求进行重新解析依赖。

当然要降级依赖的版本到一个兼容的版本,通常可能需要在版本需求中使用类似“^x.y.z”来精确指定版本;如果要降级到一个不兼容版本,和升级到不兼容版本一样,需要先修改Cargo.toml中的版本需求,然后运行cargo update,Cargo会根据新的版本号需求进行重新解析依赖。

5.3.3 删除依赖

删除一个依赖则十分容易,只需从Cargo.toml中移除或注释掉对应的依赖配置, 然后运行cargo build,Cargo会更新项目的依赖关系。

5.4 处理依赖同一个依赖项的不同版本

在某些情况下,不同的crate可能依赖同一个crate的不同版本,这也是编程语言中典型的钻石依赖问题!是一个常见的依赖管理挑战。它发生在一个依赖项被两个或更多其他依赖项共享时。比如:app依赖A、B ,而A、B又同时依赖C。

在这样的情况下,前面我们提过Go给出的解决方案包含三点:

  • 若A、B依赖的C的版本相同,那么选取这个相同的C版本即可;
  • 若A、B依赖的C的版本不同但兼容(依照semver规范),那么选取C满足A、B依赖的最小版本,这叫做最小版本选择;
  • 若A、B依赖的C的版本不同且不兼容,那么通过语义导入版本,最终app将导入C的不同版本,这两个版本将在app中共存。

那么在Rust项目中,Cargo又是如何处理的呢?我们通过一个示例分别来看看这三种情况,我们创建一个app的示例:

// 在rust-guide-for-gopher/managing-deps目录下
$tree -F app
app
├── A/
│   ├── Cargo.toml
│   └── src/
│       └── lib.rs
├── B/
│   ├── Cargo.toml
│   └── src/
│       └── lib.rs
├── C/
│   ├── Cargo.lock
│   ├── Cargo.toml
│   └── src/
│       └── lib.rs
├── Cargo.lock
├── Cargo.toml
└── src/
    └── main.rs

7 directories, 10 files

app是一个binary cargo project,它的Cargo.toml和src/main.rs内容如下:

// app/Cargo.toml
[package]
name = "app"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
A = { path = "./A", version = "0.1.0" }
B = { path = "./B", version = "0.1.0" }

// app/src/main.rs
fn main() {
    println!("Hello, world!");
    A::hello_from_a();
    B::hello_from_b();
}

我们看到:app依赖crate A和B,并且分别调用了两个crate的公共函数。

接下来,我们再来看看A和B的情况,我们分场景说明。

5.4.1 依赖C的相同版本

当A和B依赖C的相同版本时,这个不难推断cargo最终会为A和B选择同一个依赖C的版本。比如:

$cat A/Cargo.toml
[package]
name = "A"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
C = { path = "../C", version = "1.0.0" }

$cat B/Cargo.toml
[package]
name = "B"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
C = { path = "../C", version = "1.0.0" }

$cat A/src/lib.rs
pub fn hello_from_a() {
    println!("Hello from A begin");
    C::hello_from_c();
    println!("Hello from A end");
}

$cat B/src/lib.rs
pub fn hello_from_b() {
    println!("Hello from B begin");
    C::hello_from_c();
    println!("Hello from B end");
}

$cat C/Cargo.toml
[package]
name = "C"
version = "1.3.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

$cat C/src/lib.rs
pub fn hello_from_c() {
    println!("Hello from C 1.3.0");
}

在这里A和B对C的依赖都是version = "1.0.0",通过前面的讲解我们知道,这等价于C的版本范围为[1.0.0, 2.0.0)。而C目前的版本为1.3.0,那么Cargo就会为A和B都选择1.3.0版本的C。我们运行一下这个app程序:

$cargo run
... ...
Hello, world!
Hello from A begin
Hello from C 1.3.0
Hello from A end
Hello from B begin
Hello from C 1.3.0
Hello from B end

我们还可以通过cargo tree命令验证一下对A和B对C版本的依赖:

$cargo tree --workspace --target all --all-features --invert C

C v1.3.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/C)
├── A v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/A)
│   └── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app)
└── B v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/B)
    └── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app)

我们看到A和B都依赖了C的v1.3.0版本。

5.4.2 依赖C的两个兼容版本

现在我们修改一下A和B对C的依赖版本需求:

$cat A/Cargo.toml
[package]
name = "A"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
C = { path = "../C", version = "1.1.1" }

$cat B/Cargo.toml
[package]
name = "B"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
C = { path = "../C", version = "1.2.3" }

让A对C的依赖需求为1.1.1,让B依赖需求为1.2.3,这回我们再来运行一下cargo run和cargo tree:

$cargo run
... ...
Hello, world!
Hello from A begin
Hello from C 1.3.0
Hello from A end
Hello from B begin
Hello from C 1.3.0
Hello from B end

$cargo tree --workspace --target all --all-features --invert C

C v1.3.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/C)
├── A v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/A)
│   └── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app)
└── B v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/B)
    └── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app)

由于1.1.1和1.2.3是兼容版本,因此Cargo选择了兼容这两个版本的C当前的最高版本1.3.0。

5.4.3 依赖C的两个不兼容版本

现在我们来试验一下当A和B依赖的C版本不兼容时,Cargo会为A和B选择C的什么版本!由于是本地环境,我们无法在一个目录下保存两个C版本,因此我们copy一份当前的C组件,将拷贝重命名为C-1.3.0,然后将C下面的Cargo.toml和src/lib.rs修改成下面的样子:

$cat C/Cargo.toml
[package]
name = "C"
version = "2.4.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

$cat C/src/lib.rs
pub fn hello_from_c() {
    println!("Hello from C 2.4.0");
}

然后我们修改一下A和B的依赖,让他们分别依赖C-1.3.0和C:

$cat A/Cargo.toml
[package]
name = "A"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
C = { path = "../C-1.3.0", version = "1.1.1" }

$cat B/Cargo.toml
[package]
name = "B"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
C = { path = "../C", version = "2.2.3" }

我们再来运行一下该app:

$cargo run
... ...
Hello, world!
Hello from A begin
Hello from C 1.3.0
Hello from A end
Hello from B begin
Hello from C 2.4.0
Hello from B end

我们看到cargo为A选择的版本是C v1.3.0,而为B选择的C版本是C v2.4.0,也就是说C的两个不兼容版本在app中可以同时存在。

让我们再来用cargo tree查看一下对C的依赖关系:

$cargo tree --workspace --target all --all-features --invert C

error: There are multiple `C` packages in your project, and the specification `C` is ambiguous.
Please re-run this command with one of the following specifications:
  C@1.3.0
  C@2.4.0

我们看到,cargo tree提示我们两个版本不兼容,必须明确指明是要查看哪个C版本的依赖,那我们就分别按版本查看一下:

$cargo tree --workspace --target all --all-features --invert C@1.3.0

C v1.3.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/C-1.3.0)
└── A v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/A)
    └── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app)

$cargo tree --workspace --target all --all-features --invert C@2.4.0

C v2.4.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/C)
└── B v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/B)
    └── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app)

5.4.4 直接依赖C的不同版本

在Go中我们可以通过语义导入版本实现在app中直接依赖同一个包的两个不兼容版本:

import (
    "github.com/user/repo"
    v2  "github.com/user/repo/v2"
)

在Rust中,是否也可以实现这一点?如果可以,又是如何实现的呢?答案是可以。至少我们可以通过使用Cargo的依赖别名功能来实现。我们建立一个名为dep_alias的示例,其目录结构如下:

$tree -F dep_alias
dep_alias
├── C/
│   ├── Cargo.lock
│   ├── Cargo.toml
│   └── src/
│       └── lib.rs
├── C-1.3.0/
│   ├── Cargo.lock
│   ├── Cargo.toml
│   └── src/
│       └── lib.rs
├── Cargo.lock
├── Cargo.toml
└── src/
    └── main.rs

5 directories, 9 files

在这个示例中,app依赖C-1.3.0目录下的C 1.3.0版本以及C目录下的C 2.4.0版本,下面是app/Cargo.toml和app/src/main.rs的代码:

// rust-guide-for-gopher/managing-deps/dep_alias/Cargo.toml

$cat Cargo.toml
[package]
name = "app"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
C_v1 = { path = "C-1.3.0", version = "1.0.0", package = "C" }
C_v2 = { path = "C", version = "2.3.0", package = "C" }

$cat src/main.rs

$cat src/main.rs
extern crate C_v1 as C_v1;
extern crate C_v2 as C_v2;

fn main() {
    C_v1::hello_from_c();
    C_v2::hello_from_c();
}

这里,我们为C的两个不兼容版本建立了两个别名:C_v1和C_v2,然后在代码中分别使用C_v1和C_v2,cargo会分别为C_v1和C_v2选择合适的版本,这里C_v1最终选择为1.3.0,而C_v2最终定为2.4.0:

$cargo run
Hello from C 1.3.0
Hello from C 2.4.0

由于包名依然是C,所以在使用cargo tree查看依赖关系时,依然要带上不同版本:

$cargo tree --workspace --target all --all-features --invert C@1.3.0
C v1.3.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/dep_alias/C-1.3.0)
└── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/dep_alias)

$cargo tree --workspace --target all --all-features --invert C@2.4.0
C v2.4.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/dep_alias/C)
└── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/dep_alias)

5.5 小结

在这一章中,我们介绍了Rust中通过Cargo进行依赖管理的基本方法。

我们首先简要回顾了Go语言的依赖管理,特别是Go Modules的相关内容,如go.mod文件、版本控制机制等。

接着我们介绍了Rust中通过Cargo进行依赖管理的方法。Cargo依赖主要有三种来源:crates.io官方注册中心、Git仓库和本地文件系统。通过Cargo.toml文件和cargo命令,我们可以灵活添加、升级、降级或删除依赖项。文中还讲解了Cargo的版本兼容性规则和各种指定版本的语法。

针对依赖同一个库的不同版本的情况,我通过示例说明了Cargo的处理方式:如果版本相同或兼容,Cargo会选择满足要求的当前最高版本;如果版本不兼容,Cargo允许在项目中同时使用这些不兼容的版本,可以通过别名来区分使用。

总体来看,Cargo提供的依赖管理方式表达能力很强大,但相对于Go来说,还是复杂了很多,学习起来曲线要高很多,troubleshooting起来也不易,文中尚有一个遗留问题尚未解决,如果大家有解决方案或思路,可以在文章评论中告知我,感谢。

注:本文涉及的都是cargo依赖管理的基础内容,还有很多细节以及高级用法并未涉及。

本章中涉及的源码可以在这里下载。

5.6 参考资料


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