Go 1.23新特性前瞻

本文永久链接 – https://tonybai.com/2024/05/30/go-1-23-foresight

2024年5月22日,Go 1.23版本功能特性正式冻结,后续将只改bug,不增加新feature。

对Go团队来说,这意味着开始了Go 1.23rc1的冲刺,对我们普通Gopher而言,这意味着是时候对Go 1.23新增的功能做一些前瞻了

在Go没有发布Go 1.23rc1之前,我们至少可以通过以下两种渠道体验Go 1.23的最新特性:

  • 通过go install安装tip版本;
  • 使用Go playground在线体验。

注:关于Go tip的安装方法以及Go playground在线体验的详细说明,这里就不赘述了,《Go语言第一课》专栏的“03|配好环境:选择一种最适合你的Go安装方法”有系统全面的讲解,欢迎订阅阅读。

按照Go Release cycle,Go 1.23将于2024年8月发布!因此目前为时尚早,下面列出的有些变化最终不一定能进入到Go 1.23的最终版本中,有小概率被revert的可能或者推迟到下一个版本(Go 1.24),所以切记一切变更点要以最终Go 1.23版本发布时为准。

1. 语言变化

Go 1.23语言变化较少,除了range over func试验特性转正,再有就是几个悬而未决的spec变更。

1.1 range over func试验特性转正(61405)

Go 1.22版本引入了range over func试验特性,通过GOEXPERIMENT=rangefunc,可以实现函数迭代器。这一特性在Go 1.23版本正式转正,下面代码可以直接使用Go 1.23编译运行:

// go1.23-foresight/lang/range-over-function-iterator/main.go

package main

import "fmt"

func Backward[E any](s []E) func(func(int, E) bool) {
    return func(yield func(int, E) bool) {
        for i := len(s) - 1; i >= 0; i-- {
            if !yield(i, s[i]) {
                return
            }
        }
        return
    }
}

func main() {
    sl := []string{"hello", "world", "golang"}
    for i, s := range Backward(sl) {
        fmt.Printf("%d : %s\n", i, s)
    }
}

使用Go 1.23运行上述示例:

$go run main.go
2 : golang
1 : world
0 : hello

range over func可以提供一种统一、高效的迭代方式, 为泛型后的自定义容器类提供统一的迭代接口,同时也可以替代部分现有API返回切片的做法, 改为通过迭代器的形式改进性能(通过编译器的优化),甚至还可以为函数式编程风格提供标准迭代机制。

rang over func机制的实现是通过编译器在源码层面的转换,其转换形式大致如下:

// 单循环变量
for x := range f1 {
    ...
}

将被转换为:

f1(func(x T) bool {
    ...
})

而另外一种常见的双循环变量形式的for range:

for expr1, expr2 = range f2 {
    ...
}

将被转换为:

f2(func(#p1 T1, #p2 T2) bool {
    expr1, expr2 = #p1, #p2
    ...
})

前提是:f1和f2分别要满足标准库中iter包中的下面函数原型形式:

// $GOROOT/src/iter/iter.go
type Seq[V any] func(yield func(V) bool) bool
type Seq2[K, V any] func(yield func(K, V) bool) bool

此外,for range循环体中如果有break,将被转换为f1/f2中的return false,而如果有continue,则会被转换为return true。当然这只是大致的形式,实际的转换远比这个要复杂很多,要考虑的情况也是十分复杂。更为具体、复杂的转换可以参考Go编译器的实现源码rewrite.go

函数迭代器虽然转正,但肯定尚未成熟,后续还会有诸多问题(比如一些corner case)需要解决,比如Russ Cox新开的issue 65236就在讨论是否允许忽略迭代变量;issue 65237将跟踪spec中与range over func相关内容的变更。

1.2 spec:几个悬而未决的issue

这个issue来自我提出的《Go 1.22引入的包级变量初始化次序问题》,Go 1.23已经修正了该问题,并保持与Go 1.22之前的版本一致,但go spec中尚未就此给出明确的说明。

一些issue已经“跳票”多次,不能确定以上issue都能最终在Go 1.23得以解决!

2. 编译器与运行时

2.1 优化了PGO(Profile Guided Optimization)带来的处理开销 (issue 58102)

Go社区发现启用PGO后,每个cmd/compile调用都会解析完整的PGO pprof配置文件,构建完整的权重图,然后确定与该包相关的内容。这类工作项有很多,并且随着Profile文件的大小和构建包的数量的扩展,构建开销也会增大。尤其是对于那些特别大的项目,PGO Profile很大,这可能会导致构建时间增加100%以上。

Go 1.23对这个问题进行了优化,PGO开销被降到了个位数百分比。

2.2 限制将来对linkname的使用(67401)

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

// go1.23-foresight/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: 374424337532051

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

Go团队已经意识到这一点,并发现现存开源代码中有很多代码都通过//go:linkname依赖Go项目的internal下的包或Go标准库的未导出符号。这显然不是Go团队想看到的事儿,于是Russ Cox发起issue 67401,旨在考虑限制对//go:linkname的使用。

该issue虽然在Go 1.23 milestone中,但最终是否能落在Go 1.23中还不确定,毕竟这样的调整会导致一些现存代码无法正常编译运行。

2.3 其他一些优化

  • 优化内存分配器的行为,减少了大内存(带有指针)分配时的长暂停 (issue 31222)
  • 修复Windows下time.Sleep的精度问题(issue 44343)
  • 增加了设置崩溃输出的API runtime/debug.SetCrashOutput(issue 42888)
  • 对内联器继续进行大修:重构优化 (issue 61502),这是一个长期任务,估计后续版本还会继续。

3. 工具链

3.1 新增go telemetry子命令,改进go工具链的遥测能力 (issue 67111)

Russ Cox去年初就在个人博客上发布了四篇有关Go Telemetry的文章,在2023 GopherCon大会上,Russ Cox也谈到了Go Telemetry对基于数据驱动进行Go演进决策的重要性。Russ Cox亲自创建的“all: add opt-in transparent telemetry to Go toolchain”提案也被Go项目接受。

Go工具链中的telemetry是数据驱动的重要一环,最初golang.org/x/telemetry实验项目被建立。在Go 1.23中,go工具链新增了go telemetry子命令,该子命令就是基于golang.org/x/telemetry实验项目,这也是Go团队实现某一个特性的一贯套路。

go telemetry子命令用法大致如下(以最终版本的doc为准):

go telemetry - 打印telemetry mode: on, off or local;
go telemetry on - 设置mode为on;即开启telemetry且上传遥测数据。
go telemetry local - 设置mode为local;即telemetry数据仅存储在本地,但不上传。
go telemetry off - 设置mode为off。即关闭telemetry
go clean -telemetry - 清理本地的遥测数据目录。

3.2 其他一些改变

  • go build(-json)支持以json形式输出构建结果(issue 62067),让构建结果更结构化
  • 移除了对GOROOT_FINAL的支持 (issue 62047),估计很多人不知道或完全没用过GOROOT_FINAL,我也是如此。

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 新增unique包(issue 62483)

unique包的灵感来自于第三方包go4.org/intern,也是为了弥补Go语言缺乏对interning内置支持的空缺。

根据wikipedia的描述,interning是按需重复使用具有同等值对象的技术,减少创建新对象的动作。这种创建模式经常用于不同编程语言中的数和字符串,可以避免不必要的对象重复分配的开销。

Go unique包即是Go的interning机制的实现,unique包提供了一种高效的值去重和快速比较的机制,可以用于优化某些特定场景下的程序性能。

unique包提供给开发人员的API接口非常简单:Make用来创建Handle实例,Handle的方法Value用于获取值的拷贝。下面是一个使用unique包的典型示例:

// go1.23-foresight/lib/unique/main.go
package main

import (
    "fmt"
    "unique"
)

func main() {
    // 创建唯一Handle
    s1 := unique.Make("hello")
    s2 := unique.Make("world")
    s3 := unique.Make("hello")

    // s1和s3是相等的,因为它们是同一个字符串值
    fmt.Println(s1 == s3) // true
    fmt.Println(s1 == s2) // false

    // 从Handle获取原始值
    fmt.Println(s1.Value()) // hello
    fmt.Println(s2.Value()) // world
}

代码和输出结果都不难理解,这类就不赘述了。

4.3 函数迭代器相关

前面说过,函数迭代器转正了。标准库中有一些包立即就提供了一些便利的、可以与函数迭代器一起使用的函数,以slices、maps两个后加入Go标准库的泛型容器包为主。

其中slices包增加了:All、Values、Backward、Collect、AppendSeq、Sortted、SortedFunc、SortedStableFunc和Chunk。maps包增加了All、Keys、Values、Insert和Collect。

我们以slices包的All和Backward来构建一个示例,直观感受一下:

// go1.23-foresight/lib/slices/main.go

package main

import (
    "fmt"
    "slices"
)

func main() {
    sl := []string{"hello", "world", "golang"}

    for i, s := range slices.All(sl) {
        fmt.Printf("%d : %s\n", i, s)
    }

    for i, s := range slices.Backward(sl) {
        fmt.Printf("%d : %s\n", i, s)
    }
}

运行该示例:

$go run main.go
0 : hello
1 : world
2 : golang
2 : golang
1 : world
0 : hello

和以往一样,Go标准库是变化最多的一块儿,但篇幅有限,这里不便枚举,大家可以自行查看Go 1.23里程碑,选择自己关注的标准库变化,并深入研究。

5. 小结

本文主要预览了Go 1.23版本即将带来的新特性和变化。

首先在语言层面,range over func试验特性正式转正,提供统一高效的迭代方式;同时也会修复之前一些悬而未决的规范问题。

其次,在编译器和运行时方面,Go 1.23将优化PGO带来的开销,限制对linkname的使用,优化内存分配器和内联器等。工具链方面,新增telemetry子命令改进遥测能力。

标准库也有不少变化,比如Timer/Ticker的相关修复,新增unique包实现interning机制,以及为函数迭代器新增一些辅助函数。

Go 1.23的Release Notes的编写方式也做了调整,详细内容可参考我的公号文章《Go 1.23 Release Notes编写方式改进!》

总的来说,Go 1.23包含了语法、编译器、运行时、工具链和标准库等多方面的改进,其中最主要集中在编译器性能优化、PGO特性增强、新编译器功能实现以及标准库增强等方面。

不过由于Go 1.23尚未发布,文中部分变化还可能被修改或推迟到下个版本。最终还是以正式发布版为准。文末也列出了一些相关资源链接,方便读者深入了解。

截至发文时,Go 1.23 milestone已经完成59%(https://github.com/golang/go/milestone/212),还有188个open的issue待解决。究竟Go 1.23最终会做出哪些改变,让我们拭目以待!

最后,感谢Go团队以及所有Go 1.23贡献者做出的伟大工作!

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

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/05/27/gopher-rust-first-lesson-first-rust-program

经过上一章的学习,我想现在你已经成功安装好一个Rust开发环境了,是时候撸起袖子开始写Rust代码了!

程序员这个历史并不算悠久的行当,却有着一个历史悠久的传统,那就是每种编程语言都将一个名为“hello, world”的示例作为这门语言学习的第一个例子,这个传统始于20世纪70年代那本大名鼎鼎的由布莱恩·科尼根(Brian W. Kernighan)与C语言之父丹尼斯·里奇(Dennis M. Ritchie)合著的《C程序设计语言》。

在这一章中,我们也将遵从传统,从编写和运行一个可以打印出“hello, world”的Rust示例程序开始我们正式的Rust编码之旅。我希望通过这个示例程序你能够对Rust程序结构有一个直观且清晰的认识。

3.1 Hello, World

“Hello, World”是一门编程语言的最简单示例的表达形式。在Go中,我们可以像下面这样编写Go版本的Hello, World程序:

package main

func main() {
    println("Hello, World!")
}

为了简单,我们甚至没有使用fmt包的Printf系列函数(这样就可以减少一行导入包的语句),而是用了内置函数println来完成将“Hello, World”输出到控制台(更准确的说是标准错误(stderr))的任务。

Rust版本的Hello, World可以比Go还要简洁,我们在一个目录下(比如rust-guide-for-gopher/helloworld/rustc)创建一个hello_world.rs的文件。哦,没错!rust的源码文件都是以.rs作为源文件扩展名的。并且对于多个单词构成的文件名,rust的惯例是采用全小写单词+下划线连接的方式命名。这个hello_world.rs文件的内容如下:

fn main() {
    println!("Hello, World!");
}

相比于Go在每个源文件中都要使用package指定该文件归属的包名,Rust无需这样的一行。和Go一样,这里的main是函数,所有可执行的Rust程序都必须有一个main函数,它是Rust程序的入口函数。和Go使用func函数声明函数不同,Rust声明函数的关键字为fn。在这个main函数中,我们调用println!将“Hello, World!”输出到控制台上。

不过,和Go内置的println函数不同的是,这里的println!并非是一个函数,而是一个Rust宏(macro)

如果你只是学过Go,而没有学过C/C++语言,你甚至都不会知道宏(macro)是什么。在Rust中,宏是一种用于代码生成和转换的元编程工具。宏允许你在编译时根据一定的模式或规则来扩展代码。Rust宏分为声明宏(Declarative Macros)和过程宏(Procedural Macros)。println!就属于声明宏,它由macro_rules! 宏定义,我们在Rust标准库的源码中可以看到其定义:

// $(rustc --print sysroot)/lib/rustlib/src/rust/library/std/src/macros.rs

#[macro_export]
#[stable(feature = "rust1", since = "1.0.0")]
#[cfg_attr(not(test), rustc_diagnostic_item = "println_macro")]
#[allow_internal_unstable(print_internals, format_args_nl)]
macro_rules! println {
    () => {
        $crate::print!("\n")
    };
    ($($arg:tt)*) => {{
        $crate::io::_print($crate::format_args_nl!($($arg)*));
    }};
}

在Rust源码编译过程中,声明宏是在最开始的预处理阶段进行扩展的,我们也可以通过nightly版的rustc命令来查看println!宏展开后的结果(-Z选项只能在nightly版本中使用):

$rustc +nightly-2022-07-14-x86_64-apple-darwin  -Zunpretty=expanded  hello_world.rs
#![feature(prelude_import)]
#![no_std]
#[prelude_import]
use ::std::prelude::rust_2015::*;
#[macro_use]
extern crate std;
fn main() {
    {
        ::std::io::_print(::core::fmt::Arguments::new_v1(&["Hello, World!\n"],
                &[]));
    };
}

我们看到:println!宏被替换为一个标准库下的函数(_print)的调用。btw,到这里,你可能和我一样,看不懂println!展开后的代码,没关系,我们后续会逐步学习并掌握这些语法的。此外,宏是Rust的高级特性,这里也不展开说了。

另外一个和Go在语法上有所不同的是,Rust在每行语句后面都要显式使用分号,对于Gopher而言,这个很容易遗忘。

接下来,我们来编译和运行一下这个Rust版的Hello,World!,编译运行Rust代码的最简单方法就是通过rustc编译器将rust源码文件编译为可执行程序:

$rustc hello_world.rs

$ls
hello_world*        hello_world.rs

我们看到,示例通过调用rustc将hello_world.rs编译为了hello_world可执行文件。

运行rustc编译后的可执行文件将得到下面输出结果:

$./hello_world
Hello, World!

我们看到”Hello, World!”被打印到控制台。

如果觉得默认编译出的hello_world文件名字较长,我们也可以像go build -o那样指定rustc编译后得到的目标可执行文件的名字,下面的命令通过-o选项将编译后的程序命名为hello:

$rustc -o hello hello_world.rs

rustc编译出来的二进制文件size并不大,仅有400多KB(而Go默认构建的Hello, World!有1.3MB,在我的macOS上):

$ls -lh
total 856
-rwxr-xr-x  1 tonybai  staff   423K  4 20 17:56 hello_world*

我们还可以通过去掉symbols的方式继续让其“瘦身”到不到300KB(通过go build -ldflags=”-s -w” helloworld.go去除符号表和调试信息的Go二进制程序还有近900K的大小):

$rustc -C strip=symbols hello_world.rs
$ll -h
total 608
-rwxr-xr-x  1 tonybai  staff   297K  4 20 17:57 hello_world*

上面的”Hello, World”程序虽然足够简单,也能够运行,但对于初学者而言,它有两个“不足”:一来这个例子的确“太简单”,简单到无法充分展示单个Rust源码文件的结构;二来这个示例只使用了一个单个源文件,与实际开发中那种由多个文件组成的Rust实用工程有差别,同样无法帮助我们理解实用性的Rust工程的结构。

为了更好地理解Rust工程与单个源文件的构成,我们将编写一个稍微复杂一点的版本,它将使用Rust的构建管理工具cargo建立,并使用Rust标准库中的std::io模块进行输入/输出操作。

3.2 cargo版本的Hello, World

在实际开发中,Rust程序通常由多个源文件组成,并使用Cargo作为构建系统和包管理器。Cargo可以帮助我们管理项目的源代码、依赖库、构建任务等。下面我们就来创建一个使用Cargo的”Hello, World”。

3.2.1 使用Cargo创建Hello,World

我们在一个目录下(比如:rust-guide-for-gopher/helloworld/cargo)执行下面命令来创建hello_world:

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

cargo默认创建了一个binary(application)类型的rust package,我们来看看初始情况下这个rust package下都有哪些内容:

$tree hello_world
hello_world
├── Cargo.toml
└── src
    └── main.rs

1 directory, 2 files

其中,Cargo.toml是Rust包的清单(manifest)文件。它包含有关包及其依赖项的元数据。以下是上面Cargo.toml文件的全部内容:

// 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]

其中package下面的字段含义如下:

  • name: 包的名称;
  • version: 包的版本,遵循语义化版本控制规则;
  • edition: 包使用的Rust版本(edition)。在这里,它被设置为目前的最新edition:2021版。edition提供了一种向后兼容的方式来演化和改进Rust。每个edition都是向后兼容的,这意味着旧edition下编写的Rust代码可以继续在新edition版本的Rust下编译和运行,而无需进行修改。这样,开发者可以按照自己的节奏选择是否迁移到新的edition。

dependencies下面则是会记录该package对第三方依赖的情况,这个示例中并无三方依赖,因此这里为空。

我们的代码放在了src目录下,这也是rust包的标准布局。为了更好地理解Rust程序的构成,我们将编写一个稍微复杂一点的Hello, World!版本,它使用Rust标准库中的std::io模块进行输入/输出操作:

// rust-guide-for-gopher/helloworld/cargo/hello_world/src/main.rs
use std::io;
use std::io::Write;

fn main() {
    let mut output = io::stdout();
    output.write(b"Hello, World!").unwrap();
    output.flush().unwrap();
}

这个Rust的”Hello, World”程序展示了一个典型的Rust源文件结构,包括导入语句、主函数定义以及一系列的方法调用。它演示了如何使用标准库的io模块来向标准输出流打印”Hello, World!”。下面是对其程序结构的简单总结:

  1. 导入语句

源文件在最开始处使用use std::io; 和use std::io::Write;这两行导入了标准库中的io模块及其Write trait。这样程序就可以在后面的代码中直接使用io和Write,而无需完整地写出它们的命名空间。这里我们先不用关心trait是什么,你大可将其理解为和Go interface差不多的语法元素就行了。

  1. 主函数

main定义了程序的入口点。Rust 程序从main函数开始执行。

  1. 可变变量

let mut output = io::stdout(); 这行代码创建了一个可变变量output,它绑定到了一个标准输出流(stdout)。mut关键字表示该变量是可变的,可以在后续代码中修改它的值。关于变量以及绑定,我们在后面有专门的章节说明。这里要注意的是,和Go变量不同的是,Rust中的变量默认是不可变的,只有显式用mut声明的变量才是可变的。

  1. 方法调用

output.write(b”Hello, World!”).unwrap(); 调用了output的write方法,传递了一个字节串作为参数。该方法用于将字节写入输出流。unwrap方法用于处理方法调用可能产生的错误,它在这里表示“我相信这个方法调用会成功,如果不成功,就让程序 panic”。同理,output.flush().unwrap()也是这样的。关于错误以及异常处理的话题,我们会在后面进行专题性学习。

理解了源码后,我们来编译和运行一下这个程序,这次我们不再使用rustc,而是用cargo来实现。

3.2.2 使用Cargo构建Hello, World

要构建上面的示例程序,我们只需在项目根目录下运行下面命令:

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

构建成功后,我们再来查看一下当前项目下的结构变化:

$tree -F
.
├── Cargo.lock
├── Cargo.toml
├── src/
│   └── main.rs
└── target/
    ├── CACHEDIR.TAG
    └── debug/
        ├── build/
        ├── deps/
        │   ├── hello_world-07284f5d84374479*
        │   ├── hello_world-07284f5d84374479.1atc14vk0u28taij.rcgu.o
        │   ├── hello_world-07284f5d84374479.1bu89c2i9mazzqif.rcgu.o
        │   ├── hello_world-07284f5d84374479.26e3nxhmk9lhy9zy.rcgu.o
        │   ├── hello_world-07284f5d84374479.29l81xyv0i4g8s88.rcgu.o
        │   ├── hello_world-07284f5d84374479.41i7ln85cwseljfw.rcgu.o
        │   ├── hello_world-07284f5d84374479.4iz3ubiqrvegnjdp.rcgu.o
        │   ├── hello_world-07284f5d84374479.53vu8cjirf8g6rnw.rcgu.o
        │   ├── hello_world-07284f5d84374479.5f6ye0ayl23rccqv.rcgu.o
        │   └── hello_world-07284f5d84374479.d
        ├── examples/
        ├── hello_world*
        ├── hello_world.d
        └── incremental/
            └── hello_world-16yuztatbr0vh/
                ├── s-gvfwmugno5-1gy801r-1i2g78r4nmg489ix0nuktmqgb/
                │   ├── 1atc14vk0u28taij.o
                │   ├── 1bu89c2i9mazzqif.o
                │   ├── 26e3nxhmk9lhy9zy.o
                │   ├── 29l81xyv0i4g8s88.o
                │   ├── 41i7ln85cwseljfw.o
                │   ├── 4iz3ubiqrvegnjdp.o
                │   ├── 53vu8cjirf8g6rnw.o
                │   ├── 5f6ye0ayl23rccqv.o
                │   ├── dep-graph.bin
                │   ├── query-cache.bin
                │   └── work-products.bin
                └── s-gvfwmugno5-1gy801r.lock*

9 directories, 28 files

我们看到cargo build执行后,项目下多出了好多目录和文件。这些目录和文件都是做什么的呢?我们挑选主要的来看一下。

  • Cargo.lock文件

Cargo的锁定文件,用于记录每个依赖项的确切版本号,以保证构建的可重复性。

这个示例中由于没有使用第三方依赖,这个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"

另外Cargo.lock文件完全由cargo自动管理,开发人员不需要也不应该对其进行手动修改。

  • target目录

存放构建输出的目录,用于存储编译后的目标文件和可执行文件。

  • target/CACHEDIR.TAG

用于标记target目录为一个缓存目录的文件。它的内容如下:

$cat CACHEDIR.TAG
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/

这是一个符合Cache Directory Tagging Specification的Tag文件。

  • target/debug

调试模式下的构建输出目录,存储生成的可执行文件和相关文件。

  • target/debug/incremental

增量编译的目录,用于存储增量编译过程中的临时文件和缓存。

Rust编译过程缓慢,这个对比Go简直就是地下天上。在日常开发中,基于增量编译的文件进行增量构建可以大幅缩短编译时间。

  • target/debug/build

编译过程中生成的临时构建文件的目录。

  • target/debug/deps

存储编译生成的目标文件(.o 文件)和相关的依赖项。

  • target/debug/hello_world

调试模式下生成的可执行文件。

  • target/debug/hello_world.d

与hello_world相关的依赖关系信息的文件。

执行debug目录下的hello_world将得到如下输出:

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

在Go中我们可以使用go run来直接编译和运行Go源码文件,cargo也提供了该功能,我们在项目根目录下运行cargo run也可以编译和执行hello_world:

$cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/hello_world`
Hello, World!

无论是cargo run还是cargo build,默认构建的都是debug版本的可执行程序,程序中包含大量符号信息和调试信息,并且其优化级别也不是很高。发布到生产环境的程序应该是release模式下的,通过–release参数,我们可以构建release版本的可执行程序:

$cargo build --release
   Compiling hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/helloworld/cargo/hello_world)
    Finished release [optimized] target(s) in 1.06s

构建后,target目录下会多出一个release目录,其下面的内容如下:

$tree -F target/release
target/release
├── build/
├── deps/
│   ├── hello_world-c41defdc625f9244*
│   └── hello_world-c41defdc625f9244.d
├── examples/
├── hello_world*
├── hello_world.d
└── incremental/

4 directories, 4 files

相对于debug版本,release版本由于实时了大量优化,通常其构建时间会比debug版本要长。但构建出的release版本的size则要小很多。

无论是debug,还是release版,target下面都生成了许多中间文件,如果要清理文件并重头构建,我们可以使用cargo clean命令将target彻底清除:

$cargo clean
     Removed 40 files, 2.1MiB total

当然cargo clean也支持一些命令行参数,可以选择清除哪些文件。

3.2.3 使用Cargo创建library类包

通过上面的例子,我们知道cargo new默认创建的binary类型的rust package,如果我们要创建library类型的rust package,我们需要向cargo new传递–lib选项。下面的命令创建一个名为foo的library类型的rust package:

$cargo new --lib foo
     Created library `foo` package

我们看一下foo package下的目录结构:

$tree -F foo
foo
├── Cargo.toml
└── src/
    └── lib.rs

1 directory, 2 files

和binary类不同的是,src目录下不再是main.rs,而是lib.rs,它是library类package的入口:

//rust-guide-for-gopher/helloworld/cargo/foo/lib.rs

pub fn add(left: usize, right: usize) -> usize {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

lib.rs中只是一个library类package的入口模板,开发人员需要根据自己的需要对其进行调整。关于lib.rs中的内容,我们将在下一章讲解Rust代码组织时做细致说明,这里就不展开说了。

对于library类Rust package,我们同样可以通过cargo build和cargo build –release构建,下面是执行构建后目录文件情况:

$tree
.
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── target
    ├── CACHEDIR.TAG
    ├── debug
    │   ├── build
    │   ├── deps
    │   │   ├── foo-24c6d6228c521501.2k5t0f94hnorqpgh.rcgu.o
    │   │   ├── foo-24c6d6228c521501.d
    │   │   ├── libfoo-24c6d6228c521501.rlib
    │   │   └── libfoo-24c6d6228c521501.rmeta
    │   ├── examples
    │   ├── incremental
    │   │   └── foo-m2biu8poxl6i
    │   │       ├── s-gvg68shtlp-1oqrf4n-irxhgoe7rhwmtvj6jwexcu0h
    │   │       │   ├── 2k5t0f94hnorqpgh.o
    │   │       │   ├── dep-graph.bin
    │   │       │   ├── query-cache.bin
    │   │       │   └── work-products.bin
    │   │       └── s-gvg68shtlp-1oqrf4n.lock
    │   ├── libfoo.d
    │   └── libfoo.rlib
    └── release
        ├── build
        ├── deps
        │   ├── foo-9f2dd76beda509bd.d
        │   ├── libfoo-9f2dd76beda509bd.rlib
        │   └── libfoo-9f2dd76beda509bd.rmeta
        ├── examples
        ├── incremental
        ├── libfoo.d
        └── libfoo.rlib

14 directories, 20 files

我们看到,无论是debug还是release,cargo build构建的结果都是libfoo.rlib。.rlib文件是Rust的静态库文件,通常用于代码的模块化和重用,我们在后续章节讲解中,会详细说明如何使用这些构建出来的静态库。

3.3 小结

本文介绍了如何使用Rust编写”Hello, World”程序,并分别给出了rustc版和cargo版的hello, world程序版本。

在这个过程中,文章还介绍了Rust中的宏概念,并展示了如何使用println!宏来输出文本。

之后,文章聚焦于使用Cargo构建的hello,world程序版本,介绍了cargo的构建、清理、debug和release版本的区别等,最后还提及了如何使用cargo创建library类的Rust package。

cargo贯穿Rust程序的整个生命周期,在后续的每一章中可能都会提及cargo。

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


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 商务合作请联系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