标签 Go 下的文章

1 Go 程序员大多使用 Linux 或 MacOS

Go 团队在八月份对 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.Stringssort.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            .          .           } 

发生这种分配是因为编译器当前无法确认 sssi 生存期更长。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 字节的尺寸类别,非常适合分配给接口的切片值。

相关文章

  1. 我在 Devfest 2017年西伯利亚大会谈 Go 语言
  2. 如果对齐的内存写操作是原子性的,为什么我们还需要 sync/atomic 包呢?
  3. 为你的树莓派创建一个真实的串行控制台
  4. 为什么 Go 语言线程的栈是无限制的?

(题图:MJ/01d5fe46-778f-48fe-9481-162f4d0289dc)


  1. 这不是正确的对排序函数进行基准测试的方式,因为在第一次迭代之后,输入已经排序。但这又是另外一个话题了。 ↩︎
  2. 此语句的准确性取决于所使用的 Go 版本。例如,Go 1.15 版本添加了直接将一些 整数存储在接口值 中的功能,从而节省了分配和间接性。然而,对于大多数值来说,如果它不是指针类型,它的地址将被取出并存储在接口值中。 ↩︎
  3. 编译器在接口值的类型字段中跟踪了这种手法,因此它记住了分配给 si 的类型是 sort.StringSlice 而不是 *sort.StringSlice↩︎
  4. 在 32 位平台上,这个数字减半,但我们不再关注它↩︎
  5. 如果你准备限制分配为 4G 或者可能是 64KB,你可以使用较少内存来存储分配的尺寸,但实际上使用小于一个字来存储长度标头的节省会受到填充的影响。 ↩︎
  6. 将相同大小的物品存储在一起也是一种有效的对抗碎片化的策略。 ↩︎
  7. 这并不是一个不切实际的设想,字符串有各种形状和大小,生成以前未见过的大小的字符串可能就像附加空格一样简单。 ↩︎

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.goslice.gorefArray.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.gocp2.gocp3.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.gocp2.gocp3.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.Readerio.Writer 接口。此外,main() 函数中还调用了 rand.Seed() 函数,用于初始化随机数生成器。最后,for 循环允许程序一直使用 Accept() 函数来接受 TCP 客户端的连接请求,并以 goroutine 的方式来运行 handleConnection(c) 函数,处理客户端的后续请求。

net.Listen() 的第一个参数

net.Listen() 函数的第一个参数定义了使用的网络类型,而第二个参数定义了服务端监听的地址和端口号。第一个参数的有效值为 tcptcp4tcp6udpudp4udp6ipip4ip6Unix(Unix 套接字)、UnixgramUnixpacket,其中:tcp4udp4ip4 只接受 IPv4 地址,而 tcp6udp6ip6 只接受 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 客户端时的输出:

concTCP.go TCP 服务端测试

类似地,下面这张图片显示了两个 TCP 客户端的输出(使用了 nc 工具):

是用 nc 工具作为 concTCP.go 的 TCP 客户端

你可以在 维基百科 上找到更多关于 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中国 荣誉推出