1 Go 程序员大多使用 Linux 或 MacOSGo 团队在八月份对 Go 开发者进行了一项调查，调查结果显示 90% 的受访者表示他们在上一年使用 Go 时感到满意。他们在 Linux（63%）和 macOS（58%）系统上使用 Go 语言，但新的 Go 程序员更倾向于使用 Windows。虽然 x86 兼容系统仍占开发的大多数（89%），但 ARM64 现在也被大多数受访者使用（56%），这种采用似乎部分是由苹果芯片推动的。受访者最喜欢的代码编辑器是 VS Code（44%）、GoLand（31%）、Vim/Neovim（16%）和 Emacs（3%）。3/4 的受访者在使用云服务的 Go 软件上工作，这表明开发人员认为 Go 是一种适用于现代云开发的语言。（插图：DA/3cf4022f-9a7a-460b-8be4-741c10daa2f7）消息来源：Go Dev老王点评：使用 Linux 的比 macOS 和 Windows 的要多，但是为什么使用 VS Code 的最多呢。2 欧盟就《人工智能法》的推出达成共识来自欧洲议会及其 27 个成员国的谈判代表克服了巨大分歧，签署了《人工智能法》的临时政治协议。争议点主要是生成式人工智能和警方使用面部识别监控等方面。这一结果是在上周举行的马拉松式闭门会谈后达成的，其中一次会谈持续了 22 个小时，“欧盟成为第一个为人工智能的使用制定明确规则的大陆”。关于最终法律的具体内容，官员们提供的细节很少，该法律最早要到 2025 年才能生效，预计将为进一步谈判留出余地，以制定出更细的条款。（插图：DA/04fef112-325d-43c4-a6b1-1731cd9866b8）消息来源：AP News老王点评：就怕立法追不上 AI 的发展速度。3 让网速感觉更快的新标准 L4S这项名为 L4S 的新互联网标准已于今年 1 月定稿并发布，它可以大大减少我们等待网页或流媒体加载的时间，并减少视频通话中的故障。它还有助于改变我们对网速的看法，并帮助开发人员创建在当前互联网现实情况下无法实现的应用程序。L4S 是 低延迟、低损耗、可扩展吞吐量 Low Latency, Low Loss, Scalable Throughput 的缩写，其目标是通过减少排队的需要，确保数据包减少不必要的排队等待，花费尽可能少的时间。在某些情况下，数据包缓冲延迟通常为数百毫秒甚至数千毫秒，而 L4S 则可以将延迟时间降至几毫秒。更好的是，它与目前使用的拥塞控制系统广泛兼容。虽然 L4S 还没有被广泛使用，但苹果、谷歌、英伟达、爱立信、德国电信等公司都已经对此表示了极大兴趣。（插图：DA/cdd2468a-758a-487e-b5bd-b908700ce05e）消息来源：The Verge老王点评：为什么这么好的协议改进，我居然之前都没听说过。
这儿几个字节，那里几个字节，我们说的是真正的内存
今天的帖子来自于最近的 Go 语言的一次小测试，观察下面的测试基础片段 [1] ：func BenchmarkSortStrings(b *testing.B) { s := []string{"heart", "lungs", "brain", "kidneys", "pancreas"} b.ReportAllocs() for i := 0; i < b.N; i++ { sort.Strings(s) } } sort.Strings 是 sort.StringSlice(s) 的便捷包装器，sort.Strings 在原地对输入进行排序，因此不会分配内存（或至少 43% 回答此问题的 Twitter 用户是这么认为的）。然而，至少在 Go 的最近版本中，基准测试的每次迭代都会导致一次堆分配。为什么会是这种情况？正如所有 Go 程序员应该知道的那样，接口是以 双词结构 实现的。每个接口值包含一个字段，其中保存接口内容的类型，以及指向接口内容的指针。 [2] 在 Go 语言伪代码中，一个接口可能是这样的：type interface struct { // the ordinal number for the type of the value // assigned to the interface type uintptr // (usually) a pointer to the value assigned to // the interface data uintptr } interface.data 可以容纳一个机器字（在大多数情况下为 8 个字节），但一个 []string 却需要 24 个字节：一个字用于指向切片的底层数组；一个字用于存储切片的长度；另一个字用于存储底层数组的剩余容量。那么，Go 是如何将 24 个字节装入个 8 个字节的呢？通过编程中最古老的技巧，即间接引用。一个 []string，即 s，需要 24 个字节；但 *[]string —— 即指向字符串切片的指针，只需要 8 个字节。逃逸到堆为了让示例更加明确，以下是重新编写的基准测试，不使用 sort.Strings 辅助函数：func BenchmarkSortStrings(b *testing.B) { s := []string{"heart", "lungs", "brain", "kidneys", "pancreas"} b.ReportAllocs() for i := 0; i < b.N; i++ { var ss sort.StringSlice = s var si sort.Interface = ss // allocation sort.Sort(si) } } 为了让接口正常运行，编译器将赋值重写为 var si sort.Interface = &ss，即 ss 的地址分配给接口值。 [3] 我们现在有这么一种情况：出现一个持有指向 ss 的指针的接口值。它指向哪里？还有 ss 存储在哪个内存位置？似乎 ss 被移动到了堆上，这也同时导致了基准测试报告中的分配：Total: 296.01MB 296.01MB (flat, cum) 99.66% 8 . . func BenchmarkSortStrings(b *testing.B) { 9 . . s := []string{"heart", "lungs", "brain", "kidneys", "pancreas"} 10 . . b.ReportAllocs() 11 . . for i := 0; i < b.N; i++ { 12 . . var ss sort.StringSlice = s 13 296.01MB 296.01MB var si sort.Interface = ss // allocation 14 . . sort.Sort(si) 15 . . } 16 . . } 发生这种分配是因为编译器当前无法确认 ss 比 si 生存期更长。Go 编译器开发人员对此的普遍态度是，觉得 这个问题改进的余地，不过我们另找时间再议。事实上，ss 就是被分配到了堆上。因此，问题变成了：每次迭代会分配多少个字节？为什么不去询问 testing 包呢？% go test -bench=. sort_test.go goos: darwin goarch: amd64 cpu: Intel(R) Core(TM) i7-5650U CPU @ 2.20GHz BenchmarkSortStrings-4 12591951 91.36 ns/op 24 B/op 1 allocs/op PASS ok command-line-arguments 1.260s 可以看到，在 amd 64 平台的 Go 1.16 beta1 版本上，每次操作会分配 24 字节。 [4] 然而，在同一平台先前的 Go 版本中，每次操作则消耗了 32 字节。% go1.15 test -bench=. sort_test.go goos: darwin goarch: amd64 BenchmarkSortStrings-4 11453016 96.4 ns/op 32 B/op 1 allocs/op PASS ok command-line-arguments 1.225s 这引出了本文的主题，即 Go 1.16 版本中即将推出的一项便利改进。不过在讨论这个内容之前，我需要聊聊 “ 尺寸类别 size class ”。尺寸类别在解释什么是 “ 尺寸类别 size class ” 之前，我们先考虑个问题，理论上的 Go 语言在运行时是如何在其堆上分配 24 字节的。有一个简单的方法：追踪目前为止已分配到的所有内存的动向——利用指向堆上最后分配的字节的指针。分配 24 字节，堆指针就会增加 24，然后将前一个值返回给调用函数。只要写入的请求 24 字节的代码不超出该标记的范围，这种机制就没有额外开销。不过，现实情况下，内存分配器不仅要分配内存，有时还得释放内存。最终，Go 语言程序在运行时将释放这些 24 字节，但从运行的视角来看，它只知道它给调用者的开始地址。它不知道从该地址起始之后又分配了多少字节。为了允许释放内存，我们假设的 Go 语言程序运行时分配器必须记录堆上每个分配的长度值。那么这些长度值的分配存储在何处？当然是在堆上。在我们的设想中，当程序运行需要分配内存的时候，它可以请求稍微多一点，并把它用来存储请求的数量。而对于我们的切片示例而言，当我们请求 24 字节时，实际上会消耗 24 字节加上存储数字 24 的一些开销。这些开销有多大？事实上，实际上的最小开销量是一个字。 [5] 用来记录 24 字节分配的开销将是 8 字节。25% 不是很大，但也不算糟糕，随着分配的大小增加，开销将变得微不足道。然而，如果我们只想在堆上存储一个字节，会发生什么？开销将是请求数据量的 8 倍！是否有一种更高效的方式在堆上分配少量内存？与其在每个分配旁边存储长度，不如将相同大小的内容存储在一起，这个主意如何？如果所有的 24 字节的内容都存储在一起，那么运行时会自动获取它们的大小。运行时所需要的是一个单一的位，指示 24 字节区域是否在使用中。在 Go 语言中，这些区域被称为 Size Classes，因为相同大小的所有内容都会存储在一起（类似学校班级，所有学生都按同一年级分班，而不是 C++ 中的类）。当运行时需要分配少量内存时，它会使用能够容纳该分配的最小的尺寸类别。无限制的尺寸类别现在我们知道尺寸类别是如何工作的了，那么问题又来了，它们存储在哪里？和我们想的一样，尺寸类别的内存来自堆。为了最小化开销，运行时会从堆上分配较大的内存块（通常是系统页面大小的倍数），然后将该空间用于单个大小的分配。不过，这里存在一个问题————将大块区域用于存储同一大小的事物的模式很好用 [6] ，如果分配大小的数量是固定的，最好是少数几个。那么在通用语言中，程序可以要求运行时以任何大小分配内存 [7] 。例如，想象一下向运行时请求 9 字节。9 字节是一个不常见的大小，因此可能需要一个新的尺寸类别来存储 9 字节大小的物品。因为 9 字节大小的物品不常见，所以分配的其余部分（通常为 4KB 或更多）可能会被浪费。由于尺寸类别的集合是固定的，如果没有精确匹配的 size class 可用，分配将并入到下一个尺寸类别。在我们的示例中，9 字节可能会在 12 字节的尺寸类别中分配。未使用的 3 字节的开销要比几乎未使用的整个尺寸类别分配好。总结一下这是谜题的最后一块拼图。Go 1.15 版本没有 24 字节的尺寸类别，因此 ss 的堆分配是在 32 字节的尺寸类别中分配的。由于 Martin Möhrmann 的工作，Go 1.16 版本有一个 24 字节的尺寸类别，非常适合分配给接口的切片值。相关文章我在 Devfest 2017年西伯利亚大会谈 Go 语言如果对齐的内存写操作是原子性的，为什么我们还需要 sync/atomic 包呢？为你的树莓派创建一个真实的串行控制台为什么 Go 语言线程的栈是无限制的？（题图：MJ/01d5fe46-778f-48fe-9481-162f4d0289dc）这不是正确的对排序函数进行基准测试的方式，因为在第一次迭代之后，输入已经排序。但这又是另外一个话题了。 ↩︎此语句的准确性取决于所使用的 Go 版本。例如，Go 1.15 版本添加了直接将一些 整数存储在接口值 中的功能，从而节省了分配和间接性。然而，对于大多数值来说，如果它不是指针类型，它的地址将被取出并存储在接口值中。 ↩︎编译器在接口值的类型字段中跟踪了这种手法，因此它记住了分配给 si 的类型是 sort.StringSlice 而不是 *sort.StringSlice。 ↩︎在 32 位平台上，这个数字减半，但我们不再关注它。 ↩︎如果你准备限制分配为 4G 或者可能是 64KB，你可以使用较少内存来存储分配的尺寸，但实际上使用小于一个字来存储长度标头的节省会受到填充的影响。 ↩︎将相同大小的物品存储在一起也是一种有效的对抗碎片化的策略。 ↩︎这并不是一个不切实际的设想，字符串有各种形状和大小，生成以前未见过的大小的字符串可能就像附加空格一样简单。 ↩︎via: https://dave.cheney.net/2021/01/05/a-few-bytes-here-a-few-there-pretty-soon-youre-talking-real-memory作者：Dave Cheney 选题：lujun9972 译者：Drwhooooo 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
“Go 2 永远不会出现”谷歌的 Russ Cox 称现在还没有 Go 2 的计划，这就提出了一个显而易见的问题：我们什么时候才能看到会破坏旧版 Go 1 程序的 Go 2 规范呢？在今天的博文中他写道：“答案是永远不会。从与过去决裂、不再编译旧程序的意义上讲，Go 2 永远不会出现。从 2017 年开始对 Go 1 进行重大修订的意义上来说，Go 2 已经出现了。” Go 语言的开发者认为，“优先考虑兼容性是我们为 Go 1 所做的最重要的设计决定。”消息来源：Phoronix老王点评：编程语言的大版本换代中，Python、Perl 都遭遇过严重的问题，而 Go 很好地绕开了这些前辈们遇到的陷阱，显然会让 Go 的发展更加顺畅。“诈弹” 毁了 DEF CON 派对之夜本次 DEF CON 黑客大会于 8 月 10 日在拉斯维加斯召开，并于 8 月 13 日结束，今年据称有超过 3 万人参加了大会。在周六傍晚，主会场受到炸弹威胁，导致主会场大厅被清空，消防人员和警察对大楼进行了搜查，但没有找到可疑物品。炸弹威胁被认为是一次恶作剧，但恶作剧者毁掉了所有人的夜晚。受此影响，大会主办方取消了当晚在主会场的庆祝活动，令数千人倍感失望。这个恶作剧者还导致四号轨道大厅欣赏《2001：太空漫游》的观众被迫错过了电影最后十分钟的关键时刻。消息来源：The Register老王点评：本来 DEF CON 是欢迎恶作剧的，但是这个蠢蛋的恶作剧太过分了，毁掉了大家的狂欢夜。微软发现可关闭发电厂的安全漏洞微软披露了一个广泛使用的工具集合中的 15 个高严重性漏洞，这些工具用于开发可编程逻辑控制器，这种面包大小的设备用于打开和关闭阀门、转动转子以及控制全球工业设施中的各种其他物理设备。微软警告说，虽然利用代码执行和拒绝服务漏洞很困难，但它能使威胁行为者 “对目标造成巨大损害”，如关闭发电厂，而远程代码执行可为设备创建后门，让攻击者篡改操作，导致 PLC 以异常方式运行，或窃取关键信息。消息来源：Ars Technica老王点评：这种工业控制领域的安全风险，不出则已，一旦被利用，造成的危害可远不止数据丢失那么简单。
了解使用数组和切片在 Go 中存储数据的优缺点，以及为什么其中一个更好。在本系列的第四篇文章中，我将解释 Go 数组和切片，包括如何使用它们，以及为什么你通常要选择其中一个而不是另一个。数组数组是编程语言中最流行的数据结构之一，主要原因有两个：一是简单易懂，二是可以存储许多不同类型的数据。你可以声明一个名为 anArray 的 Go 数组，该数组存储四个整数，如下所示：anArray := [4]int{-1, 2, 0, -4} 数组的大小应该在它的类型之前声明，而类型应该在声明元素之前定义。len() 函数可以帮助你得到任何数组的长度。上面数组的大小是 4。如果你熟悉其他编程语言，你可能会尝试使用 for 循环来遍历数组。Go 当然也支持 for 循环，不过，正如你将在下面看到的，Go 的 range 关键字可以让你更优雅地遍历数组或切片。最后，你也可以定义一个二维数组，如下：twoD := [3][3]int{ {1, 2, 3}, {6, 7, 8}, {10, 11, 12}} arrays.go 源文件中包含了 Go 数组的示例代码。其中最重要的部分是：for i := 0; i < len(twoD); i++ { k := twoD[i] for j := 0; j < len(k); j++ { fmt.Print(k[j], " ") } fmt.Println() } for _, a := range twoD { for _, j := range a { fmt.Print(j, " ") } fmt.Println() } 通过上述代码，我们知道了如何使用 for 循环和 range 关键字迭代数组的元素。arrays.go 的其余代码则展示了如何将数组作为参数传递给函数。以下是 arrays.go 的输出：$ go run arrays.go Before change(): [-1 2 0 -4] After change(): [-1 2 0 -4] 1 2 3 6 7 8 10 11 12 1 2 3 6 7 8 10 11 12 这个输出告诉我们：对函数内的数组所做的更改，会在函数退出后丢失。数组的缺点Go 数组有很多缺点，你应该重新考虑是否要在 Go 项目中使用它们。首先，数组定义之后，大小就无法改变，这意味着 Go 数组不是动态的。简而言之，如果你需要将一个元素添加到一个没有剩余空间的数组中，你将需要创建一个更大的数组，并将旧数组的所有元素复制到新数组中。其次，当你将数组作为参数传递给函数时，实际上是传递了数组的副本，这意味着你对函数内部的数组所做的任何更改，都将在函数退出后丢失。最后，将大数组传递给函数可能会很慢，主要是因为 Go 必须创建数组的副本。以上这些问题的解决方案，就是使用 Go 切片。切片Go 切片与 Go 数组类似，但是它没有后者的缺点。首先，你可以使用 append() 函数将元素添加到现有切片中。此外，Go 切片在内部使用数组实现，这意味着 Go 中每个切片都有一个底层数组。切片具有 capacity 属性和 length 属性，它们并不总是相同的。切片的长度与元素个数相同的数组的长度相同，可以使用 len() 函数得到。切片的容量是当前为切片分配的空间，可以使用 cap() 函数得到。由于切片的大小是动态的，如果切片空间不足（也就是说，当你尝试再向切片中添加一个元素时，底层数组的长度恰好与容量相等），Go 会自动将它的当前容量加倍，使其空间能够容纳更多元素，然后将请求的元素添加到底层数组中。此外，切片是通过引用传递给函数的，这意味着实际传递给函数的是切片变量的内存地址，这样一来，你对函数内部的切片所做的任何修改，都不会在函数退出后丢失。因此，将大切片传递给函数，要比将具有相同数量元素的数组传递给同一函数快得多。这是因为 Go 不必拷贝切片 —— 它只需传递切片变量的内存地址。slice.go 源文件中有 Go 切片的代码示例，其中包含以下代码：package main import ( "fmt" ) func negative(x []int) { for i, k := range x { x[i] = -k } } func printSlice(x []int) { for _, number := range x { fmt.Printf("%d ", number) } fmt.Println() } func main() { s := []int{0, 14, 5, 0, 7, 19} printSlice(s) negative(s) printSlice(s) fmt.Printf("Before. Cap: %d, length: %d\n", cap(s), len(s)) s = append(s, -100) fmt.Printf("After. Cap: %d, length: %d\n", cap(s), len(s)) printSlice(s) anotherSlice := make([]int, 4) fmt.Printf("A new slice with 4 elements: ") printSlice(anotherSlice) } 切片和数组在定义方式上的最大区别就在于：你不需要指定切片的大小。实际上，切片的大小取决于你要放入其中的元素数量。此外，append() 函数允许你将元素添加到现有切片 —— 请注意，即使切片的容量允许你将元素添加到该切片，它的长度也不会被修改，除非你调用 append()。上述代码中的 printSlice() 函数是一个辅助函数，用于打印切片中的所有元素，而 negative() 函数将切片中的每个元素都变为各自的相反数。运行 slice.go 将得到以下输出：$ go run slice.go 0 14 5 0 7 19 0 -14 -5 0 -7 -19 Before. Cap: 6, length: 6 After. Cap: 12, length: 7 0 -14 -5 0 -7 -19 -100 A new slice with 4 elements: 0 0 0 0 请注意，当你创建一个新切片，并为给定数量的元素分配内存空间时，Go 会自动地将所有元素都初始化为其类型的零值，在本例中为 0（int 类型的零值）。使用切片来引用数组Go 允许你使用 [:] 语法，使用切片来引用现有的数组。在这种情况下，你对切片所做的任何更改都将传播到数组中 —— 详见 refArray.go。请记住，使用 [:] 不会创建数组的副本，它只是对数组的引用。refArray.go 中最有趣的部分是：func main() { anArray := [5]int{-1, 2, -3, 4, -5} refAnArray := anArray[:] fmt.Println("Array:", anArray) printSlice(refAnArray) negative(refAnArray) fmt.Println("Array:", anArray) } 运行 refArray.go，输出如下：$ go run refArray.go Array: [-1 2 -3 4 -5] -1 2 -3 4 -5 Array: [1 -2 3 -4 5] 我们可以发现：对 anArray 数组的切片引用进行了操作后，它本身也被改变了。总结尽管 Go 提供了数组和切片两种类型，你很可能还是会使用切片，因为它们比 Go 数组更加通用、强大。只有少数情况需要使用数组而不是切片，特别是当你完全确定元素的数量固定不变时。你可以在 GitHub 上找到 arrays.go、slice.go 和 refArray.go 的源代码。如果你有任何问题或反馈，请在下方发表评论或在 Twitter 上与我联系。via: https://opensource.com/article/18/7/introduction-go-arrays-and-slices作者：Mihalis Tsoukalos 选题：lkxed 译者：lkxed 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
本文是 Go 系列的第三篇文章，我将介绍三种最流行的复制文件的方法。本文将介绍展示如何使用 Go 编程语言 来复制文件。在 Go 中复制文件的方法有很多，我只介绍三种最常见的：使用 Go 库中的 io.Copy() 函数调用、一次读取输入文件并将其写入另一个文件，以及使用缓冲区一块块地复制文件。方法一：使用 io.Copy()第一种方法就是使用 Go 标准库的 io.Copy() 函数。你可以在 copy() 函数的代码中找到它的实现逻辑，如下所示：func copy(src, dst string) (int64, error) { sourceFileStat, err := os.Stat(src) if err != nil { return 0, err } if !sourceFileStat.Mode().IsRegular() { return 0, fmt.Errorf("%s is not a regular file", src) } source, err := os.Open(src) if err != nil { return 0, err } defer source.Close() destination, err := os.Create(dst) if err != nil { return 0, err } defer destination.Close() nBytes, err := io.Copy(destination, source) return nBytes, err } 首先，上述代码做了两个判断，以便确定它可以被打开读取：一是判断将要复制的文件是否存在（os.Stat(src)），二是判断它是否为常规文件（sourceFileStat.Mode().IsRegular()）。剩下的所有工作都由 io.Copy(destination, source) 这行代码来完成。io.Copy() 函数执行结束后，会返回复制的字节数和复制过程中发生的第一条错误消息。在 Go 中，如果没有错误消息，错误变量的值就为 nil。你可以在 io 包 的文档页面了解有关 io.Copy() 函数的更多信息。运行 cp1.go 将产生以下输出：$ go run cp1.go Please provide two command line arguments! $ go run cp1.go fileCP.txt /tmp/fileCPCOPY Copied 3826 bytes! $ diff fileCP.txt /tmp/fileCPCOPY 这个方法已经非常简单了，不过它没有为开发者提供灵活性。这并不总是一件坏事，但是，有些时候，开发者可能会需要/想要告诉程序该如何读取文件。方法二：使用 ioutil.WriteFile() 和 ioutil.ReadFile()复制文件的第二种方法是使用 ioutil.ReadFile() 和 ioutil.WriteFile() 函数。第一个函数用于将整个文件的内容，一次性地读入到某个内存中的字节切片里；第二个函数则用于将字节切片的内容写入到一个磁盘文件中。实现代码如下：input, err := ioutil.ReadFile(sourceFile) if err != nil { fmt.Println(err) return } err = ioutil.WriteFile(destinationFile, input, 0644) if err != nil { fmt.Println("Error creating", destinationFile) fmt.Println(err) return } 上述代码包括了两个 if 代码块（嗯，用 Go 写程序就是这样的），程序的实际功能其实体现在 ioutil.ReadFile() 和 ioutil.WriteFile() 这两行代码中。运行 cp2.go，你会得到下面的输出：$ go run cp2.go Please provide two command line arguments! $ go run cp2.go fileCP.txt /tmp/copyFileCP $ diff fileCP.txt /tmp/copyFileCP 请注意，虽然这种方法能够实现文件复制，但它在复制大文件时的效率可能不高。这是因为当文件很大时，ioutil.ReadFile() 返回的字节切片会很大。方法三：使用 os.Read() 和 os.Write()在 Go 中复制文件的第三种方法就是下面要介绍的 cp3.go。它接受三个参数：输入文件名、输出文件名和缓冲区大小。cp3.go 最重要的部分位于以下 for 循环中，你可以在 copy() 函数中找到它，如下所示：buf := make([]byte, BUFFERSIZE) for { n, err := source.Read(buf) if err != nil && err != io.EOF { return err } if n == 0 { break } if _, err := destination.Write(buf[:n]); err != nil { return err } } 该方法使用 os.Read() 将输入文件的一小部分读入名为 buf 的缓冲区，然后使用 os.Write() 将该缓冲区的内容写入文件。当读取出错或到达文件末尾（io.EOF）时，复制过程将停止。运行 cp3.go，你会得到下面的输出：$ go run cp3.go usage: cp3 source destination BUFFERSIZE $ go run cp3.go fileCP.txt /tmp/buf10 10 Copying fileCP.txt to /tmp/buf10 $ go run cp3.go fileCP.txt /tmp/buf20 20 Copying fileCP.txt to /tmp/buf20 在接下来的基准测试中，你会发现，缓冲区的大小极大地影响了 cp3.go 的性能。运行基准测试在本文的最后一部分，我将尝试比较这三个程序以及 cp3.go 在不同缓冲区大小下的性能（使用 time(1) 命令行工具）。以下输出显示了复制 500MB 大小的文件时，cp1.go、cp2.go 和 cp3.go 的性能对比：$ ls -l INPUT -rw-r--r-- 1 mtsouk staff 512000000 Jun 5 09:39 INPUT $ time go run cp1.go INPUT /tmp/cp1 Copied 512000000 bytes! real 0m0.980s user 0m0.219s sys 0m0.719s $ time go run cp2.go INPUT /tmp/cp2 real 0m1.139s user 0m0.196s sys 0m0.654s $ time go run cp3.go INPUT /tmp/cp3 1000000 Copying INPUT to /tmp/cp3 real 0m1.025s user 0m0.195s sys 0m0.486s 我们可以看出，这三个程序的性能非常接近，这意味着 Go 标准库函数的实现非常聪明、经过了充分优化。现在，让我们测试一下缓冲区大小对 cp3.go 的性能有什么影响吧！执行 cp3.go，并分别指定缓冲区大小为 10、20 和 1000 字节，在一台运行很快的机器上复制 500MB 文件，得到的结果如下：$ ls -l INPUT -rw-r--r-- 1 mtsouk staff 512000000 Jun 5 09:39 INPUT $ time go run cp3.go INPUT /tmp/buf10 10 Copying INPUT to /tmp/buf10 real 6m39.721s user 1m18.457s sys 5m19.186s $ time go run cp3.go INPUT /tmp/buf20 20 Copying INPUT to /tmp/buf20 real 3m20.819s user 0m39.444s sys 2m40.380s $ time go run cp3.go INPUT /tmp/buf1000 1000 Copying INPUT to /tmp/buf1000 real 0m4.916s user 0m1.001s sys 0m3.986s 我们可以发现，缓冲区越大，cp3.go 运行得就越快，这或多或少是符合预期的。此外，使用小于 20 字节的缓冲区来复制大文件会非常缓慢，应该避免。你可以在 GitHub 找到 cp1.go、cp2.go 和 cp3.go 的 Go 代码。如果你有任何问题或反馈，请在（原文）下方发表评论或在 Twitter 上与我（原作者）联系。via: https://opensource.com/article/18/6/copying-files-go作者：Mihalis Tsoukalos 选题：lkxed 译者：lkxed 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
仅用大约 65 行代码，开发一个用于生成随机数、支持并发的 TCP 服务端。TCP 和 UDP 服务端随处可见，它们基于 TCP/IP 协议栈，通过网络为客户端提供服务。在这篇文章中，我将介绍如何使用 Go 语言 开发一个用于返回随机数、支持并发的 TCP 服务端。对于每一个来自 TCP 客户端的连接，它都会启动一个新的 goroutine（轻量级线程）来处理相应的请求。你可以在 GitHub 上找到本项目的源码：concTcp.go。处理 TCP 连接这个程序的主要逻辑在 handleConnection() 函数中，具体实现如下：func handleConnection(c net.Conn) { fmt.Printf("Serving %s\n", c.RemoteAddr().String()) for { netData, err := bufio.NewReader(c).ReadString('\n') if err != nil { fmt.Println(err) return } temp := strings.TrimSpace(string(netData)) if temp == "STOP" { break } result := strconv.Itoa(random()) + "\n" c.Write([]byte(string(result))) } c.Close() } 如果 TCP 客户端发送了一个 “STOP” 字符串，为它提供服务的 goroutine 就会终止；否则，TCP 服务端就会返回一个随机数给它。只要客户端不主动终止，服务端就会一直提供服务，这是由 for 循环保证的。具体来说，for 循环中的代码使用了 bufio.NewReader(c).ReadString('\n') 来逐行读取客户端发来的数据，并使用 c.Write([]byte(string(result))) 来返回数据（生成的随机数）。你可以在 Go 的 net 标准包 文档 中了解更多。支持并发在 main() 函数的实现部分，每当 TCP 服务端收到 TCP 客户端的连接请求，它都会启动一个新的 goroutine 来为这个请求提供服务。func main() { arguments := os.Args if len(arguments) == 1 { fmt.Println("Please provide a port number!") return } PORT := ":" + arguments[1] l, err := net.Listen("tcp4", PORT) if err != nil { fmt.Println(err) return } defer l.Close() rand.Seed(time.Now().Unix()) for { c, err := l.Accept() if err != nil { fmt.Println(err) return } go handleConnection(c) } } 首先，main() 确保程序至少有一个命令行参数。注意，现有代码并没有检查这个参数是否为有效的 TCP 端口号。不过，如果它是一个无效的 TCP 端口号，net.Listen() 就会调用失败，并返回一个错误信息，类似下面这样：$ go run concTCP.go 12a listen tcp4: lookup tcp4/12a: nodename nor servname provided, or not known $ go run concTCP.go -10 listen tcp4: address -10: invalid port net.Listen() 函数用于告诉 Go 接受网络连接，因而承担了服务端的角色。它的返回值类型是 net.Conn，后者实现了 io.Reader 和 io.Writer 接口。此外，main() 函数中还调用了 rand.Seed() 函数，用于初始化随机数生成器。最后，for 循环允许程序一直使用 Accept() 函数来接受 TCP 客户端的连接请求，并以 goroutine 的方式来运行 handleConnection(c) 函数，处理客户端的后续请求。net.Listen() 的第一个参数net.Listen() 函数的第一个参数定义了使用的网络类型，而第二个参数定义了服务端监听的地址和端口号。第一个参数的有效值为 tcp、tcp4、tcp6、udp、udp4、udp6、ip、ip4、ip6、Unix（Unix 套接字）、Unixgram 和 Unixpacket，其中：tcp4、udp4 和 ip4 只接受 IPv4 地址，而 tcp6、udp6 和 ip6 只接受 IPv6 地址。服务端并发测试concTCP.go 需要一个命令行参数，来指定监听的端口号。当它开始服务 TCP 客户端时，你会得到类似下面的输出：$ go run concTCP.go 8001 Serving 127.0.0.1:62554 Serving 127.0.0.1:62556 netstat 的输出可以确认 congTCP.go 正在为多个 TCP 客户端提供服务，并且仍在继续监听建立连接的请求：$ netstat -anp TCP | grep 8001 tcp4 0 0 127.0.0.1.8001 127.0.0.1.62556 ESTABLISHED tcp4 0 0 127.0.0.1.62556 127.0.0.1.8001 ESTABLISHED tcp4 0 0 127.0.0.1.8001 127.0.0.1.62554 ESTABLISHED tcp4 0 0 127.0.0.1.62554 127.0.0.1.8001 ESTABLISHED tcp4 0 0 *.8001 *.* LISTEN 在上面输出中，最后一行显示了有一个进程正在监听 8001 端口，这意味着你可以继续连接 TCP 的 8001 端口。第一行和第二行显示了有一个已建立的 TCP 网络连接，它占用了 8001 和 62556 端口。相似地，第三行和第四行显示了有另一个已建立的 TCP 连接，它占用了 8001 和 62554 端口。下面这张图片显示了 concTCP.go 在服务多个 TCP 客户端时的输出：类似地，下面这张图片显示了两个 TCP 客户端的输出（使用了 nc 工具）：你可以在 维基百科 上找到更多关于 nc（即 netcat）的信息。总结现在，你学会了如何用大约 65 行 Go 代码来开发一个生成随机数、支持并发的 TCP 服务端，这真是太棒了！如果你想要让你的 TCP 服务端执行别的任务，只需要修改 handleConnection() 函数即可。via: https://opensource.com/article/18/5/building-concurrent-tcp-server-go作者：Mihalis Tsoukalos 选题：lkxed 译者：lkxed 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
Go 的随机数生成器是生成难以猜测的密码的好方法。你可以使用 Go 编程语言 提供的随机数生成器来生成由 ASCII 字符组成的难以猜测的密码。尽管本文中提供的代码很容易阅读，但是你仍需要了解 Go 的基础知识，才能更好地理解它。如果你是对 Go 还不熟悉，请阅读 Go 语言之旅 来了解更多信息，然后返回此处。在介绍实用程序和它的代码之前，让我们先来看看这个 ASCII 表的子集，它可以在 man ascii 命令的输出中找到：30 40 50 60 70 80 90 100 110 120 --------------------------------- 0: ( 2 < F P Z d n x 1: ) 3 = G Q [ e o y 2: * 4 > H R \ f p z 3: ! + 5 ? I S ] g q { 4: " , 6 @ J T ^ h r | 5: # - 7 A K U _ i s } 6: $ . 8 B L V ` j t ~ 7: % / 9 C M W a k u DEL 8: & 0 : D N X b l v 9: ' 1 ; E O Y c m w 在所有 ASCII 字符中，可打印字符的十进制值范围为 33 到 126，其他的 ASCII 值都不适合用于密码。因此，本文介绍的实用程序将生成该范围内的 ASCII 字符。生成随机整数第一个实用程序名为 random.go，它生成指定数量的随机整数，这些整数位于给定范围内。random.go 最重要的部分是这个函数：func random(min, max int) int { return rand.Intn(max-min) + min } 此函数使用了 rand.Intn() 函数来生成一个属于给定范围的随机整数。请注意，rand.Intn() 返回一个属于 [0,n) 的非负随机整数。如果它的参数是一个负数，这个函数将会抛出异常，异常消息是：panic: invalid argument to Intn。你可以在 math/rand 文档 中找到 math/rand 包的使用说明。random.go 实用程序接受三个命令行参数：生成的整数的最小值、最大值和个数。编译和执行 random.go 会产生这样的输出：$ go build random.go $ ./random Usage: ./random MIX MAX TOTAL $ ./random 1 3 10 2 2 1 2 2 1 1 2 2 1 如果你希望在 Go 中生成更安全的随机数，请使用 Go 库中的 crypto/rand 包。生成随机密码第二个实用程序 randomPass.go 用于生成随机密码。randomPass.go 使用 random() 函数来生成随机整数，它们随后被以下 Go 代码转换为 ASCII 字符：for { myRand := random(MIN, MAX) newChar := string(startChar[0] + byte(myRand)) fmt.Print(newChar) if i == LENGTH { break } i++ } MIN 的值为 0，MAX 的值为 94，而 startChar 的值为 !，它是 ASCII 表中第一个可打印的字符（十进制 ASCII 码为 33）。因此，所有生成的 ASCII 字符都位于 ! 和 ~ 之间，后者的十进制 ASCII 码为 126。因此，生成的每个随机数都大于 MIN，小于 MAX，并转换为 ASCII 字符。该过程继续进行，直到生成的密码达到指定的长度。randomPass.go 实用程序接受单个（可选）命令行参数，以定义生成密码的长度，默认值为 8，这是一个非常常见的密码长度。执行 randomPass.go 会得到类似下面的输出：$ go run randomPass.go 1 Z $ go run randomPass.go 10 #Cw^a#IwkT $ go run randomPass.go Using default values! [PP8@'Ci 最后一个细节：不要忘记调用 rand.Seed()，并提供一个 种子 seed 值，以初始化随机数生成器。如果你始终使用相同的种子值，随机数生成器将生成相同的随机整数序列。你可以在 GitHub 找到 random.go 和 randomPass.go 的源码。你也可以直接在 play.golang.org 上执行它们。我希望这篇文章对你有所帮助。如有任何问题，请在下方发表评论或在 Twitter 上与我联系。via: https://opensource.com/article/18/5/creating-random-secure-passwords-go作者：Mihalis Tsoukalos 选题：lkxed 译者：lkxed 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
这些方便的 Go 构建选项可以帮助你更好地理解 Go 的编译过程。学习一门新的编程语言最令人欣慰的部分之一，就是最终运行了一个可执行文件，并获得预期的输出。当我开始学习 Go 这门编程语言时，我先是阅读一些示例程序来熟悉语法，然后是尝试写一些小的测试程序。随着时间的推移，这种方法帮助我熟悉了编译和构建程序的过程。Go 的构建选项提供了更好地控制构建过程的方法。它们还可以提供额外的信息，帮助把这个过程分成更小的部分。在这篇文章中，我将演示我所使用的一些选项。注意：我使用的“ 构建 build ”和“ 编译 compile ”这两个词是同一个意思。开始使用 Go我使用的 Go 版本是 1.16.7。但是，这里给出的命令应该也能在最新的版本上运行。如果你没有安装 Go，你可以从 Go 官网 上下载它，并按照说明进行安装。你可以通过打开一个命令提示符，并键入下面的命令来验证你所安装的版本：$ go version 你应该会得到类似下面这样的输出，具体取决于你安装的版本：go version go1.16.7 linux/amd64 基本的 Go 程序的编译和执行方法我将从一个在屏幕上简单打印 “Hello World” 的 Go 程序示例开始，就像下面这样：$ cat hello.go package main import "fmt" func main() { fmt.Println("Hello World") } 在讨论更高级的选项之前，我将解释如何编译这个 Go 示例程序。我使用了 build 命令，后面跟着 Go 程序的源文件名，本例中是 hello.go，就像下面这样：$ go build hello.go 如果一切工作正常，你应该看到在你的当前目录下创建了一个名为 hello 的可执行文件。你可以通过使用 file 命令验证它是 ELF 二进制可执行格式（在 Linux 平台上）。你也可以直接执行它，你会看到它输出 “Hello World”。$ ls hello hello.go $ file ./hello ./hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped $ ./hello Hello World Go 提供了一个方便的 run 命令，以便你只是想看看程序是否能正常工作，并获得预期的输出，而不想生成一个最终的二进制文件。请记住，即使你在当前目录中没有看到可执行文件，Go 仍然会在某个地方编译并生成可执行文件并运行它，然后把它从系统中删除。我将在本文后面的章节中解释。$ go run hello.go Hello World $ ls hello.go 更多细节上面的命令就像一阵风一样，一下子就运行完了我的程序。然而，如果你想知道 Go 在编译这些程序的过程中做了什么，Go 提供了一个 -x 选项，它可以打印出 Go 为产生这个可执行文件所做的一切。简单看一下你就会发现，Go 在 /tmp 内创建了一个临时工作目录，并生成了可执行文件，然后把它移到了 Go 源程序所在的当前目录。$ go build -x hello.go WORK=/tmp/go-build1944767317 mkdir -p $WORK/b001/ << snip >> mkdir -p $WORK/b001/exe/ cd . /usr/lib/golang/pkg/tool/linux_amd64/link -o $WORK \ /b001/exe/a.out -importcfg $WORK/b001 \ /importcfg.link -buildmode=exe -buildid=K26hEYzgDkqJjx2Hf-wz/\ nDueg0kBjIygx25rYwbK/W-eJaGIOdPEWgwC6o546 \ /K26hEYzgDkqJjx2Hf-wz -extld=gcc /root/.cache/go-build /cc \ /cc72cb2f4fbb61229885fc434995964a7a4d6e10692a23cc0ada6707c5d3435b-d /usr/lib/golang/pkg/tool/linux_amd64/buildid -w $WORK \ /b001/exe/a.out # internal mv $WORK/b001/exe/a.out hello rm -r $WORK/b001/ 这有助于解决在程序运行后却在当前目录下没有生成可执行文件的谜团。使用 -x 显示可执行文件确实在 /tmp 工作目录下创建并被执行了。然而，与 build 命令不同的是，可执行文件并没有移动到当前目录，这使得看起来没有可执行文件被创建。$ go run -x hello.go mkdir -p $WORK/b001/exe/ cd . /usr/lib/golang/pkg/tool/linux_amd64/link -o $WORK/b001 \ /exe/hello -importcfg $WORK/b001/importcfg.link -s -w -buildmode=exe -buildid=hK3wnAP20DapUDeuvAAS/E_TzkbzwXz6tM5dEC8Mx \ /7HYBzuaDGVdaZwSMEWAa/hK3wnAP20DapUDeuvAAS -extld=gcc \ /root/.cache/go-build/75/ \ 7531fcf5e48444eed677bfc5cda1276a52b73c62ebac3aa99da3c4094fa57dc3-d $WORK/b001/exe/hello Hello World 模仿编译而不产生可执行文件假设你不想编译程序并产生一个实际的二进制文件，但你确实想看到这个过程中的所有步骤。你可以通过使用 -n 这个构建选项来做到这一点，该选项会打印出通常的执行步骤，而不会实际创建二进制文件。$ go build -n hello.go 保存临时目录很多工作都发生在 /tmp 工作目录中，一旦可执行文件被创建和运行，它就会被删除。但是如果你想看看哪些文件是在编译过程中创建的呢？Go 提供了一个 -work 选项，它可以在编译程序时使用。-work 选项除了运行程序外，还打印了工作目录的路径，但它并不会在这之后删除工作目录，所以你可以切换到该目录，检查在编译过程中创建的所有文件。$ go run -work hello.go WORK=/tmp/go-build3209320645 Hello World $ find /tmp/go-build3209320645 /tmp/go-build3209320645 /tmp/go-build3209320645/b001 /tmp/go-build3209320645/b001/importcfg.link /tmp/go-build3209320645/b001/exe /tmp/go-build3209320645/b001/exe/hello $ /tmp/go-build3209320645/b001/exe/hello Hello World 其他编译选项如果说，你想手动编译程序，而不是使用 Go 的 build 和 run 这两个方便的命令，最后得到一个可以直接由你的操作系统（这里指 Linux）运行的可执行文件。那么，你该怎么做呢？这个过程可以分为两部分：编译和链接。你可以使用 tool 选项来看看它是如何工作的。首先，使用 tool compile 命令产生结果的 ar 归档文件，它包含了 .o 中间文件。接下来，对这个 hello.o 文件执行 tool link 命令，产生最终的可执行文件，然后你就可以运行它了。$ go tool compile hello.go $ file hello.o hello.o: current ar archive $ ar t hello.o __.PKGDEF _go_.o $ go tool link -o hello hello.o $ file hello hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped $ ./hello Hello World 如果你想进一步查看基于 hello.o 文件产生可执行文件的链接过程，你可以使用 -v 选项，它会搜索每个 Go 可执行文件中包含的 runtime.a 文件。$ go tool link -v -o hello hello.o HEADER = -H5 -T0x401000 -R0x1000 searching for runtime.a in /usr/lib/golang/pkg/linux_amd64/runtime.a 82052 symbols, 18774 reachable 1 package symbols, 1106 hashed symbols, 77185 non-package symbols, 3760 external symbols 81968 liveness data 交叉编译选项现在我已经解释了 Go 程序的编译过程，接下来，我将演示 Go 如何通过在实际的 build 命令之前提供 GOOS 和 GOARCH 这两个环境变量，来允许你构建针对不同硬件架构和操作系统的可执行文件。这有什么用呢？举个例子，你会发现为 ARM（arch64）架构制作的可执行文件不能在英特尔（x86\_64）架构上运行，而且会产生一个 Exec 格式错误。下面的这些选项使得生成跨平台的二进制文件变得小菜一碟：$ GOOS=linux GOARCH=arm64 go build hello.go $ file ./hello ./hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, not stripped $ ./hello bash: ./hello: cannot execute binary file: Exec format error $ uname -m x86_64 你可以阅读我之前的博文，以更多了解我在 使用 Go 进行交叉编译 方面的经验。查看底层汇编指令源代码并不会直接转换为可执行文件，尽管它生成了一种中间汇编格式，然后最终被组装为可执行文件。在 Go 中，这被映射为一种中间汇编格式，而不是底层硬件汇编指令。要查看这个中间汇编格式，请在使用 build 命令时，提供 -gcflags 选项，后面跟着 -S。这个命令将会显示使用到的汇编指令：$ go build -gcflags="-S" hello.go # command-line-arguments "".main STEXT size=138 args=0x0 locals=0x58 funcid=0x0 0x0000 00000 (/test/hello.go:5) TEXT "".main(SB), ABIInternal, $88-0 0x0000 00000 (/test/hello.go:5) MOVQ (TLS), CX 0x0009 00009 (/test/hello.go:5) CMPQ SP, 16(CX) 0x000d 00013 (/test/hello.go:5) PCDATA $0, $-2 0x000d 00013 (/test/hello.go:5) JLS 128 << snip >> 你也可以使用 objdump -s 选项，来查看已经编译好的可执行程序的汇编指令，就像下面这样：$ ls hello hello.go $ go tool objdump -s main.main hello TEXT main.main(SB) /test/hello.go hello.go:5 0x4975a0 64488b0c25f8ffffff MOVQ FS:0xfffffff8, CX hello.go:5 0x4975a9 483b6110 CMPQ 0x10(CX), SP hello.go:5 0x4975ad 7671 JBE 0x497620 hello.go:5 0x4975af 4883ec58 SUBQ $0x58, SP hello.go:6 0x4975d8 4889442448 MOVQ AX, 0x48(SP) << snip >> 分离二进制文件以减少其大小Go 的二进制文件通常比较大。例如, 一个简单的 “Hello World” 程序将会产生一个 1.9M 大小的二进制文件。$ go build hello.go $ $ du -sh hello 1.9M hello $ $ file hello hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped $ 为了减少生成的二进制文件的大小，你可以分离执行过程中不需要的信息。使用 -ldflags 和 -s -w 选项可以使生成的二进制文件略微变小为 1.3M。$ go build -ldflags="-s -w" hello.go $ $ du -sh hello 1.3M hello $ $ file hello hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped $ 总结我希望这篇文章向你介绍了一些方便的 Go 编译选项，同时帮助了你更好地理解 Go 编译过程。关于构建过程的其他信息和其他有趣的选项，请参考 Go 命令帮助：$ go help build题图由 Ashraf Chemban 在 Pixabay 上发布。via: https://opensource.com/article/22/4/go-build-options作者：Gaurav Kamathe 选题：lkxed 译者：lkxed 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
硬核观察 #597 Fedora 项目负责人抨击英伟达的专有 Linux 驱动程序
Fedora 项目负责人抨击英伟达的专有 Linux 驱动程序据消息，Fedora 项目负责人称，Fedora/Red Hat 和其他公司已经做了“一大堆额外的费劲工作”来改善英伟达的专有驱动程序的 Linux 体验，但结果并不理想。他呼吁英伟达工程师为开源的 Nouveau 驱动程序做出贡献，由于缺乏对重启的支持，以及签名固件镜像的障碍，该驱动程序对英伟达的现代 GPU 相当无用。而与之对比的是，英特尔和 AMD 都支持开源驱动程序，并为上游 Linux 组件做出了巨大贡献。老王点评：看来 Torvalds 多年前指着鼻子的臭骂还不够啊。英国政府让皇家铸币厂铸造 NFT据消息，英国财政大臣已经要求英国皇家铸币厂在今年夏天发行一种 NFT。财政大臣说，这一宣布是“使英国成为全球加密资产技术中心”，并计划建立一个“金融市场基础设施沙盒”，让加密货币公司可以进行实验和创新。但另一方面，英国金融行为监管局也加强了对提供加密货币产品和服务的公司的监管，并警告消费者，加密货币资产是“高风险的”。老王点评：监管与拥抱，缺一不可，这才是开放的态度。谷歌要求每项 Go 语言变化都要有两名员工审查据消息，谷歌正计划加强其开源 Go 语言的安全性，现在要求在向用户发布每项改动之前，必须有两名谷歌员工进行审查，而以前只需要一名谷歌员工。审查者可以投下从 +2 到 -2 的票，至少有一个维护者对该修改投 +2 的票才能通过。但是也有贡献者对此改变不满，认为“这一变化有效地将提交者群体限制为谷歌员工”。老王点评：虽然初衷可能是为了保障 Go 语言的供应链安全，但是确实加强了谷歌对 Go 语言的控制力。
Go 团队接受了新增对模糊测试的支持的提议。Go 的应用越来越广泛。现在它是云原生软件、容器软件、命令行工具和数据库等等的首选语言。Go 很早之前就已经有了内建的 对测试的支持。这使得写测试代码和运行都相当简单。什么是模糊测试？ 模糊测试 fuzz testing （fuzzing）是指向你的软件输入非预期的数据。理想情况下，这种测试会让你的应用程序崩溃或有非预期的表现。抛开最终的结果，从程序对非预期的输入数据的处理结果中你可以得到很多信息，这样你就可以增加一些合适的错误处理。任何一个软件都有对不同来源的输入或数据的接收说明，软件会对这些数据进行处理并返回适当的结果。软件开发后，测试工程师团队对其进行测试，找出软件中的错误，给出测试报告，并（由开发者）修复。通常测试的目的是验证软件的行为是否符合预期。测试又可以细分为不同的类型，如功能测试、集成测试、性能测试等等。每种测试方法关注软件功能的某一个方面，以便发现错误或者提升可靠性或性能。模糊测试在这一测试过程上更进一步，尝试向软件程序输入一些“无效”或“随机”的数据。这种输入是故意的，期望得到的结果就是程序崩溃或输出异常，这样就可以暴露程序中的错误以便由开发者来修复它们。与其他测试类似，很少需要手动进行模糊测试，业界有大量的模糊测试工具可以将这个过程自动化。Go 中的软件测试举个例子，假如你想测试 add.go 中的 Add() 函数，你可以在 add_test.go 中导入 testing 包并把测试体写在以 TestXXX() 开头的函数内。考虑如下代码：func Add(num1, num2 int) int { } 在 add_test.go 文件中，你可能有如下测试代码：import "testing" func TestAdd(t *testing.T) { } 运行测试：$ go test 新增对模糊测试的支持Go 团队已经接受了 新增对模糊测试的支持的提议，以进一步推动这项工作。这涉及到新增一个 testing.F 类型，在 _test.go 文件中新增 FuzzXXX() 函数，在 Go 工具中会新增一个 -fuzz 选项来执行这些测试。在 add_test.go 文件中：func FuzzAdd(f *testing.F) { } 执行以下代码：$ go test -fuzz 在本文编写时，这个 功能还是试验性的，但是应该会在 1.18 发布版本中包含。（LCTT 译注：Go 1.18 刚刚发布，已经包含了对模糊测试的支持）目前很多功能如 -keepfuzzing、-race 等也还没有支持。Go 团队最近发布了一篇 模糊测试教程，值得读一下。安装 gotip 来获取最新的功能如果你极度渴望在正式发布之前尝试这些功能，你可以使用 gotip 来测试即将正式发布的 Go 功能并反馈给他们。你可以使用下面的命令来安装 gotip。安装之后，你可以用 gotip 程序代替以前的 go 程序来编译和运行程序。$ go install golang.org/dl/gotip@latest $ gotip download $ gotip version go version devel go1.18-f009910 Thu Jan 6 16:22:21 2022 +0000 linux/amd64 社区对于模糊测试的观点软件社区中经常会讨论模糊测试，不同的人对模糊测试有不同的看法。有些人认为这是一种有用的技术，可以找到错误，尤其是在安全方面。然而考虑到模糊测试所需要的资源（CPU、内存），有人就认为这是一种浪费，而他们更愿意用其他的测试方法。即使在 Go 团队内部，意见也不统一。我们可以看到 Go 的联合创始人 Rob Pike 对模糊测试的使用和在 Go 中的实现是持轻微的怀疑态度的。...虽然模糊测试有助于发现某类错误，但是它会占用大量的 CPU 和存储资源，并且效益成本比率也不明确。我担心为了写模糊测试浪费精力，或者 git 仓库中充斥大量无用的测试数据~Rob Pike然而，Go 安全团队的另一个成员，Filo Sottile，似乎对 Go 新增支持模糊测试很乐观，举了很多例子来支持，也希望模糊测试能成为开发过程中的一部分。我想说模糊测试可以发现极端情况下的错误。这是我们作为安全团队对其感兴趣的原因：在极端情况下发现的错误可以避免在生产环境中成为弱点。我们希望模糊测试能成为开发的一部分 —— 不只是构建或安全方面 —— 而是整个开发过程：它能提升相关代码的质量...~Filo Sottile现实中的模糊测试对我而言，模糊测试在发现错误以及让系统变得更安全和更有弹性方面似乎非常有效。举个例子，Linux 内核也会使用名为 syzkaller 的工具进行模糊测试，这个工具已经发现了 大量 错误。AFL 也是比较流行的模糊测试工具，用来测试 C/C++ 写的程序。之前也有对 Go 程序进行模糊测试的观点，其中之一就是 Filo 在 GitHub 评论中提到的 go-fuzz。go-fuzz 的记录提供了相当惊人的证据，证明模糊处理能很好地找到人类没有发现的错误。根据我的经验，我们只需要消耗一点点 CPU 的时间就可以得到极端情况下非常高效的测试结果。为什么在 Go 中新增对模糊测试的原生支持如果我们的需求是对 Go 程序进行模糊测试，之前的工具像 go-fuzz 就可以完成，那么为什么要在这种语言中增加原生支持呢？Go 模糊测试设计草案 中说明了这样做的一些根本原因。设计的思路是让开发过程更简单，因为前面说的工具增加了开发者的工作量，还有功能缺失。如果你没有接触过模糊测试，那么我建议你读一下设计草案文档。开发者可以使用诸如 go-fuzz 或 fzgo（基于 go-fuzz）来解决某些需求。然而，已有的每种解决方案都需要在典型的 Go 测试上做更多的事，而且还缺少关键的功能。相比于其他的 Go 测试（如基准测试和单元测试），模糊测试不应该比它们复杂，功能也不应该比它们少。已有的解决方案增加了额外的开销，比如自定义命令行工具。模糊测试工具在大家期望 Go 语言新增功能的列表中，模糊测试是其中很受欢迎的一项。虽然现在还是试验性的，但在将要到来的发布版本中会变得更强大。这给了我们足够的时间去尝试它以及探索它的使用场景。我们不应该把它视为一种开销，如果使用得当它会是一种发现错误非常高效的测试工具。使用 Go 的团队应该推动它的使用，开发者可以写简单的模糊测试，测试团队去慢慢扩展以此来使用它全部的能力。via: https://opensource.com/article/22/1/native-go-fuzz-testing作者：Gaurav Kamathe 选题：lujun9972 译者：lxbwolf 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出