标签 Rust 下的文章

Go语言中的SIMD加速:以矩阵加法为例

本文永久链接 – https://tonybai.com/2024/07/21/simd-in-go

前些日子,一些资深Gopher,比如fasthttp的作者Aliaksandr Valialkin函数迭代器加入Go 1.23版本而抱怨Go的演进走错了方向:朝着增加复杂性和隐式代码执行的方向发展,而没有专注于Go语言的基本设计哲学——简单性、生产力和性能。Valialkin希望Go团队能专注于一些性能打磨和优化的环节,比如使用SIMD提升一些计算场景下Go代码的性能,避免Go的某些领地被以性能和安全性著称的Rust抢去!

无独有偶,在Go项目issues中,我们也能看到很多有关希望Go支持SIMD指令的issue,比如近期的一个proposal,就期望Go团队可以在标准库中添加simd包以支持高性能的SIMD计算,就像Rust std::simd那样。当然,早期这类issue也有很多,比如:issue 53171issue 58610等。

那么什么是SIMD指令?在Go官方尚未支持simd包或SIMD计算的情况下,如何在Go中使用SIMD指令进行计算加速呢?在这篇文章中,我们就来做个入门版介绍,并以一个最简单的矩阵加法的示例来展示一下SIMD指令的加速效果。

1. SIMD指令简介

SIMD是“单指令多数据”(Single Instruction Multiple Data)的缩写。与之对应的则是SISD(Single Instruction, Single Data),即“单指令单数据”。

在大学学习汇编时,用于举例的汇编指令通常是SISD指令,比如常见的ADD、MOV、LEA、XCHG等。这些指令每执行一次,仅处理一个数据项。早期的x86架构下,SISD指令处理的数据仅限于8字节(64位)或更小的数据。随着处理器架构的发展,特别是x86-64架构的引入,SISD指令也能处理更大的数据项,使用更大的寄存器。但SISD指令每次仍然只处理一个数据项,即使这个数据项可能比较大。

相反,SIMD指令是一种特殊的指令集,它可以让处理器可以同时处理多个数据项,提高计算效率。我们可以用下面这个更为形象生动的比喻来体会SIMD和SISD的差别。

想象你是一个厨师,需要切100个苹果。普通的方式是一次切一个苹果,这就像普通的SISD处理器指令。而SIMD指令就像是你突然多了几双手,可以同时切4个或8个苹果。显然,多手同时工作会大大提高切苹果的速度。

具体来说,SIMD指令的优势在于以下几点:

  • 并行处理:一条指令可以同时对多个数据进行相同的操作。
  • 数据打包:将多个较小的数据(如32位浮点数)打包到一个较大的寄存器(如256位)中。
  • 提高数据吞吐量:每个时钟周期可以处理更多的数据。

这种并行处理方式特别适合于需要大量重复计算的任务,如图像处理、音频处理、科学计算等。通过使用SIMD指令,可以显著提高这些应用的性能。

主流的x86-64(amd64)和arm系列CPU都有对SIMD指令的支持。以x86-64为例,该CPU体系下支持的SIMD指令就包括MMX(MultiMedia eXtensions)、SSE (Streaming SIMD Extensions)、SSE2、SSE3、SSSE3、SSE4、AVX(Advanced Vector Extensions)、AVX2以及AVX-512等。ARM架构下也有对应的SIMD指令集,包括VFP (Vector Floating Point)、NEON (Advanced SIMD)、SVE (Scalable Vector Extension)、SVE2以及Helium (M-Profile Vector Extension, MVE)等。

注:在Linux上,你可以通过lscpu或cat /proc/cpuinfo来查看当前主机cpu支持的SIMD指令集的种类。
注:Go在Go 1.11版本才开始支持AVX-512指令

每类SIMD指令集都有其特定的优势和应用场景,以x86-64下的SIMD指令集为例:

  • MMX主要用于早期的多媒体处理;
  • SSE系列逐步改进了浮点运算和整数运算能力,广泛应用于图形处理和音视频编码;
  • AVX系列大幅提高了并行处理能力,特别适合科学计算和高性能计算场景。


x86-64下SIMD指令集演进

这些指令集的演进反映了处理器技术的发展和应用需求的变化。从支持64位计算的MMX到支持512位计算的AVX-512,SIMD指令的并行处理能力不断提升,更多更大的寄存器加入进来,为各种复杂的计算任务提供了强大的硬件支持。

注:SSE和AVX各自有16个寄存器,SSE的16个寄存器为XMM0-XMM15,XMM是128位寄存器,而YMM是256位寄存器。支持AVX的x86-64处理器包含16个256位大小的寄存器,从YMM0到YMM15。每个YMM寄存器的低128位是相对应的XMM寄存器。大多数AVX指令可以使用任何一个XMM或者YMM寄存器作为SIMD操作数。AVX512将每个AVXSIMD寄存器的大小从256位扩展到512位,称为ZMM寄存器;符合AVX512标准的处理器包含32个ZMM寄存器,从ZMM0~ZMM31。YMM和XMM寄存器分别对应于每个ZMM寄存器的低256位和低128位。

既然SIMD指令这么好,那么在Go中应该如何使用SIMD指令呢?接下来我们就来看看。

2. 在Go中如何使用SIMD指令

Go主要面向的是云计算领域、微服务领域,这些领域中对计算性能的要求相对没那么极致。以至于在一些对性能要求较高的场景,比如高性能计算、 图形学、数字信号处理等领域,很多gopher会遇到对Go计算性能进行优化的需求。

纯计算领域,怎么优化呢?此时此刻,Go官方并没有提供对SIMD提供支持的simd包。

一种想法是使用cgo机制在Go中调用更快的C或C++,但cgo的负担又不能不考虑,cgo不是go,很多人不愿意引入cgo。

另外一种想法就是再向下一层,直接上汇编,在汇编中直接利用SIMD指令实现并行计算。但手写汇编难度是很高的,手写Plan9风格、资料甚少的Go汇编难度则更高。那么有什么方法避免直接手搓汇编呢?目前看大致有这么几种(如果有更好的方法,欢迎在评论区提出你的建议):

  • 使用c2goasm(https://github.com/minio/c2goasm/)转换

我们可以先用c/c++实现对应的函数功能(可以利用类似intel提供的面向simd的intrisic functions),然后生成汇编代码(基于clang),再用c2goasm转换为go语言汇编。不过目前c2goasm已经public archive了,并且该方法应用受很多因素限制,比如clang版本和特定的编译选项啥的。亲测这种方法上手难度较高。

  • 使用uber工程师Michael McLoughlin开源的avo来生成go汇编

avo(https://github.com/mmcloughlin/avo)是一个go包,它支持以一种相对高级一些的Go语法来编写汇编,至少你可以不必直面那些晦涩难懂的汇编代码。但使用avo编写汇编也不是很容易的事情,你仍然需要大致知道汇编的运作原理和基本的编写规则。此外avo与汇编的能力并非完全等价,其作者声明:avo也还处于实验阶段。

  • 使用goplus/llgo集成c/c++生态

在go中调用c的cgo机制不受待见,llgo反其道而行之,将go、python、c/c++等代码统统转换为llvm中间代码进而通过clang编译和优化为可执行文件。这样就可以直接利用python、c/c++的生态,进而利用高性能的c/c++实现(比如支持SIMD指令)。目前llgo还不成熟,七牛云老板许式伟正在全力开发llgo,等llgo成熟后,这后续可能也是一种选择。

考虑到Go目前不直接支持intel intrisic functions for SIMD,要在Go中使用SIMD只能直接使用汇编。而在手搓汇编难度太高的情况下,通过avo生成汇编便是一条可以尝试的路径,我们可以将一些计算的核心部分用avo生成的汇编来进行加速。

接下来,我们就来通过一个矩阵加法的示例看看SIMD指令的加速效果。基于SIMD指令的矩阵加法的汇编逻辑,我们采用avo实现。

3. 第一版SIMD优化(基于SSE)

我们使用avo先来实现一版基于SSE指令集的矩阵加法。前面说过avo是一个Go库,我们无需安装任何二进制程序,直接使用avo库中的类型和函数编写矩阵加法的实现即可:

// simd-in-go/matadd-sse/pkg/asm.go

//go:build ignore
// +build ignore

package main

import (
    "github.com/mmcloughlin/avo/attr"
    . "github.com/mmcloughlin/avo/build"
    . "github.com/mmcloughlin/avo/operand"
)

func main() {
    TEXT("MatrixAddSIMD", attr.NOSPLIT, "func(a, b, c []float32)")
    a := Mem{Base: Load(Param("a").Base(), GP64())}
    b := Mem{Base: Load(Param("b").Base(), GP64())}
    c := Mem{Base: Load(Param("c").Base(), GP64())}
    n := Load(Param("a").Len(), GP64())

    X0 := XMM()
    X1 := XMM()

    Label("loop")
    CMPQ(n, U32(4))
    JL(LabelRef("done"))

    MOVUPS(a.Offset(0), X0)
    MOVUPS(b.Offset(0), X1)
    ADDPS(X1, X0)
    MOVUPS(X0, c.Offset(0))

    ADDQ(U32(16), a.Base)
    ADDQ(U32(16), b.Base)
    ADDQ(U32(16), c.Base)
    SUBQ(U32(4), n)
    JMP(LabelRef("loop"))

    Label("done")
    RET()

    Generate()
}

第一次看上面这段代码,你是不是觉得即便使用avo来生成矩阵加法的代码,如果你不了解汇编的编写和运行模式,你也是无从下手的。简单说一下这段代码。

首先,该文件是用于生成矩阵加法的汇编代码的,因此该asm.go并不会编译到最终的可执行文件中或测试代码中,这里利用go编译器构建约束将该文件排除在外。

main函数的第一行的TEXT函数定义了一个名为MatrixAddSIMD的函数,使用attr.NOSPLIT属性表示不需要栈分割,函数签名是:

func(a, b, c []float32)

变量a, b, c分别表示输入矩阵a, b和输出矩阵c的内存地址,使用Load函数从参数中加载基地址到GP64返回的通用寄存器。n表示矩阵的长度,使用 Load函数从参数中加载长度到GP64返回的通用寄存器。

X0和X1定义了两个XMM寄存器,用于SIMD操作。

接下来定义了一个循环,在这个循环的循环体中,将通过SSE指令处理输入的矩阵数据:

  • MOVUPS(a.Offset(0), X0):将矩阵a的前16字节(4 个float32)加载到XMM寄存器X0。
  • MOVUPS(b.Offset(0), X1):将矩阵b的前16字节(4个float32)加载到XMM寄存器X1。
  • ADDPS(X1, X0):将X1和X0中的数据相加,结果存入X0。
  • MOVUPS(X0, c.Offset(0)):将结果从X0存入矩阵c的前16字节。
  • ADDQ(U32(16), a.Base):将矩阵a的基地址增加16字节(4个float32)。
  • ADDQ(U32(16), b.Base):将矩阵b的基地址增加16字节(4个float32)。
  • ADDQ(U32(16), c.Base):将矩阵c的基地址增加16字节(4个float32)。
  • SUBQ(U32(4), n):将矩阵长度n减少4。
  • JMP(LabelRef(“loop”)):无条件跳转到标签loop,继续循环。

最后调用Generate函数生成汇编代码。

下面我们就来运行该代码,生成相应的汇编代码以及stub函数:

$cd matadd-sse/pkg
$make
go run asm.go -out add.s -stubs stub.go

下面是生产的add.s的全部汇编代码:

// simd-in-go/matadd-sse/pkg/add.s

// Code generated by command: go run asm.go -out add.s -stubs stub.go. DO NOT EDIT.

#include "textflag.h"

// func MatrixAddSIMD(a []float32, b []float32, c []float32)
// Requires: SSE
TEXT ·MatrixAddSIMD(SB), NOSPLIT, $0-72
    MOVQ a_base+0(FP), AX
    MOVQ b_base+24(FP), CX
    MOVQ c_base+48(FP), DX
    MOVQ a_len+8(FP), BX

loop:
    CMPQ   BX, $0x00000004
    JL     done
    MOVUPS (AX), X0
    MOVUPS (CX), X1
    ADDPS  X1, X0
    MOVUPS X0, (DX)
    ADDQ   $0x00000010, AX
    ADDQ   $0x00000010, CX
    ADDQ   $0x00000010, DX
    SUBQ   $0x00000004, BX
    JMP    loop

done:
    RET

这里使用的ADDPS、MOVUPS和ADDQ都是SSE指令:

  • ADDPS (Add Packed Single-Precision Floating-Point Values): 这是一个SSE指令,用于对两个128位的XMM寄存器中的4个单精度浮点数进行并行加法运算。
  • MOVUPS (Move Unaligned Packed Single-Precision Floating-Point Values): 这也是一个SSE指令,用于在内存和XMM寄存器之间移动128位的单精度浮点数数据。与MOVAPS(Move Aligned Packed Single-Precision Floating-Point Values) 指令不同,MOVUPS不要求地址对齐,可以处理非对齐的数据。

除了生成汇编代码外,asm.go还生成了一个stub函数:MatrixAddSIMD,即上面汇编实现的那个函数。

// simd-in-go/matadd-sse/pkg/stub.go

// Code generated by command: go run asm.go -out add.s -stubs stub.go. DO NOT EDIT.

package pkg

func MatrixAddSIMD(a []float32, b []float32, c []float32)

在matadd-sse/pkg/add-no-simd.go中,我们放置了常规的矩阵加法的实现:

package pkg

func MatrixAddNonSIMD(a, b, c []float32) {
    n := len(a)
    for i := 0; i < n; i++ {
        c[i] = a[i] + b[i]
    }
}

接下来,我们编写一些单测代码,确保一下MatrixAddSIMD和MatrixAddNonSIMD的功能是正确的:

// simd-in-go/matadd-sse/matrix_add_test.go
package main

import (
    "demo/pkg"
    "testing"
)

func TestMatrixAddNonSIMD(t *testing.T) {
    size := 1024
    a := make([]float32, size)
    b := make([]float32, size)
    c := make([]float32, size)
    expected := make([]float32, size)

    for i := 0; i < size; i++ {
        a[i] = float32(i)
        b[i] = float32(i)
        expected[i] = a[i] + b[i]
    }

    pkg.MatrixAddNonSIMD(a, b, c)

    for i := 0; i < size; i++ {
        if c[i] != expected[i] {
            t.Errorf("MatrixAddNonSIMD: expected %f, got %f at index %d", expected[i], c[i], i)
        }
    }
}

func TestMatrixAddSIMD(t *testing.T) {
    size := 1024
    a := make([]float32, size)
    b := make([]float32, size)
    c := make([]float32, size)
    expected := make([]float32, size)

    for i := 0; i < size; i++ {
        a[i] = float32(i)
        b[i] = float32(i)
        expected[i] = a[i] + b[i]
    }

    pkg.MatrixAddSIMD(a, b, c)

    for i := 0; i < size; i++ {
        if c[i] != expected[i] {
            t.Errorf("MatrixAddSIMD: expected %f, got %f at index %d", expected[i], c[i], i)
        }
    }
}

如我们预期的那样,上述单测代码可以顺利通过。接下来,我们再来做一下benchmark,看看使用SSE实现的矩阵加法性能到底提升了多少:

// simd-in-go/matadd-sse/benchmark_test.go
package main

import (
    "demo/pkg"
    "testing"
)

func BenchmarkMatrixAddNonSIMD(tb *testing.B) {
    size := 1024
    a := make([]float32, size)
    b := make([]float32, size)
    c := make([]float32, size)

    for i := 0; i < size; i++ {
        a[i] = float32(i)
        b[i] = float32(i)
    }

    tb.ResetTimer()
    for i := 0; i < tb.N; i++ {
        pkg.MatrixAddNonSIMD(a, b, c)
    }
}

func BenchmarkMatrixAddSIMD(tb *testing.B) {
    size := 1024
    a := make([]float32, size)
    b := make([]float32, size)
    c := make([]float32, size)

    for i := 0; i < size; i++ {
        a[i] = float32(i)
        b[i] = float32(i)
    }

    tb.ResetTimer()
    for i := 0; i < tb.N; i++ {
        pkg.MatrixAddSIMD(a, b, c)
    }
}

运行这个benchmark,我们得到下面结果:

$go test -bench .
goos: darwin
goarch: amd64
pkg: demo
... ...
BenchmarkMatrixAddNonSIMD-8      2129426           554.4 ns/op
BenchmarkMatrixAddSIMD-8         3481318           357.4 ns/op
PASS
ok      demo    3.350s

我们看到SIMD实现的确性能优秀,几乎在非SIMD实现的基础上提升了一倍。但这似乎还并不足以说明SIMD的优秀。我们再来扩展一下并行处理的数据的数量和宽度,使用AVX指令再来实现一版矩阵加法,看是否还会有进一步的性能提升。

4. 第二版SIMD优化(基于AVX)

下面是基于avo使用AVX指令实现的Go代码:

// simd-in-go/matadd-avx/pkg/asm.go

//go:build ignore
// +build ignore

package main

import (
    "github.com/mmcloughlin/avo/attr"
    . "github.com/mmcloughlin/avo/build"
    . "github.com/mmcloughlin/avo/operand"
)

func main() {
    TEXT("MatrixAddSIMD", attr.NOSPLIT, "func(a, b, c []float32)")
    a := Mem{Base: Load(Param("a").Base(), GP64())}
    b := Mem{Base: Load(Param("b").Base(), GP64())}
    c := Mem{Base: Load(Param("c").Base(), GP64())}
    n := Load(Param("a").Len(), GP64())

    Y0 := YMM()
    Y1 := YMM()

    Label("loop")
    CMPQ(n, U32(8))
    JL(LabelRef("done"))

    VMOVUPS(a.Offset(0), Y0)
    VMOVUPS(b.Offset(0), Y1)
    VADDPS(Y1, Y0, Y0)
    VMOVUPS(Y0, c.Offset(0))

    ADDQ(U32(32), a.Base)
    ADDQ(U32(32), b.Base)
    ADDQ(U32(32), c.Base)
    SUBQ(U32(8), n)
    JMP(LabelRef("loop"))

    Label("done")
    RET()

    Generate()
}

这里的代码与上面sse实现的代码逻辑类似,只是指令换成了avx的指令,包括VMOVUPS、VADDPS等:

  • VADDPS (Vectorized Add Packed Single-Precision Floating-Point Values): 是AVX (Advanced Vector Extensions) 指令集中的一个指令,用于对两个256位的YMM寄存器中的8个单精度浮点数进行并行加法运算。
  • VMOVUPS (Vectorized Move Unaligned Packed Single-Precision Floating-Point Values): 这也是一个AVX指令,用于在内存和YMM寄存器之间移动256位的单精度浮点数数据。与MOVUPS指令相比,VMOVUPS可以处理更宽的256位SIMD数据。

由于在SSE实现的版本中做了详细说明,这里就不再赘述代码逻辑,其他单元测试与benchmark测试的代码也都完全相同,我们直接看benchmark的结果:

$go test -bench .
goos: darwin
goarch: amd64
pkg: demo
... ...
BenchmarkMatrixAddNonSIMD-8      2115284           566.6 ns/op
BenchmarkMatrixAddSIMD-8        10703102           111.5 ns/op
PASS
ok      demo    3.088s

我们看到AVX版的矩阵加法的性能是常规实现的5倍多,是SSE实现的性能的近3倍,在实际生产中,这将大大提升代码的执行效率。

也许还有更优化的实现,但我们已经达到了基于SIMD加速矩阵加法的目的,这里就不再做继续优化了,大家如果有什么新的想法和验证的结果,可以在评论区留言告诉我哦!

5. 小结

在这篇文章中,我们探讨了在Go语言中使用SIMD指令进行计算加速的方法。尽管Go官方目前还没有直接支持SIMD的包,但我们通过使用avo库生成汇编代码的方式,成功实现了基于SSE和AVX指令集的矩阵加法优化。

我们首先介绍了SIMD指令的基本概念和优势,然后讨论了在Go中使用SIMD指令的几种可能方法。接着,我们通过一个具体的矩阵加法示例,展示了如何使用avo库生成基于SSE和AVX指令集的汇编代码。

通过benchmark测试,我们看到基于SSE指令的实现相比常规实现提升了约1.5倍的性能,而基于AVX指令的实现则带来了约5倍的性能提升。这充分说明了SIMD指令在并行计算密集型任务中的强大优势。

虽然直接使用SIMD指令需要一定的汇编知识,增加了代码的复杂性,但在一些对性能要求极高的场景下,这种优化方法仍然是非常有价值的。我希望这篇文章能为Go开发者在进行性能优化时提供一些新的思路和参考。

当然,这里展示的只是SIMD优化的一个简单示例。在实际应用中,可能还需要考虑更多因素,如数据对齐、边界条件处理等。大家可以在此基础上进行更深入的探索和实践。

本文涉及的源码可以在这里下载 – https://github.com/bigwhite/experiments/blob/master/simd-in-go

本文部分源代码由deepseek coder v2实现。

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的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