标签 谷歌 下的文章

Go语言数据竞争检测与数据竞争模式

本文永久链接 – https://tonybai.com/2022/06/21/data-race-detection-and-pattern-in-go

uber,就是那个早早退出中国打车市场的优步,是Go语言早期接纳者,也是Go技术栈的“重度用户”。uber内部的Go代码仓库有5000w+行Go代码,有2100个Go实现的独立服务,这样的Go应用规模在世界范围内估计也是Top3了吧。

uber不仅用Go,还经常输出它们使用Go的经验与教训,uber工程博客就是这些高质量Go文章的载体,这些文章都值得想“深造”的gopher们反复阅读和体会。

近期该博客发布了两篇有关Go并发数据竞争的文章,一篇为《Dynamic Data Race Detection in Go Code》,另一篇为《Data Race Patterns in Go》。这两篇文章也源于uber工程师发表在arxiv上的预印版论文《A Study of Real-World Data Races in Golang》

感慨一下:不得不佩服国外工程师的这种“下得了厨房,还上得了厅堂”的研发能力,这也是我在团队中为大家树立的目标。

这里和大家过一下这两篇精简版的博客文章,希望我们都能有收获。


一. Go内置data race detector

我们知道:并发程序不好开发,更难于调试。并发是问题的滋生地,即便Go内置并发并提供了基于CSP并发模型的并发原语(goroutine、channel和select),实际证明,现实世界中,Go程序带来的并发问题并没有因此减少(手动允悲)。“没有银弹”再一次应验

不过Go核心团队早已意识到了这一点,在Go 1.1版本中就为Go工具增加了race detector,通过在执行go工具命令时加入-race,该detector可以发现程序中因对同一变量的并发访问(至少一个访问是写操作)而引发潜在并发错误的地方。Go标准库也是引入race detector后的受益者。race detector曾帮助Go标准库检测出42个数据竞争问题

race detector基于Google一个团队开发的工具Thread Sanitizer(TSan)(除了thread sanitizer,google还有一堆sanitizer,比如:AddressSanitizer, LeakSanitizer, MemorySanitizer等)。第一版TSan的实现发布于2009年,其使用的检测算法“源于”老牌工具Valgrind。出世后,TSan就帮助Chromium浏览器团队找出近200个潜在的并发问题,不过第一版TSan有一个最大的问题,那就是慢!

因为有了成绩,开发团队决定重写TSan,于是就有了v2版本。与V1版本相比,v2版本有几个主要变化:

  • 编译期注入代码(instrumentation);
  • 重新实现运行时库,并内置到编译器(LLVM和GCC)中;
  • 除了可以做数据竞争(data race)检测外,还可以检测死锁、加锁状态下的锁释放等问题;
  • 与V1版本相比,v2版本性能提升约20倍;
  • 支持Go语言。

那么TSan v2究竟是怎么工作的呢?我们继续往下看。

二. ThreadSanitizer v2版本工作原理

根据Thread Sanitizer wiki上对v2版算法的描述,Thread Sanitizer分为两部分:注入代码与运行时库

1. 注入代码

第一部分是在编译阶段配合编译器在源码中注入代码。那么在什么位置注入什么代码呢?前面说过Thread Sanitizer会跟踪程序中的每次内存访问,因此TSan会在每次内存访问的地方注入代码,当然下面的情况除外:

  • 肯定不会出现数据竞争的内存访问

比如:全局常量的读访问、函数中对已被证明不会逃逸到堆上的内存的访问;

  • 冗余访问:写入某个内存位置之前发生的读操作
  • … …

那么注入的什么代码呢?下面是一个在函数foo内写内存操作的例子:

我们看到对地址p的写操作前注入了__tsan_write4函数,函数foo的入口和出口分别注入了__tsan_func_entry和 __tsan_func_exit。而对于需要注入代码的内存读操作,注入代码则是__tsan_read4;原子内存操作使用__tsan_atomic进行注入…。

2. TSan运行时库

一旦在编译期注入代码完毕,构建出带有TSan的Go程序,那么在Go程序运行阶段,起到数据竞争检测作用的就是Tsan运行时库了。TSan是如何检测到有数据竞争的呢?

TSan的检测借助了一个称为Shadow Cell的概念。什么是Shadow Cell呢?一个Shadow Cell本身是一个8字节的内存单元,它代表一个对某个内存地址的读/写操作的事件,即每次对某内存块的写或读操作都会生成一个Shadow Cell。显然Shadow Cell作为内存读写事件的记录者,其本身存储了与此事件相关的信息,如下图:

我们看到,每个Shadow Cell记录了线程ID、时钟时间、操作访问内存的位置(偏移)和长度以及该内存访问事件的操作属性(是否是写操作)。针对每个应用程序的8字节内存,TSan都会对应有一组(N个)Shadow Cell,如下图:

N可以取2、4和8。N的取值直接影响TSan带来的开销以及data race检测的“精度”。

3. 检测算法

有了代码注入,也有了记录内存访问事件的Shadow Cell,那么TSan是通过什么逻辑检测data race的呢?我们结合Google大神Dmitry Vyukov在一次speak中举的例子来看一下检测算法是怎么运作的:

我们以N=8为例(即8个Shadow Cell用于跟踪和校验一个应用的8字节内存块),下面是初始情况,假设此时尚没有对该8字节应用内存块的读写操作:

现在,一个线程T1向该块内存的前两个字节进行了写操作,写操作会生成第一个Shadow Cell,如下图所示:

这里我们结合图中的Shadow Cell说说Pos字段。Pos字段描述的是写/读操作访问的8字节内存单元的起始偏移与长度,比如这里的0:2代表的就是起始字节为第一个字节,长度为2个字节。此时Shadow Cell窗口只有一个Shadow Cell,不存在race的可能。

接下来,一个线程T2又针对该块内存的后四个字节进行了一次读操作,读操作会生成第二个Shadow Cell,如下图所示:

此次读操作涉及的字节与第一个Shadow Cell没有交集,不存在data race的可能。

再接下来,一个线程T3针对该块内存的前四个字节进行了一次写操作,写操作会生成第三个Shadow Cell,如下图所示:

我们看到T1和T3两个线程对该内存块的访问有重叠区域,且T1为写操作,那么这种情况就有可能存在data race。而TSan的race检测算法本质上就是一个状态机,每当发生一次内存访问,都会走一遍状态机。状态机的逻辑也很简单,就是遍历这块内存对应的Shadow Cell窗口中的所有Cell,用最新的Cell与已存在的Cell逐一比对,如果存在race,则给出warning。

像这个例子中T1的write与T3的read区域重叠,如果Shallow Cell1的时钟E1没有happens-before Shadow Cell的时钟E3,那么就存在data race的情况。happens-before如何判定,我们可以从tsan的实现中找到端倪:

https://code.woboq.org/gcc/libsanitizer/tsan/tsan_rtl.cc.html

static inline bool HappensBefore(Shadow old, ThreadState *thr) {
    return thr->clock.get(old.TidWithIgnore()) >= old.epoch();
}

在这个例子中,对应一个8字节应用内存的一组Shadow Cell的数量为N=8,但内存访问是高频事件,因此很快Shadow Cell窗口就会写满,那么新的Shadow Cell存储在哪里呢?在这种情况下,TSan算法会随机删除一个old Shadow Cell,并将新Shadow Cell写入。这也印证了前面提到的:N值的选取会在一定程度上影响到TSan的检测精度。

好了,初步了解了TSan v2的检测原理后,我们再回到uber的文章,看看uber是在何时部署race检测的。

三. 何时部署一个动态的Go数据竞争检测器

通过前面对TSan原理的简单描述我们也可以看出,-race带来的数据竞争检测对程序运行性能和开销的影响还是蛮大的。Go官方文档《Data Race Detector》一文中给出使用-race构建的Go程序相较于正常构建的Go程序,运行时其内存开销是后者的5-10倍,执行时间是2-20倍。但我们知道race detector只能在程序运行时才能实施数据竞争问题的检测。因此,Gopher在使用-race都会比较慎重,尤其是在生产环境中。 2013年,Dmitry Vyukov和Andrew Gerrand联合撰写的介绍Go race detector的文章“introducing the go race detector”中也直言:在生产环境一直开着race detector是不实际的。他们推荐两个使用race detector的时机:一个是在测试执行中开启race detector,尤其是集成测试和压力测试场景下;另外一个则是在生产环境下开启race detector,但具体操作是:仅在众多服务实例中保留一个带有race detector的服务实例,但有多少流量打到这个实例上,你自己看着办^_^。

那么,uber内部是怎么做的呢?前面提到过:uber内部有一个包含5000w+行代码的单一仓库,在这个仓库中有10w+的单元测试用例。uber在部署race detector的时机上遇到两个问题:

  • 由于-race探测结果的不确定性,使得针对每个pr进行race detect的效果不好。

比如:某个pr存在数据竞争,但race detector执行时没有检测到;后来的没有data race的PR在执行race detect时可能会因前面的pr中的data race而被检测出问题,这就可能影响该pr的顺利合入,影响相关开发人员的效率。

同时,将已有的5000w+代码中的所有data race情况都找出来本身也是不可能的事情。

  • race detector的开销会影响到SLA(我理解是uber内部的CI流水线也有时间上的SLA(给开发者的承诺),每个PR跑race detect,可能无法按时跑完),并且提升硬件成本

针对上述这两个问题,给出的部署策略是“事后检测”,即每隔一段时间,取出一版代码仓库的快照,然后在-race开启的情况下,把所有单元测试用例跑一遍。好吧,似乎没有什么新鲜玩意。很多公司可能都是这么做的。

发现data race问题,就发报告给相应开发者。这块uber工程师做了一些工作,通过data race检测结果信息找出最可能引入该bug的作者,并将报告发给他。

不过有一个数据值得大家参考:在没有data race检测的情况下,uber内部跑完所有单元测试的时间p95位数是25分钟,而在启用data race后,这个时间增加了4倍,约为100分钟。

uber工程师在2021年中旬实施的上述实验,在这一实验过程中,他们找到了产生data race的主要代码模式,后续他们可能会针对这些模式制作静态代码分析工具,以更早、更有效地帮助开发人员捕捉代码中的data race问题。接下来,我们就来看看这些代码模式。

四. 常见的数据竞争模式都有哪些

uber工程师总结了7类数据竞争模式,我们逐一看一下。

1. 闭包的“锅”

Go语言原生提供了对闭包(closure)的支持。在Go语言中,闭包就是函数字面值。闭包可以引用其包裹函数(surrounding function)中定义的变量。然后,这些变量在包裹函数和函数字面值之间共享,只要它们可以被访问,这些变量就会继续存在。

不过不知道大家是否意识到了Go闭包对其包裹函数中的变量的捕捉方式都是通过引用的方式。而不像C++等语言那样可以选择通过值方式(by value)还是引用方式(by reference)进行捕捉。引用的捕捉方式意味着一旦闭包在一个新的goroutine中执行,那么两个goroutine对被捕捉的变量的访问就很大可能形成数据竞争。“不巧的”的是在Go中闭包常被用来作为一个goroutine的执行函数。

uber文章中给出了三个与这种无差别的通过引用方式对变量的捕捉方式导致的数据竞争模式的例子:

  • 例子1

这第一个例子中,每次循环都基于一个闭包函数创建一个新的goroutine,这些goroutine都捕捉了外面的循环变量job,这就在多个goroutine之间建立起对job的竞争态势。

  • 例子2

例子2中闭包与变量声明作用域的结合共同造就了新goroutine中的err变量就是外部Foo函数的返回值err。这就会造成err值成为两个goroutine竞争的“焦点”。

  • 例子3

例子3中,具名返回值变量result被作为新goroutine执行函数的闭包所捕获,导致了两个goroutine在result这个变量上产生数据竞争。

2. 切片的“锅”

切片是Go内置的复合数据类型,与传统数组相比,切片具备动态扩容的能力,并且在传递时传递的是“切片描述符”,开销小且固定,这让其在Go语言中得到了广泛的应用。但灵活的同时,切片也是Go语言中“挖坑”最多的数据类型之一,大家在使用切片时务必认真细致,稍不留神就可能犯错。

下面是一个在切片变量上形成数据竞争的例子:

从这份代码来看,开发人员虽然对被捕捉的切片变量myResults通过mutex做了同步,但在后面创建新goroutine时,在传入切片时却因没有使用mutex保护。不过例子代码似乎有问题,传入的myResults似乎没有额外的使用。

3. map的“锅”

map是Go另外一个最常用的内置复合数据类型, 对于go入学者而言,由map导致的问题可能仅次于切片。go map并非goroutine-safe的,go禁止对map变量的并发读写。但由于是内置hash表类型,map在go编程中得到了十分广泛的应用。

上面例子就是一个并发读写map的例子,不过与slice不同,go在map实现中内置了对并发读写的检测,即便不加入-race,一旦发现也会抛出panic。

4. 误传值惹的祸

Go推荐使用传值语义,因为它简化了逃逸分析,并使变量有更好的机会被分配到栈中,从而减少GC的压力。但有些类型是不能通过传值方式传递的,比如下面例子中的sync.Mutex:

sync.Mutex是一个零值可用的类型,我们无需做任何初始赋值即可使用Mutex实例。但Mutex类型有内部状态的:

通过传值方式会导致状态拷贝,失去了在多个goroutine间同步数据访问的作用,就像上面例子中的Mutex类型变量m那样。

5. 误用消息传递(channel)与共享内存

Go采用CSP的并发模型,而channel类型充当goroutine间的通信机制。虽然相对于共享内存,CSP并发模型更为高级,但从实际来看,在对CSP模型理解不到位的情况下,使用channel时也十分易错。

这个例子中的问题在于Start函数启动的goroutine可能阻塞在f.ch的send操作上。因为,一旦ctx cancel了,Wait就会退出,此时没有goroutine再在f.ch上阻塞读,这将导致Start函数启动的新goroutine可能阻塞在“f.ch <- 1”这一行上。

大家也可以看到,像这样的问题很细微,如果不细致分析,很难肉眼识别出来。

6. sync.WaitGroup误用导致data race问题

sync.WaitGroup是Go并发程序常用的用于等待一组goroutine退出的机制。它通过Add和Done方法实现内部计数的调整。而Wait方法用于等待,直到内部计数器为0才会返回。不过像下面例子中的对WaitGroup的误用会导致data race问题:

我们看到例子中的代码将wg.Add(1)放在了goroutine执行的函数中了,而没有像正确方法那样,将Add(1)放在goroutine创建启动之前,这就导致了对WaitGroup内部计数器形成了数据竞争,很可能因goroutine调度问题,是的Add(1)在未来得及调用,从而导致Wait提前返回。

下面这个例子则是由于defer函数在函数返回时的执行顺序问题,导致两个goroutine在locationErr这个变量上形成数据竞争:

main goroutine在判断locationErr是否为nil的时候,另一个goroutine中的doCleanup可能执行,也可能没有执行。

7. 并行的表驱动测试可能引发数据竞争

Go内置单测框架,并支持并行测试(testing.T.Parallel())。但如若使用并行测试,则极其容易导致数据竞争问题,原文没有给出例子,这个大家自行体会吧。

五. 小结

关于data race的代码模式,在uber发布这两篇文章之前,也有一些资料对数据竞争问题的代码模式进行了分类整理,比如下面两个资源,大家可以参照着看。

  • 《Data Race Detector》- https://go.dev/doc/articles/race_detector
  • 《ThreadSanitizer Popular Data Races》- https://github.com/google/sanitizers/wiki/ThreadSanitizerPopularDataRaces中的模式

在刚刚发布的Go 1.19beta1版本中提到,最新的-race升级到了TSan v3版本,race检测性能相对于上一版将提升1.5倍-2倍,内存开销减半,并且没有对goroutine的数量的上限限制。

注:Go要使用-race,则必须启用CGO。

// runtime/race.go

//go:nosplit
func raceinit() (gctx, pctx uintptr) {
    // cgo is required to initialize libc, which is used by race runtime
    if !iscgo {
        throw("raceinit: race build must use cgo")
    }
    ... ...
}

六. 参考资料

  • “Finding races and memory errors with compiler instrumentation” – http://gcc.gnu.org/wiki/cauldron2012?action=AttachFile&do=get&target=kcc.pdf
  • 《Race detection and more with ThreadSanitizer 2》 – https://lwn.net/Articles/598486/
  • 《Google ThreadSanitizer — 排查多线程问题data race的大杀器》- https://zhuanlan.zhihu.com/p/139000777
  • 《Introducing the Go Race Detector》- https://go.dev/blog/race-detector
  • ThreadSanitizer Algorithm V2 – https://github.com/google/sanitizers/wiki/ThreadSanitizerAlgorithm
  • paper: FastTrack: Efficient and Precise Dynamic Race Detection – https://users.soe.ucsc.edu/~cormac/papers/pldi09.pdf
  • paper: Eraser: A Dynamic Data Race Detector for Multithreaded Programs – https://homes.cs.washington.edu/~tom/pubs/eraser.pdf

“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

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

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

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

Rust vs. Go:为什么强强联合会更好

本文翻译自乔纳森·特纳(Jonathan Turner)和史蒂夫·弗朗西亚(Steve Francia)的文章《Rust vs. Go: Why They’re Better Together》

史蒂夫·弗朗西亚(Steve Francia):在过去的25年里,Steve Francia建立了一些最具创新性和成功的技术和公司,这些技术和公司已经成为云计算的基础,被全世界的企业和开发者所接受。他目前是谷歌Go编程语言的产品和战略负责人。他是Hugo、Cobra、Viper、spf13-vim和许多其他开源项目的创建者,拥有领导世界上最大的五个开源项目的独特荣誉。

乔纳森-特纳(Jonathan Turner)在开源领域工作了20多年,从小型项目到大型项目,包括帮助微软向开源转型。他是创建TypeScript团队的一员,并作为项目经理和设计团队的负责人帮助其成长。他还作为Rust社区成员和Mozilla Rust团队的一员参与Rust的工作,包括共同设计Rust的错误信息和IDE支持。

虽然其他人可能认为RustGo是竞争性的编程语言,但Rust和Go团队却都不这么认为。恰恰相反,我们的团队非常尊重其他团队正在做的事情,并认为这两种编程语言是相辅相成的,有着共同的愿景,即在整个行业内实现软件开发状态的现代化。

在本文中,我们将讨论Rust和Go的优缺点、它们如何相互补充和支持以及我们对每种语言的最佳使用时机的建议。

一些公司正在发掘采用这两种语言的价值以及它们的互补价值。为了从我们的观点转向用户的实际体验,我们采访了三家这样的公司,DropboxFastlyCloudflare,讲述了他们共同使用Go和Rust的经验。他们的经验之谈将被引用并贯穿本文,为大家提供更进一步的观点。

1. 语言比较

编程语言 Go Rust
创建时间 2009 2010
创建于 谷歌 Mozilla
知名项目 Kubernetes,Docker,Github CLI,Hugo,Caddy,Drone,Ethereum,Syncthing,Terraform Firefox, ripgrep, alacritty, deno, Habitat
典型用途 APIs, Web Apps, CLI apps, DevOps, Networking, Data Processing, cloud apps IoT, processing engines, security-sensitive apps, system components, cloud apps
开发者采用 8.8%(第12名) 5.1%(第19名)
开发者最爱 62.3%(第5名) 86.1%(第1名)
开发最想要 17.9%(第3名) 14.6%(第5名)

2. 相似之处

Go和Rust有很多共同点。两者都是现代软件语言,都是出于为影响软件开发的问题提供一个安全和可扩展的解决方案的需要而诞生的。两者都是为了应对创建者在行业内现有语言中遇到的缺点而创建的,尤其是开发者生产力、可扩展性、安全性和并发性方面的缺点。

当今流行的大多数语言都是30多年前设计的。当这些语言被设计出来的时候,与今天有五个关键的区别:

  • 摩尔定律被认为是永恒不变的。
  • 大多数软件项目都是由小团队编写的,并且经常一个人单干。
  • 大多数软件有相对较少的依赖性,大多数是专有的。
  • 安全性是次要的考虑因素……或者根本不是考虑因素。
  • 软件通常是为单一平台编写的。

相比之下,Rust和Go都是为今天的世界而写的,并都采取了相似的方法来设计一种适合今天开发需求的语言。

1) 性能和并发

Go和Rust都是专注于生产高效代码的编译语言。它们还可以方便地使用当今机器的多个处理器,使它们成为编写高效并行代码的理想语言。

“使用Go使得MercadoLibre公司将他们用于这项服务的服务器数量减少到原来的八分之一(从32台服务器减少到4台),另外,每台服务器可以用更少的功率运行(原来是4个CPU核,现在减少到2个CPU核)。有了Go,该公司省去了88%的服务器,并将剩余服务器上的CPU削减了一半–产生了巨大的成本节约。”–“MercadoLibre与Go一起成长”

“在我们严格管理的环境中,在我们运行Go代码的环境中,我们看到CPU减少了大约百分之十[与C++相比],代码更干净,更可维护。” – Bala Natarajan,Paypal

“在AWS,我们也很喜欢Rust,因为它能帮助AWS编写高性能、安全的基础设施级网络和其他系统软件。亚马逊第一个用Rust构建的重要产品Firecracker于2018年公开发布,它提供了开源虚拟化技术,为AWS Lambda和其他无服务器产品提供动力。但我们也使用Rust来提供亚马逊简单存储服务(Amazon S3)、亚马逊弹性计算云(Amazon EC2)、Amazon CloudFront、Amazon Route 53等服务。最近,我们推出了基于Linux的容器操作系统Bottlerocket,它是用Rust编写的。” – Matt Asay,亚马逊网络服务

我们”看到我们的速度非凡地提高了1200-1500%! 我们从实现了较少解析规则的Scala的模式下的300-450ms,到实现了更多解析模式的Rust模式下的25-30ms!” – Josh Hannaford,IBM

2) 团队可扩展—-可审查

今天的软件开发是由团队建立的,这些团队不断成长和扩大,经常使用源码控制以分布式的方式进行协作。Go和Rust都是针对团队的工作方式而设计的,通过消除不必要的担忧,如格式(比如go的gofmt)、安全和复杂的组织,来改善代码审查。这两种语言都需要相对较少的上下文来理解代码的工作,使审查人员能够更快速地使用其他人编写的代码,并审查团队成员的代码和你团队以外的开源开发人员贡献的代码。

“我早期的职业生涯有Java和Ruby的背景,构建Go和Rust代码对我来说就像卸下了无法承受的重担。当我在Google时,遇到用Go编写的服务让我很欣慰,因为我知道它易于构建和运行。Rust的情况也是如此,尽管我只是在更小的工作范围内使用了它。我希望无限可配置的构建系统的日子已经过去了,而语言都有自己的专用构建工具,开箱即用。”– Sam Rose,CV合伙人

“用Go写服务的时候,我往往会松一口气,因为与动态语言相比,Go的静态类型系统非常简单,易于推理,并发性是一等公民,Go的标准库既无比精致强大,又切中要害。安装一个标准的Go,再使用一个grpc库和一个数据库连接器,你在服务器端几乎不需要其他的东西,每个工程师都能看懂代码,看懂库。在用Rust编写模块时,Dropbox工程师在2019年Async-await稳定下来之前,感受到了Rust在服务器端的成长之痛,但从那时起,crate(译注:Rust中的概念)正在趋向于使用它,我们得到了Async模式并从并发中受益。” – Daniel Reiter Horn,Dropbox

3) 开放源码意识

今天一般软件项目所使用的依赖关系数量是惊人的。长达几十年的软件重用目标在现代开发中已经实现,今天的软件可能是复用了100多个项目而构建的。为此,开发人员使用软件仓库,这越来越成为软件开发的主旋律,并在越来越广泛的领域应用。开发者所包含的每一个软件包,又有自己的依赖关系。为今天的编程环境而设计出的编程语言需要毫不费力地处理这种复杂性。

Go和Rust都有包管理系统,允许开发人员列出一个简单的清单,列出他们想要构建的包,语言工具就会自动为他们获取和维护这些包,这样开发人员就可以把更多的精力放在自己的代码上,而不是放在对其他包的管理上。

4) 安全性

Go和Rust都很好地解决了当今应用的安全问题,保证了用这些语言构建的代码在运行时不会让用户暴露在各种经典的安全漏洞中,比如缓冲区溢出、use-after-free(内存释放后还使用)等。通过消除这些顾虑,开发者可以专注于手头的问题,并在默认情况下构建更安全的应用程序。

“Rust编译器在解决您遇到的错误时确实能助您一臂之力。这样一来,您就可以专注于自己的业务目标,而不必寻找错误或解密隐秘消息。” -Josh Hannaford,IBM

简而言之,Rust的灵活性,安全性和安全性带给我们的益处超过了必须遵循严格的lifetime,borrow(rust中的概念)和其他编译器规则甚至缺乏垃圾收集器所带来的任何不便。这些功能是云软件项目中非常需要的功能,将有助于避免其中常见的许多错误。” —微软高级泰勒·托马斯(Taylor Thomas)

“Go是强静态类型化的,没有隐式转换,但语法开销还是小得惊人。这是通过赋值中简单的类型推理与非类型化的数值常量一起实现的。这使得Go比Java(有隐式转换)具有更强的类型安全性,但代码读起来更像Python(有非类型变量)。” – Stefan Nilsson,计算机科学教授

“当我们在Dropbox构建用于存储块数据的Brotli压缩库时,我们将自己限制在Rust的安全子集上,而且,也限制在核心库(no-stdlib)上,分配器指定为通用。这样使用Rust的子集,使得在客户端从Rust调用Rust-Brotli库,以及在服务器上使用Python和Go的C FFI变得非常容易。这种编译模式也提供了大量的安全保障。经过一些调整,Rust Brotli的实现尽管是100%安全的、经过数组边界检查的代码,但仍然比C语言中相应的原生Brotli代码快。” – Daniel Reiter Horn,Dropbox

5) 真正的可移植性

在Go和Rust中,写一个软件,在许多不同的操作系统和架构上运行是很容易的。”一次编写,随处编译”。此外,Go和Rust都原生支持交叉编译,消除了旧编译语言常见的”build farm”的需要。

“Go在生产优化方面拥有很好的特质,比如拥有较小的内存占用,这支持其在大型项目中被用于构建模块,以及开箱即用,易于交叉编译到其他架构。由于Go代码被编译成单一的静态二进制,我们可以轻松将其容器化,并且通过扩展,我们可以很轻松地将Go部署到任何高可用环境(如Kubernetes)中。” – Dewet Diener,Curve

“当你看一个基于云的基础设施时,通常你会使用类似Docker容器这样的东西来部署你的工作负载。通过在Go中构建的静态二进制,你可以拥有一个10、11、12兆字节的Docker文件,而不是带来整个Node.js生态系统,或像Python或Java那样动辄数百兆字节大小的Docker镜像文件。所以,交付那个微小的二进制文件是很神奇的。” – Brian Ketelsen,微软

“有了Rust,我们将拥有一个高性能和可移植的平台,我们可以轻松地在Mac、iOS、Linux、Android和Windows上运行。” – Matt Ronge,Astropad

3. 差异

在设计中,总是要做出一些取舍。虽然Go和Rust大约在同一时间出现,目标相似,但由于他们决策时选择了不同的取舍,使得这两种语言在关键的方面有所区别。

1) 性能方面

Go开箱即有出色的性能。在设计上,几乎没有预留任何旋钮或开关可以让你从Go中榨取更多的性能。Rust的设计是为了让您能够从代码中榨取每一滴性能;在这方面,您确实无法找到比Rust更快的语言。然而,Rust的性能提升是以额外的复杂性为代价的。

“值得注意的是,在编写Rust版本时,我们只在优化方面投入了非常基本的思考。即使只做了基本的优化,Rust的性能也能超过超手工调整的Go版本。这极大地证明了用Rust编写高效的程序是多么容易,相比之下,我们不得不对Go进行深挖。” – Jesse Howarth,Discord

“Dropbox工程师通过将行对行的Python代码移植到Go中,往往可以看到5倍的性能提升和延迟下降,与Python相比,内存使用率往往会大幅下降,因为没有GIL,进程数可能会减少。然而,当我们的内存受限时,比如在桌面客户端软件或某些服务器进程中,我们会转而使用Rust,因为Rust中的手动内存管理效率大大高于Go GC。” – Daniel Reiter Horn,Dropbox

2) 适应性/交互性

Go快速迭代的优势让开发人员可以快速尝试各种想法,并磨合出能解决手头任务的工作代码。通常情况下,这就足够了,可以让开发者腾出手来处理其他任务。另一方面,与Go相比,Rust的编译时间更长,导致迭代时间更慢。这就导致了Go在一些场景中能更好地工作,因为更快的周转时间能让开发人员适应不断变化的需求,而Rust则在一些场景中茁壮成长,因为在这些场景中,可以给予更多的时间来做出更精致、更高性能的实现。

“Go类型系统的天才之处在于调用者可以定义Interface,允许库返回仅需满足小接口但却支持扩展的结构。Rust类型系统的天才设计在于匹配语法与Result<>的结合,你可以静态地确定每一种可能性都会被处理,永远不必发明空值来满足未使用的返回参数。” – Daniel Reiter Horn,Dropbox

“(我)如果你的用例离客户更近,更容易受到需求变化的影响,那么用Go就会好很多,因为持续重构的成本要便宜很多。这就是你能多快地表达新的需求并尝试它们。” – Peter Bourgon,Fastly

3) 可学性

简单来说,真的没有比Go更“平易近人”的语言了。有很多团队能够在几周内采用Go并将Go服务/应用投入生产的故事。此外,Go在语言中是比较独特的,它的语言设计和实践在它10多年的生命中是相当一致的。所以,投入到学习Go上的时间可以保持很长一段时间的价值。相比之下,Rust由于其复杂性,被认为是一门难学的语言。一般来说,学习Rust需要几个月的时间才能感觉到自如,但这种额外的复杂性也带来了精确的控制和性能的提高。

“当时,没有一个团队成员知道Go,但在一个月内,每个人都在用Go写作”–Jaime Garcia,Capital One

“Go与其他编程语言不同的地方在于认知负担。你可以用更少的代码做更多的事情,这使得你更容易推理和理解你最终编写的代码。大多数Go代码最终看起来都很相似,所以,即使你在使用一个全新的代码库,你也可以很快上手并运行。” – Glen Balliet 美国运通忠诚度平台工程总监 美国运通使用Go进行支付和奖励

“然而,与其他编程语言不同,Go是为了最大限度地提高用户效率而创建的。因此,具有Java或PHP背景的开发人员和工程师可以在几周内获得使用Go的高级技能和培训–根据我们的经验,他们中的许多人最终都喜欢上了Go。” – Dewet Diener,Curve

4) 精确控制

也许Rust最大的优势之一就是开发者对如何管理内存、如何使用机器的可用资源、如何优化代码以及如何制作问题解决方案的控制。与Go相比,这并不是没有很大的复杂度成本,因为Go的设计并不是为了这种精确的制作,而是为了更快的探索时间和更快的周转时间。

“随着我们对Rust经验的增长,它在另外两个轴上显示出了优势:作为一种具有强大内存安全性的语言,它是边缘处理的好选择;作为一种具有巨大热情的语言,它成为了重写组件的流行语言。” – John Graham-Cumming,Cloudflare。

3. 总结/主要收获

Go的简单性、性能和开发人员的生产力使Go成为创建面向用户的应用程序和服务的理想语言。快速的迭代让团队能够快速地作出反应以满足用户不断变化的需求,让团队有办法将精力集中在灵活性上。

Rust更精细的控制允许更多的精确性,使得Rust成为低级操作的理想语言,这些低级操作不太可能发生变化,并且会从比Go略微提高的性能中受益,特别是在非常大的规模部署时。

Rust的优势在最接近“金属”(指底层机器)的地方。Go的优势是在离用户更近的地方最有利。这并不是说两者都不能在对方的空间里工作,但这样做会增加摩擦。当你的需求从灵活性转变为效率时,用Rust重写库的理由就更充分了。

虽然Go和Rust的设计有很大的不同,但它们的设计发挥了兼容的优势,而且–当一起使用时–既可以有很大的灵活性,又可以有很好的性能。

4. 我们的建议

对于大多数公司和用户来说,Go是正确的默认选择。它的性能很强,Go很容易采用,而且Go的高度模块化特性使它特别适合需求不断变化或发展的情况。

随着你的产品逐渐成熟,需求趋于稳定,可能会有机会从性能的边际增长中获得巨大的胜利。在这些情况下,使用Rust来最大限度地提高性能可能很值得你进行初始投资。


“Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!部落目前虽小,但持续力很强。在2021年上半年,部落将策划两个专题系列分享,并且是部落独享哦:

  • Go技术书籍的书摘和读书体会系列
  • Go与eBPF系列

欢迎各位Gopher加入!

Go技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中!本专栏主要满足广大gopher关于Go语言进阶的需求,围绕如何写出地道且高质量Go代码给出50条有效实践建议,上线后收到一致好评!欢迎大家订阅!目前该技术专栏正在新春促销!关注我的个人公众号“iamtonybai”,发送“go专栏活动”即可获取专栏专属优惠码,可在订阅专栏时抵扣20元哦(2021.2月末前有效)。

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网热卖中,欢迎小伙伴们订阅学习!

img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

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

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

微信赞赏:
img{512x368}

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

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! 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