蚊章汇 - Dave Cheney
type: channel
link: https://linuxcn.bwsl.wang/author/553/
description: Dave Cheney
items: 这儿几个字节，那里几个字节，我们说的是真正的内存
items: 在 Go 中如何转储一个方法的 GOSSAFUNC 图
items: 给开源项目贡献代码时：先讨论，再编码
items: 通过禁止比较让 Go 二进制文件变小
items: Go 中对栈中函数进行内联
items: Go 中的内联优化
items: 五种加速 Go 的特性
items: 从零开始学习 Go 语言的切片
items: 为什么使用 Go 语言？

这儿几个字节，那里几个字节，我们说的是真正的内存
type: item
date: 2023-11-15T23:48:58+08:00
description: 今天的帖子来自于最近的 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-arg…

在 Go 中如何转储一个方法的 GOSSAFUNC 图
type: item
date: 2020-06-26T16:39:00+08:00
description: Go 编译器的 SSA 后端包含一种工具，可以生成编译阶段的 HTML 调试输出。这篇文章介绍了如何为函数和方法打印 SSA 输出。让我们从一个包含函数、值方法和指针方法的示例程序开始：package main import ( "fmt" ) type Numbers struct { vals []int } func (n *Numbers) Add(v int) { n.vals = append(n.vals, v) } func (n Numbers) Average() float64 { sum := 0.0 for _, num := range n.vals { sum += float64(num) } return sum / float64(len(n.vals)) } func main() { var numbers Numbers numbers.Add(200) numbers.Add(43) numbers.Add(-6) fmt.Println(numbers.Average()) } 通过 GOSSAFUNC 环境变量控制 SSA 调试输出。此变量含有要转储的函数的名称。这不是函数的完全限定名。对于上面的 func main，函数名称为 main 而不是 main.main。% env GOSSAFUNC=main go build runtime dumped SSA to ../../go/src/runtime/ssa.html t dumped SSA to ./ssa.html 在这个例子中，GOSSAFUNC=main 同时匹配了 main.main 和一个名为 runtime.main 的函数。 [1] 这有点不走运，但是实际上可能没什么大不了的，因为如果你要对代码进行性能调整，它就不会出现在 func main 中的巨大的意大利面块中。你的代码更有可能在方法中，你可能已经看到这篇文章，并寻找能够转储方法的 SSA 输出。要为指针方法 func (n *Numbers) Add 打印 SSA 调试，等效函数名为 (*Numbers).Add： [2] % env "GOSSAFUNC=(*Numbers).Add" go build t dumped SSA to ./ssa.html 要为值方法 func (n Numbers) Average 打印 SSA 调试，等效函数名为 (*Numbers).Average，即使这是一个值方法：% env "GOSSAFUNC=(*Numbers).Average" go build t dumped SSA to ./ssa.html 如果你没有从源码构建 Go，那么 runtime 软件包的路径可能是只读的，并且可能会收到错误消息。请不要使用 sudo 来解决此问题。 ↩︎请注意 shell 引用 ↩︎via: https://dave.cheney.net/2020/06/19/how-to-dump-the-gossafunc-graph-for-a-method作者：Dave Cheney 选题：lujun9972 译者：geekpi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出

给开源项目贡献代码时：先讨论，再编码
type: item
date: 2020-06-17T00:01:02+08:00
description: 我所参与的开源项目遵循的是一种这样的理念，我把它描述为 “先讨论，再编码”。我认为一般来说这是开发软件的好方法，我想花一点时间来谈谈这种方法的好处。避免伤害感情先讨论你想做的改变最重要的原因是避免伤害感情。我经常看到一个贡献者闭门造车地提交了一个 PR，却发现他的努力工作被拒绝了。这可能有一堆原因：PR 太大了，PR 没有遵循本地风格，PR 修复了一个对项目不重要的问题或者最近间接修复了的问题，等等。这些问题的根本原因都是缺乏沟通。“先讨论，再编码” 理念的目标不是阻碍你或给你带来挫折，而是确保一个功能第一次就能正确落地，而不至于产生大量的维护债务。无论是改动的作者，还是审查者，当一个改动突然出现时，并暗示说 “好吧，我已经做完了，你要做的就是合并它，对吧？”，先讨论可以让他们不必背负伤害感情的情绪负担。讨论应该如何进行？每一个新功能或错误修复都应该在工作开始前与项目的维护者讨论。私下试验是可以的，但不要在没有讨论之前就发送修改。对于简单的改动，“讨论” 的定义可以只是 GitHub 议题中的一个设计草图。如果你的 PR 修复了一个 bug，你应该链接到它修复的 bug。如果没有，你应该先提出一个 bug，等待维护者确认后再发送 PR。这可能看起来有点落后 —— 谁不希望一个 bug 被修复呢 —— 但考虑到这个 bug 可能是对软件工作方式的误解，也可能是一个更大问题的症状，这需要进一步调查。对于比较复杂的改动，尤其是功能请求，我建议在发送代码之前，先分发一份设计文档并达成一致。这不一定是一个完整的文档，发一个议题，带个草图可能就足够了，但关键是在用代码搞定之前，先用文字达成一致。在任何情况下，你都不应该继续发送你的代码，直到维护者同意你的方法是他们所满意的。拉取请求是日常生活，而不仅仅是为了赶着过节。代码审查，而不是由委员会设计代码审查不是争论设计的地方。这有两个原因。首先，大多数代码审查工具都不适合长长的评论会话，GitHub 的 PR 界面在这方面非常糟糕，Gerrit 比较好，但很少有管理员团队会维护 Gerrit 实例。更重要的是，在代码审查阶段就出现了分歧，说明大家对如何实现这个变化并没有达成一致。讨论你想写的代码，然后再写你所讨论的代码。请不要反其道而行之。via: https://dave.cheney.net/2019/02/18/talk-then-code作者：Dave Cheney 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出

通过禁止比较让 Go 二进制文件变小
type: item
date: 2020-05-22T18:17:46+08:00
description: 大家常规的认知是，Go 程序中声明的类型越多，生成的二进制文件就越大。这个符合直觉，毕竟如果你写的代码不去操作定义的类型，那么定义一堆类型就没有意义了。然而，链接器的部分工作就是检测没有被程序引用的函数（比如说它们是一个库的一部分，其中只有一个子集的功能被使用），然后把它们从最后的编译产出中删除。常言道，“类型越多，二进制文件越大”，对于多数 Go 程序还是正确的。本文中我会深入讲解在 Go 程序的上下文中“相等”的意义，以及为什么像这样的修改会对 Go 程序的大小有重大的影响。定义两个值相等Go 的语法定义了“赋值”和“相等”的概念。赋值是把一个值赋给一个标识符的行为。并不是所有声明的标识符都可以被赋值，如常量和函数就不可以。相等是通过检查标识符的内容是否相等来比较两个标识符的行为。作为强类型语言，“相同”的概念从根源上被植入标识符的类型中。两个标识符只有是相同类型的前提下，才有可能相同。除此之外，值的类型定义了如何比较该类型的两个值。例如，整型是用算数方法进行比较的。对于指针类型，是否相等是指它们指向的地址是否相同。映射和通道等引用类型，跟指针类似，如果它们指向相同的地址，那么就认为它们是相同的。上面都是按位比较相等的例子，即值占用的内存的位模式是相同的，那么这些值就相等。这就是所谓的 memcmp，即内存比较，相等是通过比较两个内存区域的内容来定义的。记住这个思路，我过会儿再来谈。结构体相等除了整型、浮点型和指针等标量类型，还有复合类型：结构体。所有的结构体以程序中的顺序被排列在内存中。因此下面这个声明：type S struct { a, b, c, d int64 }会占用 32 字节的内存空间；a 占用 8 个字节，b 占用 8 个字节，以此类推。Go 的规则说如果结构体所有的字段都是可以比较的，那么结构体的值就是可以比较的。因此如果两个结构体所有的字段都相等，那么它们就相等。a := S{1, 2, 3, 4} b := S{1, 2, 3, 4} fmt.Println(a == b) // 输出 true编译器在底层使用 memcmp 来比较 a 的 32 个字节和 b 的 32 个字节。填充和对齐然而，在下面的场景下过分简单化的按位比较的策略会返回错误的结果：type S struct { a byte b uint64 c int16 d uint32 } func main() a := S{1, 2, 3, 4} b := S{1, 2, 3, 4} fmt.Println(a == b) // 输出 true }编译代码后，这个比较表达式的结果还是 true，但是编译器在底层并不能仅依赖比较 a 和 b 的位模式，因为结构体有填充。Go 要求结构体的所有字段都对齐。2 字节的值必须从偶数地址开始，4 字节的值必须从 4 的倍数地址开始，以此类推 1 。编译器根据字段的类型和底层平台加入了填充来确保字段都对齐。在填充之后，编译器实际上看到的是 2 ：type S struct { a byte _ [7]byte // 填充 b uint64 c int16 _ [2]int16 // 填充 d uint32 }填充的存在保证了字段正确对齐，而填充确实占用了内存空间，但是填充字节的内容是未知的。你可能会认为在 Go 中 填充字节都是 0，但实际上并不是 — 填充字节的内容是未定义的。由于它们并不是被定义为某个确定的值，因此按位比较会因为分布在 s 的 24 字节中的 9 个填充字节不一样而返回错误结果。Go 通过生成所谓的相等函数来解决这个问题。在这个例子中，s 的相等函数只比较函数中的字段略过填充部分，这样就能正确比较类型 s 的两个值。类型算法呵，这是个很大的设置，说明了为什么，对于 Go 程序中定义的每种类型，编译器都会生成几个支持函数，编译器内部把它们称作类型的算法。如果类型是一个映射的键，那么除相等函数外，编译器还会生成一个哈希函数。为了维持稳定，哈希函数在计算结果时也会像相等函数一样考虑诸如填充等因素。凭直觉判断编译器什么时候生成这些函数实际上很难，有时并不明显，（因为）这超出了你的预期，而且链接器也很难消除没有被使用的函数，因为反射往往导致链接器在裁剪类型时变得更保守。通过禁止比较来减小二进制文件的大小现在，我们来解释一下 Brad 的修改。向类型添加一个不可比较的字段 3 ，结构体也随之变成不可比较的，从而强制编译器不再生成相等函数和哈希函数，规避了链接器对那些类型的消除，在实际应用中减小了生成的二进制文件的大小。作为这项技术的一个例子，下面的程序：package main import "fmt" func main() { type t struct { // _ [0][]byt…

Go 中对栈中函数进行内联
type: item
date: 2020-05-05T07:04:20+08:00
description: 上一篇文章中我论述了 叶子内联 leaf inlining 是怎样让 Go 编译器减少函数调用的开销的，以及延伸出了跨函数边界的优化的机会。本文中，我要论述内联的限制以及叶子内联与 栈中内联 mid-stack inlining 的对比。内联的限制把函数内联到它的调用处消除了调用的开销，为编译器进行其他的优化提供了更好的机会，那么问题来了，既然内联这么好，内联得越多开销就越少，为什么不尽可能多地内联呢？内联可能会以增加程序大小换来更快的执行时间。限制内联的最主要原因是，创建许多函数的内联副本会增加编译时间，并导致生成更大的二进制文件的边际效应。即使把内联带来的进一步的优化机会考虑在内，太激进的内联也可能会增加生成的二进制文件的大小和编译时间。内联收益最大的是小函数，相对于调用它们的开销来说，这些函数做很少的工作。随着函数大小的增长，函数内部做的工作与函数调用的开销相比省下的时间越来越少。函数越大通常越复杂，因此优化其内联形式相对于原地优化的好处会减少。内联预算在编译过程中，每个函数的内联能力是用内联预算计算的 1 。开销的计算过程可以巧妙地内化，像一元和二元等简单操作，在 抽象语法数 Abstract Syntax Tree （AST）中通常是每个节点一个单位，更复杂的操作如 make 可能单位更多。考虑下面的例子：package main func small() string { s := "hello, " + "world!" return s } func large() string { s := "a" s += "b" s += "c" s += "d" s += "e" s += "f" s += "g" s += "h" s += "i" s += "j" s += "k" s += "l" s += "m" s += "n" s += "o" s += "p" s += "q" s += "r" s += "s" s += "t" s += "u" s += "v" s += "w" s += "x" s += "y" s += "z" return s } func main() { small() large() }使用 -gcflags=-m=2 参数编译这个函数能让我们看到编译器分配给每个函数的开销：% go build -gcflags=-m=2 inl.go # command-line-arguments ./inl.go:3:6: can inline small with cost 7 as: func() string { s := "hello, world!"; return s } ./inl.go:8:6: cannot inline large: function too complex: cost 82 exceeds budget 80 ./inl.go:38:6: can inline main with cost 68 as: func() { small(); large() } ./inl.go:39:7: inlining call to small func() string { s := "hello, world!"; return s }编译器根据函数 func small() 的开销（7）决定可以对它内联，而 func large() 的开销太大，编译器决定不进行内联。func main() 被标记为适合内联的，分配了 68 的开销；其中 small 占用 7，调用 small 函数占用 57，剩余的（4）是它自己的开销。可以用 -gcflag=-l 参数控制内联预算的等级。下面是可使用的值：-gcflags=-l=0 默认的内联等级。-gcflags=-l（或 -gcflags=-l=1）取消内联。-gcflags=-l=2 和 -gcflags=-l=3 现在已经不使用了。和 -gcflags=-l=0 相比没有区别。-gcflags=-l=4 减少非叶子函数和通过接口调用的函数的开销。 2 不确定语句的优化一些函数虽然内联的开销很小，但由于太复杂它们仍不适合进行内联。这就是函数的不确定性，因为一些操作的语义在内联后很难去推导，如 recover、break。其他的操作，如 select 和 go 涉及运行时的协调，因此内联后引入的额外的开销不能抵消内联带来的收益。不确定的语句也包括 for 和 range，这些语句不一定开销很大，但目前为止还没有对它们进行优化。栈中函数优化在过去，Go 编译器只对叶子函数进行内联 —— 只有那些不调用其他函数的函数才有资格。在上一段不确定的语句的探讨内容中，一次函数调用就会让这个函数失去内联的资格。进入栈中进行内…

Go 中的内联优化
type: item
date: 2020-05-03T06:22:47+08:00
description: 本文讨论 Go 编译器是如何实现内联的，以及这种优化方法如何影响你的 Go 代码。请注意：本文重点讨论 gc，这是来自 golang.org 的事实标准的 Go 编译器。讨论到的概念可以广泛适用于其它 Go 编译器，如 gccgo 和 llgo，但它们在实现方式和功效上可能有所差异。内联是什么？ 内联 inlining 就是把简短的函数在调用它的地方展开。在计算机发展历程的早期，这个优化是由程序员手动实现的。现在，内联已经成为编译过程中自动实现的基本优化过程的其中一步。为什么内联很重要？有两个原因。第一个是它消除了函数调用本身的开销。第二个是它使得编译器能更高效地执行其他的优化策略。函数调用的开销在任何语言中，调用一个函数 1 都会有消耗。把参数编组进寄存器或放入栈中（取决于 ABI），在返回结果时的逆反过程都会有开销。引入一次函数调用会导致程序计数器从指令流的一点跳到另一点，这可能导致管道滞后。函数内部通常有 前置处理 preamble ，需要为函数执行准备新的栈帧，还有与前置相似的 后续处理 epilogue ，需要在返回给调用方之前释放栈帧空间。在 Go 中函数调用会消耗额外的资源来支持栈的动态增长。在进入函数时，goroutine 可用的栈空间与函数需要的空间大小进行比较。如果可用空间不同，前置处理就会跳到 运行时 runtime 的逻辑中，通过把数据复制到一块新的、更大的空间的来增长栈空间。当这个复制完成后，运行时就会跳回到原来的函数入口，再执行栈空间检查，现在通过了检查，函数调用继续执行。这种方式下，goroutine 开始时可以申请很小的栈空间，在有需要时再申请更大的空间。 2 这个检查消耗很小，只有几个指令，而且由于 goroutine 的栈是成几何级数增长的，因此这个检查很少失败。这样，现代处理器的分支预测单元可以通过假定检查肯定会成功来隐藏栈空间检查的消耗。当处理器预测错了栈空间检查，不得不放弃它在推测性执行所做的操作时，与为了增加 goroutine 的栈空间运行时所需的操作消耗的资源相比，管道滞后的代价更小。虽然现代处理器可以用预测性执行技术优化每次函数调用中的泛型和 Go 特定的元素的开销，但那些开销不能被完全消除，因此在每次函数调用执行必要的工作过程中都会有性能消耗。一次函数调用本身的开销是固定的，与更大的函数相比，调用小函数的代价更大，因为在每次调用过程中它们做的有用的工作更少。因此，消除这些开销的方法必须是要消除函数调用本身，Go 的编译器就是这么做的，在某些条件下通过用函数的内容来替换函数调用来实现。这个过程被称为内联，因为它在函数调用处把函数体展开了。改进的优化机会Cliff Click 博士把内联描述为现代编译器做的优化措施，像常量传播（LCTT 译注：此处作者笔误，原文为 constant proportion，修正为 constant propagation）和死代码消除一样，都是编译器的基本优化方法。实际上，内联可以让编译器看得更深，使编译器可以观察调用的特定函数的上下文内容，可以看到能继续简化或彻底消除的逻辑。由于可以递归地执行内联，因此不仅可以在每个独立的函数上下文处进行这种优化决策，也可以在整个函数调用链中进行。实践中的内联下面这个例子可以演示内联的影响：package main import "testing" //go:noinline func max(a, b int) int { if a > b { return a } return b } var Result int func BenchmarkMax(b *testing.B) { var r int for i := 0; i < b.N; i++ { r = max(-1, i) } Result = r }运行这个基准，会得到如下结果： 3 % go test -bench=. BenchmarkMax-4 530687617 2.24 ns/op在我的 2015 MacBook Air 上 max(-1, i) 的耗时约为 2.24 纳秒。现在去掉 //go:noinline 编译指令，再看下结果：% go test -bench=. BenchmarkMax-4 1000000000 0.514 ns/op从 2.24 纳秒降到了 0.51 纳秒，或者从 benchstat 的结果可以看出，有 78% 的提升。% benchstat {old,new}.txt name old time/op new time/op delta Max-4 2.21ns ± 1% 0.49ns ± 6% -77.96% (p=0.000 n=18+19)这个提升是从哪儿来的呢？首先，移除掉函数调用以及与之关联的前置…

五种加速 Go 的特性
type: item
date: 2018-10-11T23:06:40+08:00
description: Anthony Starks 使用他出色的 Deck 演示工具重构了我原来的基于 Google Slides 的幻灯片。你可以在他的博客上查看他重构后的幻灯片, mindchunk.blogspot.com.au/2014/06/remixing-with-deck。我最近被邀请在 Gocon 发表演讲，这是一个每半年在日本东京举行的 Go 的精彩大会。Gocon 2014 是一个完全由社区驱动的为期一天的活动，由培训和一整个下午的围绕着生产环境中的 Go 这个主题的演讲组成.（LCTT 译注：本文发表于 2014 年）以下是我的讲义。原文的结构能让我缓慢而清晰的演讲，因此我已经编辑了它使其更可读。我要感谢 Bill Kennedy 和 Minux Ma，特别是 Josh Bleecher Snyder，感谢他们在我准备这次演讲中的帮助。大家下午好。我叫 David.我很高兴今天能来到 Gocon。我想参加这个会议已经两年了，我很感谢主办方能提供给我向你们演讲的机会。我想以一个问题开始我的演讲。为什么选择 Go？当大家讨论学习或在生产环境中使用 Go 的原因时，答案不一而足，但因为以下三个原因的最多。这就是 TOP3 的原因。第一，并发。Go 的 并发原语 Concurrency Primitives 对于来自 Nodejs，Ruby 或 Python 等单线程脚本语言的程序员，或者来自 C++ 或 Java 等重量级线程模型的语言都很有吸引力。易于部署。我们今天从经验丰富的 Gophers 那里听说过，他们非常欣赏部署 Go 应用的简单性。然后是性能。我相信人们选择 Go 的一个重要原因是它 快。在今天的演讲中，我想讨论五个有助于提高 Go 性能的特性。我还将与大家分享 Go 如何实现这些特性的细节。我要谈的第一个特性是 Go 对于值的高效处理和存储。这是 Go 中一个值的例子。编译时，gocon 正好消耗四个字节的内存。让我们将 Go 与其他一些语言进行比较由于 Python 表示变量的方式的开销，使用 Python 存储相同的值会消耗六倍的内存。Python 使用额外的内存来跟踪类型信息，进行 引用计数 Reference Counting 等。让我们看另一个例子：与 Go 类似，Java 消耗 4 个字节的内存来存储 int 型。但是，要在像 List 或 Map 这样的集合中使用此值，编译器必须将其转换为 Integer 对象。因此，Java 中的整数通常消耗 16 到 24 个字节的内存。为什么这很重要？ 内存便宜且充足，为什么这个开销很重要？这是一张显示 CPU 时钟速度与内存总线速度的图表。请注意 CPU 时钟速度和内存总线速度之间的差距如何继续扩大。两者之间的差异实际上是 CPU 花费多少时间等待内存。自 1960 年代后期以来，CPU 设计师已经意识到了这个问题。他们的解决方案是一个缓存，一个更小、更快的内存区域，介入 CPU 和主存之间。这是一个 Location 类型，它保存物体在三维空间中的位置。它是用 Go 编写的，因此每个 Location 只消耗 24 个字节的存储空间。我们可以使用这种类型来构造一个容纳 1000 个 Location 的数组类型，它只消耗 24000 字节的内存。在数组内部，Location 结构体是顺序存储的，而不是随机存储的 1000 个 Location 结构体的指针。这很重要，因为现在所有 1000 个 Location 结构体都按顺序放在缓存中，紧密排列在一起。Go 允许您创建紧凑的数据结构，避免不必要的填充字节。紧凑的数据结构能更好地利用缓存。更好的缓存利用率可带来更好的性能。函数调用不是无开销的。调用函数时会发生三件事。创建一个新的 栈帧 Stack Frame ，并记录调用者的详细信息。在函数调用期间可能被覆盖的任何寄存器都将保存到栈中。处理器计算函数的地址并执行到该新地址的分支。由于函数调用是非常常见的操作，因此 CPU 设计师一直在努力优化此过程，但他们无法消除开销。函调固有开销，或重于泰山，或轻于鸿毛，这取决于函数做了什么。减少函数调用开销的解决方案是 内联 Inlining 。Go 编译器通过将函数体视为调用者的一部分来内联函数。内联也有成本，它增加了二进制文件大小。只有当调用开销与函数所做工作关联度的很大时内联才有意义，因此只有简单的函数才能用于内联。复杂的函数通常不受调用它们的开销所支配，因此不会内联。这个例子显示函数 Double 调用 util.Max。为了减少调用 util.Max 的开销，编译器可以将 util.Max 内联到 Double 中，就象这样内联后不再调用 util.Max，但是 Double 的行为没有改变。内联并不是 Go …

从零开始学习 Go 语言的切片
type: item
date: 2018-08-20T05:59:00+08:00
description: 这篇文章受到了我与同事讨论使用 切片 slice 作为 栈 stack 的一次聊天的启发。后来话题聊到了 Go 语言中的切片是如何工作的。我认为这些信息对别人也有用，所以就把它记录了下来。数组任何关于 Go 语言切片的讨论都要从另一个数据结构也就是 数组 array 开始。Go 的数组有两个特性：数组的长度是固定的；[5]int 是由 5 个 int 构成的数组，和 [3]int 不同。数组是值类型。看下面这个示例：package main import "fmt" func main() { var a [5]int b := a b[2] = 7 fmt.Println(a, b) // prints [0 0 0 0 0] [0 0 7 0 0] }语句 b := a 定义了一个类型是 [5]int 的新变量 b，然后把 a 中的内容 复制 到 b 中。改变 b 对 a 中的内容没有影响，因为 a 和 b 是相互独立的值。 1 切片Go 语言的切片和数组的主要有如下两个区别：切片没有一个固定的长度。切片的长度不是它类型定义的一部分，而是由切片内部自己维护的。我们可以使用内置的 len 函数知道它的长度。 2 将一个切片赋值给另一个切片时 不会 对切片内容进行复制操作。这是因为切片没有直接持有其内部数据，而是保留了一个指向 底层数组 3 的指针。数据都保留在底层数组里。基于第二个特性，两个切片可以享有共同的底层数组。看下面的示例：对切片取切片package main import "fmt" func main() { var a = []int{1,2,3,4,5} b := a[2:] b[0] = 0 fmt.Println(a, b) // prints [1 2 0 4 5] [0 4 5] }在这个例子里，a 和 b 享有共同的底层数组 —— 尽管 b 在数组里的起始偏移量不同，两者的长度也不同。通过 b 修改底层数组的值也会导致 a 里的值的改变。将切片传进函数package main import "fmt" func negate(s []int) { for i := range s { s[i] = -s[i] } } func main() { var a = []int{1, 2, 3, 4, 5} negate(a) fmt.Println(a) // prints [-1 -2 -3 -4 -5] }在这个例子里，a 作为形参 s 的实参传进了 negate 函数，这个函数遍历 s 内的元素并改变其符号。尽管 nagate 没有返回值，且没有访问到 main 函数里的 a。但是当将之传进 negate 函数内时，a 里面的值却被改变了。大多数程序员都能直观地了解 Go 语言切片的底层数组是如何工作的，因为它与其它语言中类似数组的工作方式类似。比如下面就是使用 Python 重写的这一小节的第一个示例：Python 2.7.10 (default, Feb 7 2017, 00:08:15) [GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.34)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> a = [1,2,3,4,5] >>> b = a >>> b[2] = 0 >>> a [1, 2, 0, 4, 5]以及使用 Ruby 重写的版本:irb(main):001:0> a = [1,2,3,4,5] => [1, 2, 3, 4, 5] irb(main):002:0> b = a => [1, 2, 3, 4, 5] irb(main):003:0> b[2] = 0 => 0 irb(main):004:0> a => [1, 2, 0, 4, 5]在大多数将数组视为对象或者是引用类型的语言也是如此。 4 切片头切片同时拥有值和指针特性的神奇之处在于理解切片实际上是一个 结构体 struct 类型。通常在 反射 reflect 包内相应部分之后的这个结构体被称作 切片头 slice header 。切片头的定义大致如下：package runtime type slice struct { ptr unsafe.Pointer len int cap int }这很重要，因为和 map 以及 chan 这两个类型不同，切片是值类型，当被赋值或者被作为参数传入函数时候会被复制过去。程序员们都能理解 square 的形参 v 和 main 中声明的 v 的是相互独立的。请看下面的例子：package main …

为什么使用 Go 语言？
type: item
date: 2017-04-03T02:04:00+08:00
description: 几个星期前，我一个朋友问我：“为什么要关心 Go 语言”？ 因为他们知道我热衷于 Go 语言，但他们想知道为什么我认为其他人也应该关心。本文包含三个我认为 Go 是重要的编程语言的原因。安全个人而言，你和我或许完全有能力在 C 中编写程序，既不会泄漏内存，也不会不安全地重复使用内存。然而，整体上，即使有超过 40 年)的经验，用 C 的程序员也无法可靠地这样做。尽管静态代码分析、valgrind、tsan 以及 “-Werror” 已经存在了几十年，却很少有证据表明这些工具被广泛认可，更不用说广泛采用。总而言之，事实表明，程序员根本无法安全地管理自己的内存。现在是离开 C 的时候了。Go 不需要程序员直接管理内存，所有内存分配都由语言运行时自行管理，使用前初始化，必要时检查边界。它肯定不是提供这些安全保障的第一个主流语言，Java（1995）可能是该冠军的竞争者。关键是，世界对不安全的编程语言没有胃口，所以人们默认认为，Go 是内存安全的。开发人员生产力从 20 世纪 70 年代末，开发人员的时间变得比硬件所耗费的时间更昂贵了。开发人员的生产力是一个不断扩展的话题，但它归结为这一点：你花了多少时间做有用的工作，又有多少时间等待编译器或者失望地迷失在外部代码库中。有个笑话说 Go 是在等待 C ++ 程序编译时开发的。快速编译是 Go 的一个重要功能，也是吸引新开发人员的关键工具。虽然编译速度仍然是一个永久的战场，但公平地说，在其他语言中需要几分钟的编译，在 Go 中只需要几秒钟。Go 程序员意识到生产力的更根本的问题是代码是为了读而写的，所以将代码的阅读行为放在编写之上。Go 通过工具和自定义来强制所有代码格式化成特定的样式。这消除了学习项目特定语言的方言时的困难，并有助于发现错误，因为它们看上去就是不正确。由于专注于分析和机器辅助，Go 开发人员开始采用越来越多的工具来发现常见的编码错误，这种工具从来没有在 C 语言开发者中产生共鸣 - Go 开发人员希望工具帮助他们保持代码清洁。并发性十多年来，芯片设计师一直在警告免费午餐将会结束。从最低端的手机到最耗电的服务器，硬件的并行性以更多、更慢、堆砌 cpu 内核的形式出现，但只有当你的语言可以利用它们才有意义。因此，并发特性需要内置到我们编写的要在今天的硬件上运行的软件中。通过提供一种基于协程的轻量级并发模型，或者是 Go 中已知的 goroutines，Go 超越了那些暴露操作系统的多进程或多线程并行模型的语言。goroutines 允许程序员避开复杂的回调，而语言运行时确保有足够的线程来保持你的内核的活跃。总结我给朋友推荐 Go 有三个原因：安全性、生产力和并发性。有些语言可以涵盖一个也有可能是两个方面，但是这三个方面的结合使得 Go 成为主流程序员的绝佳选择。相关文章：为什么 Go 和 Rust 不是竞争对手听听我在 OSCON 上谈 Go 语言性能我在 GopherChina 和 GopherCon Singapore 中的演讲压力测试你 Go 包via: https://dave.cheney.net/2017/03/20/why-go作者：Dave Cheney 译者：geekpi 校对：jasminepeng本文由 LCTT 原创编译，Linux中国 荣誉推出