Brantou的日常

二三事


  • 首页

  • 归档

  • 分类

  • 标签

  • 关于

  • 搜索

Go的测试覆盖率

发表于 2017-05-24 | 分类于 技术积累 |

测试覆盖率是一个术语,用于统计通过运行程序包的测试多少代码得到执行。 如果执行测试套件导致80%的语句得到了运行,则测试覆盖率为80%。

计算测试覆盖率的通常方法是埋点二进制可执行文件。 例如,GNU gcov 在二进制文件中设置执行分支断点。 当每个分支执行时,断点被清除,并且分支的目标语句被标记为“被覆盖”。

这种方法是成功和广泛使用的。 Go的早期测试覆盖工具甚至以相同的方式工作。但它有问题。 由于分析二进制文件的执行是很困难的,所以很难实现。 它还需要将执行跟踪绑定回源代码的可靠方法,这也可能是困难的。 那里的问题包括不正确的调试信息和诸如内联功能的问题, 使分析变得复杂。 最重要的是,这种方法非常不具有可移植性。 对于每个机器架构需要重新编写,在某种程度上,可能对于每个操作系统都需要重新编写,因为从系统到系统的调试支持差异很大。

Go 1.2 的发布引入了一个 test coverage 的新工具, 它采用了一种不寻常的方式来生成覆盖率统计数据,这种方法建立在Godoc的技术的基础上。

1 Go的测试覆盖率

对于Go的新测试覆盖工具,采取了一种避免动态调试的不同方法。 想法很简单:在编译之前重写包的源代码,以埋点,编译和运行修改的源,并转储统计信息。 重写很容易编排,因为 go的工具链 控制从源到测试到执行的整个流程。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func Size(a int) string {
switch {
case a < 0:
return "negative"
case a == 0:
return "zero"
case a < 10:
return "small"
case a < 100:
return "big"
case a < 1000:
return "huge"
}
return "enormous"
}

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Test struct {
in int
out string
}

var tests = []Test{
{-1, "negative"},
{5, "small"},
}

func TestSize(t *testing.T) {
for i, test := range tests {
size := Size(test.in)
if size != test.out {
t.Errorf("#%d: Size(%d)=%s; want %s", i, test.in, size, test.out)
}
}
}

执行代码覆盖率测试如下:

1
2
3
cd ../src/cover/size/
go test -cover
cd -
PASS
coverage: 42.9% of statements
ok      _/home/parallels/program/org/github-pages/source/src/cover/size 0.001s
/home/parallels/program/org/github-pages/source/_posts

启用测试覆盖后,/go test/ 运行 cover 工具,在编译之前重写源代码。 以下是重写后的 Size 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func Size(a int) string {
GoCover.Count[0] = 1
switch {
case a < 0:
GoCover.Count[2] = 1
return "negative"
case a == 0:
GoCover.Count[3] = 1
return "zero"
case a < 10:
GoCover.Count[4] = 1
return "small"
case a < 100:
GoCover.Count[5] = 1
return "big"
case a < 1000:
GoCover.Count[6] = 1
return "huge"
}
GoCover.Count[1] = 1
return "enormous"
}

上面示例的每个可执行部分用赋值语句进行注解,赋值语句用于在运行时做统计。 计数器与 cover 工具生成的第二个只读数据结构记录的语句的原始源位置相关联。 测试运行完成后,收集计数器,通过查看设置的数量的来计算百分比。

虽然分配注解看起来可能很昂贵,但是它被编译为单个“移动”指令。 因此,其运行时开销不大,运行典型(或更实际)测试时只增加约3%开销。 这使得把测试覆盖率作为标准开发流程的一部分是合情合理的。

2 查看结果

上面的例子的测试覆盖率很差。 为了探索具体为什么,需要 go test 写一个 coverage profile , 这是一个保存收集的统计信息的文件,以便能详细地研究覆盖的细节。 这很容易做:使用 -coverprofile 标志来指定输出的文件:

1
2
cd ../src/cover/size/
go test -coverprofile=size_coverage.out

注: -coverprofile 标志自动设置 -cover 来启用覆盖率分析。

测试与以前一样运行,但结果保存在文件中。 要研究它们,需要运行 test coverage tool 。 一开始,可以要求 覆盖率 按函数分解,虽然在当前情况下没有太多意义,因为只有一个函数:

1
2
cd ../src/cover/size/
go tool cover -func=size_coverage.out

查看的更有趣的方式是获取 覆盖率信息注释的源代码 的HTML展示。 该显示由 -html 标志调用:

1
2
cd ../src/cover/size/
go tool cover -html=size_coverage.out

运行此命令时,浏览器将弹出窗口,已覆盖(绿色),未覆盖(红色)和 未埋点(灰色)。 下面是一个屏幕截图:

<img src="/images/go-test-cover-set.png" />

有了这个信息页,问题变得很明显:上面忽略了几个 case 的测试! 可以准确地看出具体是哪一个,这样可以轻松地提高的测试覆盖率。

3 热力图

源代码级方式来测试覆盖率的一大优点在于,可以很容易用不同的方式对代码进行埋点处理。 例如,不仅可以检测是否已执行了一个语句,而且还可以查询执行了多少次。

go test 命令接受 -covermode 标志将覆盖模式设置为三种设置之一:

  • set: 每个语句是否执行?
  • count: 每个语句执行了几次?
  • atomic: 类似于 count, 但表示的是并行程序中的精确计数

set 是默认设置,上面示例已经看到了。 只有运行并行算法需要精确的计数时,才需要进行 atomic 设置。 它使用来自 sync/atomic 包的原子操作,这可能会相当昂贵。 然而,对于大多数情况, count 模式工作正常,并且像默认设置模式一样非常快。

下面来试试一个标准包, fmt 格式化包语句执行的计数。 进行测试并写出 coverage profile ,以便能够很好地进行信息的呈现。

go test -covermode=count -coverprofile=../src/cover/count.out fmt

这比以前的例子好的测试覆盖率。 (覆盖率不受覆盖模式的影响)可以显示函数细节:

go tool cover -func=../src/cover/count.out

HTML输出产生了巨大的回报:

go tool cover -html=../src/cover/count.out

pad 函数如下所示:

<img src="/images/go-test-cover-count.png" />

注意绿色的强度是如何变化。 最明亮的绿色的代表较高的执行数; 较少灰暗的绿色代表较低的执行数。 甚至可以将鼠标悬停在语句上,以便在弹出的 tool tip 中提示实际计数。 test coverage 产生了关于函数执行的大量信息,在分析中很有用的信息。

4 基础块

你可能已经注意到,上一个示例中/ 有关于闭合大括号中间的行的计数/ 不是你所期望的那样。 这是因为一直以来 test coverage 都不是一个不精确的科学。

这里发生的很值得解释。 我们希望覆盖注解由程序中的分支划分,当二进制文件在传统方法中被调用时,它们是分开的。 不过,通过重写源代码很难做到这一点,因为分支没有明确展示在源代码中。

覆盖注解的作用是是埋点,通常由大括号来限定。 一般来说,使之工作正常是非常困难的。 所使用的算法的处理结果是闭合括号看起来像属于它配对的块,而开放大括号看起来像属于块之外。 一个更有趣的结果出现在如下的一个表达式中:

f() && g()

没有试图单独调用对f和g的调用,无论事实如何,它们总是看起来像是运行相同的次数。

公平来说,即使gcov在这里也有麻烦。 该工具使机制正确,但呈现是基于行的,因此可能会错过一些细微差别。

5 总结

这是关于 Go 1.2 test coverage 故事。 具有有趣实现的新工具不仅可以实现测试覆盖率的统计,而且易于解释,甚至可以提取 profile 信息。

测试是软件开发和的重要组成部分,/test coverage/ 为测试策略添加一个简单的标准。 走向前, test 和 cover 。

Last Updated 2017-05-25 Thu 16:59.
Render by hexo-renderer-org with Emacs 25.3.2 (Org mode 8.2.10)

数组,切片:‘append’的机制

发表于 2017-05-24 | 分类于 技术积累 |

编程语言最常见的特征之一是数组的概念。 数组看起来像简单的东西,但是在将它添加到语言中时,必须回答许多问题,例如:

  • 固定的size,还是可变的size?
  • size是类型的一部分吗?
  • 多维数组看起来是什么样子的?
  • 空数组代表了什么含义?

以上问题的如何解答,影响着数组在语言中的定位,仅仅是语言的特性还是其设计的核心部分。

在Go的早期发展中,在设计感觉正确之前,花了大约一年时间来决定这些问题的答案。 关键的一步是引入了 切片 ,其建立在固定大小的数组之上,并提供灵活的,可扩展的数据结构。 不过,到目前为止,Go的新手经常会在 切片 的工作方式上遇到问题, 也许是因为来自其他语言的经验使他们戴着有色眼镜来看待 切片 。

在这篇文章中,将尝试清除混淆。 将通过构建代码来解释 append 内置函数的工作原理以及为什么它如此的工作。

<!– more –>

1 数组

数组是Go中的重要组成部分,但像建筑的基础一样,它们通常隐藏在更可见的组件之下。 这里先简要介绍一下它们,然后再继续讨论 切片 更有趣,强大和突出的特性。

数组在Go程序中不常见,因为数组的大小是其类型的一部分,这限制了其表达力。

var buffer [256]byte

该声明声明了变量 buffer ,其中包含256个字节。 缓冲区的类型包括其大小 1byte 。 512字节的数组将是 2byte , 是不同的类型。

与数组关联的数据就是这样的:一个元素数组。 显然, buffer 在内存中看起来像这样,

buffer: byte byte byte ... 256 times ... byte byte byte

也就是说,变量除了保存256字节的数据之外,没有其他的。 可以使用熟悉的索引语法, buffer3,buffer4等一直到buffer5来访问它的元素。 (索引范围0到255涵盖256个元素) 尝试使用超出该范围的值对buffer进行索引时,会导致程序崩溃。

内置的函数 len ,它返回数组和 切片 以及其他一些数据类型的元素个数。 对于数组,len返回是很清楚明确的。 在上面的例子中, len(buffer) 返回固定值256。

Go的数组是值类型。 数组变量表示整个数组; 它不是指向第一个数组元素的指针(如C中的情况)。 这意味着,当分配或传递数组值时,将产生出其内容的副本。 (为了避免复制,可以传递一个指向数组的指针,但是这是一个指向数组的指针,而不是一个数组)。 思考数组的一种方法是将其作为一种结构体,但是使用索引而不是命名字段:fixed-size的复合值。

数组有特定的使用场景 – 矩阵变换就是很好的例子 - 但是在Go中最常通常的用途就是用作一个 切片 的存储空间。

2 切片: slice header

切片是很容易使用,但是要很好地使用它们,必须明确了解它是什么以及它能做什么。

切片是描述与切片变量本身分开存储的数组连续部分的数据结构。 切片不是数组。 切片描述一个数组的一个片段。

给定上一节的 buffer 数组变量,可以通过对数组进行切片来创建描述元素100到150 (确切地说,包括100到149)的切片:

var slice []byte = buffer[100:150]

变量 slice 具有类型 []byte ,读作 slice of bytes , 并被初始化为数组从元素100(包括)到150(排他)的切片。 更惯用的语法,删除类型,由初始化表达式来设置类型:

var slice = buffer[100:150]

在函数中可使用更短的声明形式:

slice := buffer[100:150]

这个 slice 变量究竟是什么? 这不完全是一个完整的故事, 但是现在可把切片看作一个有两个元素的小数据结构:一个长度和一个指向数组元素的指针。 你可想到,其实幕后就是如此构造:

1
2
3
4
5
6
7
8
9
type sliceHeader struct {
Length int
ZerothElement *byte
}

slice := sliceHeader{
Length: 50,
ZerothElement: &buffer[100],
}

当然这只是一个示例。 尽管这段代码片段已表明 sliceHeader 结构对程序员是不可见的, 元素指针的类型取决于元素的类型,但这给出了机制的共同之处。

当目前为止只做了数组的切片操作,但是切片上也可作切片操作:

slice2 := slice[5:10]

像以前一样,这个操作创建一个新的切片,在这种情况下,原始切片的元素5到9(包括),意味着原始阵列的元素105到109。 slice2变量的底层sliceHeader结构如下所示:

1
2
3
4
slice2 := sliceHeader{
Length: 5,
ZerothElement: &buffer[105],
}

注:此 header 仍然指向相同的底层数组,存储在 buffer 变量中。

也可以二次切片,也就是说切片并将结果存储回原来的切片结构。

slice = slice[5:10]

经过上面的二次切片,slice变量的sliceHeader结构与slice2变量的一样。 可能会经常看到二次切片,例如截断切片。 此语句删除了我们的切片的第一个和最后一个元素:

slice = slice[1:len(slice)-1]

经过上面的而且切片后, sliceHeader 如下所示:

1
2
3
4
slice := sliceHeader{
Length: 3,
ZerothElement: &buffer[104],
}

你会经常听到有经验的Go程序员谈论 slice header ,因为它真的是存储在切片变量中。 例如,当你调用将切片作为参数的函数(如bytes.IndexRune)时,该 header 就是传递给该函数的参数。 在下面调用中, slice 是入参,但事实上却是 slice header 。

slashPos := bytes.IndexRune(slice, '/')

在 slice header 中还有一个数据项,会在下面讨论,但首先看看当你使用 slice 编程时, slice header 的存在意味着什么。

3 切片作为函数的入参

很重要的一点,一个slice包含一个指针,但它本身就是一个值。 是一个保存指针和长度的结构体的值, 不是指向结构体的指针。

当在前面的例子中调用 IndexRune 时,传递了一个 slice header 的副本。 这样的方式有重要的副作用。

思考下面这个简单的函数:

1
2
3
4
5
func AddOneToEachElement(slice []byte) {
for i := range slice {
slice[i]++
}
}

迭代一个切片的索引(使用范围循环),增加其元素。 调用如下:

1
2
3
4
5
6
7
8
9
10
func main() {
var buffer [256]byte
slice := buffer[10:20]
for i := 0; i < len(slice); i++ {
slice[i] = byte(i)
}
fmt.Println("before", slice)
AddOneToEachElement(slice)
fmt.Println("after", slice)
}

即使 slice header 通过值传递, header 包含指向数组元素的指针,因此原始 header 和传递给该函数的标题的副本都描述相同的数组。 因此,当函数返回时,可以通过原始 slice 变量看到被修改的元素。

slice header 真的只是传递了副本,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
func SubtractOneFromLength(slice []byte) []byte {
slice = slice[0 : len(slice)-1]
return slice
}

func main() {
var buffer [256]byte
slice := buffer[10:20]
fmt.Println("Before: len(slice) =", len(slice))
newSlice := SubtractOneFromLength(slice)
fmt.Println("After: len(slice) =", len(slice))
fmt.Println("After: len(newSlice) =", len(newSlice))
}

这里看到一个slice参数的内容可以被一个函数修改,但是它的 header 不能。 存储在 slice 变量中的长度不会通过对函数的调用进行修改,因为函数传递了 slice header 的副本,而不是原始的变量。 因此,如果要编写一个修改 header 的函数,那么必须将其作为结果参数返回,就像上面所做的那样。 slice 变量不变,但返回的值具有新的长度,然后将其存储在 newSlice 中。

4 切片指针:Method receivers

另外一个通过函数调用修改 slice header 的方式时传递 header 的指针。下面上买示例的一个变种:

1
2
3
4
5
6
7
8
9
10
11
12
func PtrSubtractOneFromLength(slicePtr *[]byte) {
slice := *slicePtr
*slicePtr = slice[0 : len(slice)-1]
}

func main() {
var buffer [256]byte
slice := buffer[10:20]
fmt.Println("Before: len(slice) =", len(slice))
PtrSubtractOneFromLength(&slice)
fmt.Println("After: len(slice) =", len(slice))
}

这个例子看起来很笨拙,特别是处理了需要一个额外的间接引用,但这是对于指向slice的指针而言,常见的处理方式。 对于修改 slice 使用指针 receiver 是惯用的。

假设现在希望在一个切片上有一个方法,以最后的斜杠截断它。 可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type path []byte

func (p *path) TruncateAtFinalSlash() {
i := bytes.LastIndex(*p, []byte("/"))
if i >= 0 {
*p = (*p)[0:i]
}
}

func main() {
pathName := path("/usr/bin/tso") // Conversion from string to path.
pathName.TruncateAtFinalSlash()
fmt.Printf("%s\n", pathName)
}

如果运行此示例,将看到它正常工作,更新调用者中的切片。

另一方面,如果想写一个路径的方法,在路径中的ASCII字母(忽略非英文名称)转变为大写,该方法调用者可以是一个值, 因为值 receiver 仍将指向相同的底层阵列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type path []byte

func (p path) ToUpper() {
for i, b := range p {
if 'a' <= b && b <= 'z' {
p[i] = b + 'A' - 'a'
}
}
}

func main() {
pathName := path("/usr/bin/tso")
pathName.ToUpper()
fmt.Printf("%s\n", pathName)
}

这里, ToUpper 方法在for范围构造中使用两个变量来捕获索引和切片元素。 这种形式的循环避免了在身体中多次使用 p[i] 。

5 容量

下面函数扩展整数切片:

1
2
3
4
5
6
func Extend(slice []int, element int) []int {
n := len(slice)
slice = slice[0 : n+1]
slice[n] = element
return slice
}

调用如下:

1
2
3
4
5
6
7
8
func main() {
var iBuffer [10]int
slice := iBuffer[0:0]
for i := 0; i < 20; i++ {
slice = Extend(slice, i)
fmt.Println(slice)
}
}

观察切片怎么增长直到…

现在是时候谈谈 slice header 的第三个字段:它的容量。 除了数组指针和长度之外,slice头还存储其容量:

1
2
3
4
5
type sliceHeader struct {
Length int
Capacity int
ZerothElement *byte
}

容量字段记录底层数组实际有多少空间; 它是长度可以达到的最大值。 试图将切片超越其容量,将超出数组的极限,并将触发 panic 。

上面示例中 slice 初始语句如下:

slice := iBuffer[0:0]

它的 header 如下所示:

1
2
3
4
5
slice := sliceHeader{
Length: 0,
Capacity: 10,
ZerothElement: &iBuffer[0],
}

容量字段等于底层数组的长度减去切片的第一个元素在数组中的索引值。如果需要查询切片的容量,请使用内置函数 cap :

1
2
3
if cap(slice) == len(slice) {
fmt.Println("slice is full!")
}

6 Make

如果想要超越其容量,那怎么办? 不能这么做! 根据定义,容量是增长的极限。 但是可以通过分配一个新数组,复制数据和修改切片来指向新数组来实现相同的结果。

可以使用内置函数来重新分配一个更大的数组,然后分割它,但是更简单的是使用 make 内置函数。 它分配一个新数组,并创建一个 slice header 来描述它。 make 函数有三个参数:slice的类型,它的初始长度和它的容量,容量是分配保存slice数据的数组的长度。 这个调用创建一个长度为10的片段,还有5个(15-10)的空间,你可以通过运行它看到:

1
2
slice := make([]int, 10, 15)
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))

下面代码将 int slice 的容量加倍,但是它的长度保持不变:

1
2
3
4
5
6
7
8
slice := make([]int, 10, 15)
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))
newSlice := make([]int, len(slice), 2*cap(slice))
for i := range slice {
newSlice[i] = slice[i]
}
slice = newSlice
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))

运行此代码之后,在另一个重新分配之前,切片有了更多的空间。

当创建切片时,长度和容量通常是一样的。 该内置函数有一个这种常见情况的缩写。 length参数默认为容量,此您可以将其设置为相同的值。

gophers := make([]Gopher, 10)

gophers slice 有相同的长度和容量,都为10。

7 复制

上面的示例中,当切片的容量加倍时,需要写了一个循环来将旧数据复制到新的切片。 Go有一个内置的函数 copy ,使这更容易。 它的参数是两个切片,它将数据从右边的参数复制到左边的参数。 示例重写为使用 copy :

1
2
newSlice := make([]int, len(slice), 2*cap(slice))
copy(newSlice, slice)

copy 函数很智能。 它只复制它可以复制,会关注两个 slice 参数的长度。 换句话说,它复制的元素的数量是两个切片中长度的最小值。 此外,copy返回一个整数值,复制的元素数量,虽然并不总是需要检查。

当源和目的地有重叠时,/copy/ 函数也可以正确的,这意味着它可以用于在单个切片中移动项目。 以下是使用 copy 将值插入片段中间的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Insert inserts the value into the slice at the specified index,
// which must be in range.
// The slice must have room for the new element.
func Insert(slice []int, index, value int) []int {
// Grow the slice by one element.
slice = slice[0 : len(slice)+1]
// Use copy to move the upper part of the slice out of the way and open a hole.
copy(slice[index+1:], slice[index:]) // slice[index:] 等价于 slice[index:len(slice)]
// Store the new value.
slice[index] = value
// Return the result.
return slice
}

上面函数有几件事要注意。 首先,当然,它必须返回更新的切片,因为它的长度已经改变。 其次,它使用方便的速记。表达式:

slice[i:]

等价于:

slice[i:len(slice)]

此外,虽然还没有使用技巧,但也可以省略一个切片表达式的第一个元素; 它默认为零。因此

slice[:]

仅仅代表 slice 自身, 这在切片化数组时很有用。下面表达式切片整个数组:

array[:]

调用 Insert 函数如下:

1
2
3
4
5
6
7
8
9
func main() {
slice := make([]int, 10, 20) // Note capacity > length: room to add element.
for i := range slice {
slice[i] = i
}
fmt.Println(slice)
slice = Insert(slice, 5, 99)
fmt.Println(slice)
}

8 Append: 示例

往前回顾下,写的一个 Extend 函数,它将一个切片扩展。 然而,它是错误的,因为如果切片的容量太小,该函数将崩溃。 ( Insert 示例有相同的问题。) 现在已经准备好了修复这些的知识,所以现在重新编写一个强大的 Extend 实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
func Extend(slice []int, element int) []int {
n := len(slice)
if n == cap(slice) {
// Slice is full; must grow.
// We double its size and add 1, so if the size is zero we still grow.
newSlice := make([]int, len(slice), 2*len(slice)+1)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0 : n+1]
slice[n] = element
return slice
}

在上面情况下,函数返回切片尤为重要,因为当它重新分配生成的切片时,会指向一个完全不同的数组。 下面这段代码,用于说明切片填满时会发生什么:

1
2
3
4
5
6
7
8
func main() {
slice := make([]int, 0, 5)
for i := 0; i < 10; i++ {
slice = Extend(slice, i)
fmt.Printf("len=%d cap=%d slice=%v\n", len(slice), cap(slice), slice)
fmt.Println("address of 0th element:", &slice[0])
}
}

注意当初始数组5被填满时的重新分配。 当分配新数组时,第零个元素的地址和容量都会发生变化。

以强大的Extend功能为指导,可以编写一个更好的功能,能支持多个元素来扩展切片。 为此,当调用函数时,使用Go的原生特性将函数的参数列表转换为切片。 也就是说,使用Go的可变参数列表。

函数命名为 Append 。 对于第一个版本,重复调用Extend,以使可变函数参数的机制清晰明了。 Append的签名:

func Append(slice []int, items ...int) []int

Append 需要一个参数,一个 slice ,后跟零个或多个 int 参数。 就 Append 的实现而言,这些参数正好是int的切片,可以看到:

1
2
3
4
5
6
7
8
// Append appends the items to the slice.
// First version: just loop calling Extend.
func Append(slice []int, items ...int) []int {
for _, item := range items {
slice = Extend(slice, item)
}
return slice
}

注意范围循环遍历items参数的元素,暗含了类型[]int。 还要注意使用空白标识符来舍弃循环中的索引,在这种情况下不需要这个索引。

Append 使用如下:

1
2
3
4
5
6
func main() {
slice := []int{0, 1, 2, 3, 4}
fmt.Println(slice)
slice = Append(slice, 5, 6, 7, 8)
fmt.Println(slice)
}

不仅可以 append 元素,还可以 append 另外一个切片,如下所示:

1
2
3
4
5
slice1 := []int{0, 1, 2, 3, 4}
slice2 := []int{55, 66, 77}
fmt.Println(slice1)
slice1 = Append(slice1, slice2...) // The '...' is essential!
fmt.Println(slice1)

当然, Append 函数优化实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Append appends the elements to the slice.
// Efficient version.
func Append(slice []int, elements ...int) []int {
n := len(slice)
total := len(slice) + len(elements)
if total > cap(slice) {
// Reallocate. Grow to 1.5 times the new size, so we can still grow.
newSize := total*3/2 + 1
newSlice := make([]int, total, newSize)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[:total]
copy(slice[n:], elements)
return slice
}

注意使用 copy 两次,一次将切片数据移动到新分配的内存,然后将附加的切片数据复制到旧数据的末尾。

行为和上面快速实现一样:

1
2
3
4
5
slice1 := []int{0, 1, 2, 3, 4}
slice2 := []int{55, 66, 77}
fmt.Println(slice1)
slice1 = Append(slice1, slice2...) // The '...' is essential!
fmt.Println(slice1)

9 Append:内建函数

目前我们 get 到了 append 内置函数设计的动机了。 它完全和上面定义的/Append/ 示例的函数有同样的功能,同等效率,但它适用于任何切片类型。

Go的一个弱点是任何通用类型的操作都必须由运行时提供。 有一天可能会改变,但是现在,为了使切片更容易使用,Go提供了一个内置的通用 append 函数。 它与上面的int切片版本相同,但适用于任何切片类型。

记住,由于 slice header 总是通过调用 append 来更新,所以需要在调用后保存返回的切片。 实际上,编译器不会让你调用 append 而不保存结果。

使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Create a couple of starter slices.
slice := []int{1, 2, 3}
slice2 := []int{55, 66, 77}
fmt.Println("Start slice: ", slice)
fmt.Println("Start slice2:", slice2)

// Add an item to a slice.
slice = append(slice, 4)
fmt.Println("Add one item:", slice)

// Add one slice to another.
slice = append(slice, slice2...)
fmt.Println("Add one slice:", slice)

// Make a copy of a slice (of int).
slice3 := append([]int(nil), slice...)
fmt.Println("Copy a slice:", slice3)

// Copy a slice to the end of itself.
fmt.Println("Before append to self:", slice)
slice = append(slice, slice...)
fmt.Println("After append to self:", slice)

请仔细考虑该例子的最后一行内容,以了解切片的设计如何使这个简单的调用成功。

10 Nil

另外,通过新学习到的知识,可以看到一个零切片的表示形式。 当然,它是 slice header 的零值:

1
2
3
4
5
sliceHeader{
Length: 0,
Capacity: 0,
ZerothElement: nil,
}

或者

sliceHeader{}

关键的细节之处就是元素指针为 nil 。 切片被创建

array[0:0]

长度为0,但是元素指针不为空,所以不是一个 nil 切片。

应该清楚的是,一个空切片可以增长(假设它具有非零容量),但是一个 nil 切片没有数组将数值填入,并且永远不会增长,甚至连一个元素都不能。

也就是说,/nil/ 切片在功能上等同于零长度切片,即使它没有指向。 它的长度为零,并可以附加到分配。 例如,上面示例中的一行,通过追加到一个 nil 切片来复制一个切片。

11 字符串

在切片的上下文中,这里简要介绍一下Go的字符串部分。

字符串实际上非常简单:它只是只读字节的切片,有一点源于语言的额外的语法支持。

因为它们是只读的,所以不需要容量(你也不能扩展它们),但是大多数情况下,可以像只读字节片一样对待它们。

对于初学者,可以对它们进行索引以访问单个字节

slash := "/usr/ken"[0] // yields the byte value '/'.

可以分割一个字符串来获取一个子字符串:

usr := "/usr/ken"[0:4] // yields the string "/usr"

现在应该清楚了,当裁剪一个字符串时,幕后发生了什么。

也可以用正常的字节切片,并通过简单的转换创建一个字符串:

str := string(slice)

反向转换也是可以的:

slice := []byte(usr)

字符串下面的数组从视图中隐藏; 除了通过字符串之外,没有办法访问其内容。 这意味着当我们进行这些转换之一时,必须复制数组。 在这些转换中的任何一个之后,对字节片下面的数组的修改不会影响相应的字符串。

字符串这种切片式设计的一个重要结果是创建子字符串非常高效。 所有需要发生的事情是创建一个两个字的字符串头。 由于字符串是只读的,原始字符串和由slice操作生成的字符串可以安全地共享相同的数组。

一个历史备注:字符串的最早实现始终被分配,但是当切片被添加到语言中时, 它们提供了一个有效的字符串处理模型。 一些基准测试结果都展示出巨大的加速。

当然,还有更多的字符串知识,一个 单独的博客文章 更深入地介绍了它们。

12 总结

要了解切片是如何工作的,它有助于了解它们是如何实现的。 有一些数据结构, slice header ,即与切片变量相关联的条目, 该 header 描述了单独分配的数组的一部分。 当我们传递切片值时, header 被复制,但它指向的数组始终是共享的。

一旦了解它们如何工作,切片就变得不仅易于使用,而且功能强大而富有表现力, 特别是在 copy 和 append 内置函数的帮助下。

13 延伸阅读

在Go里面有很多关于切片的使用场景。 如前所述 Slice Tricks" Wiki page页面有很多示例。 Go Slices博客文章使用清晰的图表描述了内存布局细节。 Russ Cox的Go Data Structures文章包括对片的讨论以及Go的其他内部数据结构。

有更多的材料可用,但是了解切片的最佳方式是使用它们。

Footnotes:

1

DEFINITION NOT FOUND.

2

DEFINITION NOT FOUND.

3

DEFINITION NOT FOUND.

4

DEFINITION NOT FOUND.

5

DEFINITION NOT FOUND.

Last Updated 2017-05-27 Sat 10:38.
Render by hexo-renderer-org with Emacs 25.3.2 (Org mode 8.2.10)

Go的HTTP tracing

发表于 2017-05-24 | 分类于 技术积累 |

在Go 1.7中,引入了 HTTP tracing ,这是在HTTP客户端请求的整个生命周期中收集细粒度信息的工具。 由 net/http/httptrace 包提供 HTTP tracing 的支持。 收集的信息可用于调试延迟问题,服务监控,编写自适应系统等。

1 HTTP事件

httptrace包提供了许多钩子,用于在HTTP往返期间收集各种事件的信息。 这些事件包括:

  • 连接创建
  • 连接复用
  • DNS 查询
  • 将请求写入网路
  • 读取响应

2 跟踪事件

可以通过将包含钩子函数的 *httptrace.ClientTrace 放在请求的 context.Context 中来启用 HTTP tracing 。 http.RoundTripper 通过查找 context 的 *httptrace.ClientTrace , 并调用相关的钩子函数报告内部事件。

追踪范围限于请求的 context ,用户应在 context 上下文之前放置一个 *httptrace.ClientTrace , 然后才能启动请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
req, _ := http.NewRequest("GET", "https://google.com", nil)
trace := &httptrace.ClientTrace{
GotConn: func(connInfo httptrace.GotConnInfo) {
fmt.Printf("Got Conn: %+v\n", connInfo)
},
DNSDone: func(dnsInfo httptrace.DNSDoneInfo) {
fmt.Printf("DNS Info: %+v\n", dnsInfo)
},
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
_, err := http.DefaultTransport.RoundTrip(req)
if err != nil {
log.Fatal(err)
}
}
DNS Info: {Addrs:[{IP:192.168.83.230 Zone:}] Err:<nil> Coalesced:false}
Got Conn: {Conn:0xc42001ce00 Reused:false WasIdle:false IdleTime:0s}

在 round trip 中,/http.DefaultTransport/ 会在事件发生时调用每个钩子。 一旦DNS查找完成,将打印DNS信息。 当与请求的主机建立连接时,它将类似地打印连接信息。

3 跟踪http.Client

跟踪机制旨在跟踪单个http.Transport.RoundTrip的生命周期中的事件。 但是,客户端可以进行多次往返,以完成HTTP请求。 例如,在URL重定向的情况下,注册的钩子将被调用多次,客户端遵循HTTP重定向,进行多个请求。 用户有职责在 http.Client 级别识别这些事件。 下面的示例使用 http.RoundTripper wrapper 来标识当前的请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"fmt"
"log"
"net/http"
"net/http/httptrace"
)

// transport is an http.RoundTripper that keeps track of the in-flight
// request and implements hooks to report HTTP tracing events.
type transport struct {
current *http.Request
}

// RoundTrip wraps http.DefaultTransport.RoundTrip to keep track
// of the current request.
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
t.current = req
return http.DefaultTransport.RoundTrip(req)
}

// GotConn prints whether the connection has been used previously
// for the current request.
func (t *transport) GotConn(info httptrace.GotConnInfo) {
fmt.Printf("Connection reused for %v? %v\n", t.current.URL, info.Reused)
}

func main() {
t := &transport{}

req, _ := http.NewRequest("GET", "https://google.com", nil)
trace := &httptrace.ClientTrace{
GotConn: t.GotConn,
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))

client := &http.Client{Transport: t}
if _, err := client.Do(req); err != nil {
log.Fatal(err)
}
}

上面示例从 google.com 重定向到 www.google.com, 输出如下:

Connection reused for https://google.com? false
Connection reused for https://www.google.com.hk/?gfe_rd=cr&ei=olwkWd3BAa-M8Qfjs73IBA? false

net/http包中的 Transport 支持跟踪 HTTP/1 和 HTTP/2 的 request。

如果你是自定义 http.RoundTripper 实现的作者,则可以通过检查 *httptest.ClientTrace 的请求 context 来支持跟踪,并在事件发生时调用相关的钩子。

4 总结

对于那些有兴趣调试HTTP请求延迟和编写工具来进行出站流量的网络调试的人来说, HTTP tracing 是一个有价值的补充。 通过启用这个新工具,希望看到来自社区的HTTP调试,基准测试和可视化工具,如httpstat。

Last Updated 2017-05-24 Wed 00:02.
Render by hexo-renderer-org with Emacs 25.3.2 (Org mode 8.2.10)

Go的竞态探测器

发表于 2017-05-23 | 分类于 技术积累 |

Race conditions 是最隐晦和难以捉摸的编程错误之一。 通常,在代码部署到生产之后很长时间才会发作,而且通常会导致很神秘的故障。 Go的并发机制使得编写干净并发代码变得容易,但它们并不能防止 /Race conditions/。 需要谨慎,勤勉和测试。 工具很有帮助。

Go 1.1引入竞态探测器,一个用于在Go代码中查找 Race conditions 的新工具。 它基于 C/C++ ThreadSanitizer运行库 ,此库被用于检测Google内部代码库和Chromium中的许多错误。 该技术于2012年9月与Go集成; 此后,它已经应用到了标准库中。 它现在已经成为持续建设过程的一部分,在这些过程中,它们会随着时间的推移而捕捉到产生的 Race conditions 。

1 工作原理

竞态探测器集成在go工具链中。 当设置了-race命令行标志时,编译器将使用访问内存的时间和方式的代码记录下来,用于设置所有内存访问, 而运行时库会监视对共享变量的不同步访问。 当检测到这种“racy”行为时,会打印一个警告。

由于其设计,竞态探测器只能在运行代码实际触发时才能检测到竞争条件,这意味着需要在真实的工作负载下运行启用探测器。 然而,启用竞态探测的可执行文件可能使用十倍的CPU和内存,因此始终启用探测器是不切实际的。 出于这个困境的一个办法是在启用竞态探测的情况下运行一些测试。 负载测试和集成测试是很好的候选者,因为它们往往会执行代码的并发部分。 另外的可选途径:生产工作负载环境中, 在运行的服务器池中, 部署单个启用竞态探测的实例。

2 使用

竞态探测器与Go工具链完全集成。 要启用竞态检测器的情况下,构建代码,只需将 -race 标志添加到命令行:

1
2
3
4
go test -race mypkg    // test the package
go run -race mysrc.go // compile and run the program
go build -race mycmd // build the command
go install -race mypkg // install the package

3 示例

3.1 Timer.Reset

当前例子是竞态探测器发现的实际bug的简化版本。 在使用定时器, 0到1秒的随机时间间隔之后打印消息。 打印过程反复进行了五秒钟。 使用 time.AfterFunc 为第一条消息创建一个 Timer ,然后使用 Reset 方法调度下一条消息,每次都复用原有 Timer 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
start := time.Now()
var t *time.Timer
t = time.AfterFunc(randomDuration(), func() {
fmt.Println(time.Now().Sub(start))
t.Reset(randomDuration())
})
time.Sleep(5 * time.Second)
}

func randomDuration() time.Duration {
return time.Duration(rand.Int63n(1e9))
}

这似乎是合理的代码,但在某些情况下,它以令人惊讶的方式失败:

panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xb code=0x1 addr=0x8 pc=0x41e38a]

goroutine 4 [running]:
time.stopTimer(0x8, 0x12fe6b35d9472d96)
    src/pkg/runtime/ztime_linux_amd64.c:35 +0x25
time.(*Timer).Reset(0x0, 0x4e5904f, 0x1)
    src/pkg/time/sleep.go:81 +0x42
main.func·001()
    race.go:14 +0xe3
created by time.goFunc
    src/pkg/time/sleep.go:122 +0x48

发生了什么? 启用竞态探测器的然后在运行一次:

==================
WARNING: DATA RACE
Read at 0x00c420084018 by goroutine 7:
  main.main.func1()
      /tmp/babel-27165ee_/go-src-27165GUv.go:17 +0x17c

Previous write at 0x00c420084018 by main goroutine:
  main.main()
      /tmp/babel-27165ee_/go-src-27165GUv.go:18 +0x17a

Goroutine 7 (running) created at:
  time.goFunc()
      /home/parallels/.gvm/gos/go1.8/src/time/sleep.go:170 +0x51
==================
Found 1 data race(s)
exit status 66

竞态探测器展示出问题根源:来自不同 goroutines 对变量 t 有不同步读和写。 如果初始定时器时间间隔非常小,则定时器函数可能会在主 goroutine 赋值到 t 之前触发,因此对 t.Reset 的调用发生在 nil 上。

修复这个 race condition 问题,可通过读写发生在一个 goroutine 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
start := time.Now()
reset := make(chan bool)
var t *time.Timer
t = time.AfterFunc(randomDuration(), func() {
fmt.Println(time.Now().Sub(start))
reset <- true
})
for time.Since(start) < 5*time.Second {
<-reset
t.Reset(randomDuration())
}
}

func randomDuration() time.Duration {
return time.Duration(rand.Int63n(1e9))
}

主 goroutine 完全负责设置和重置定时器 t ,通过一个新的重置 channel 传达重置定时器的信号,然后以线程安全的方式重置定时器。

最简单但不相对不那么高效的方式是避免复用timer。

3.2 ioutil.Discard

ioutil包的 Discard 实现了接口 io.Writer , 但是忽略了所有写给它的数据。 可认为如 dev/null 一般:发送你需要读取而不需要存储的数据的一个地方。 它通常与 /io.Copy 一起使用,清空reader,如下所示:

io.Copy(ioutil.Discard, reader)

回到2011年7月,Go团队注意到,以这种方式使用Discard效率不高:Copy功能在每次调用时内部都会分配一个 32kB 的缓冲区, 但是当与 Discard 一起使用时,缓冲区完全没必要,因为只是丢弃读取到的数据。 他们认为这种惯用的复制和丢弃不应该那么昂贵。修复此问题的方式,就是给 Writer 实现方法 ReadFrom,如下所示:

writer.ReadFrom(reader)

Go团队向 Discard 的底层类型添加了一个ReadFrom方法,该类型具有内部缓冲区,该缓冲区在其所有用户之间共享。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var blackHole [4096]byte // shared buffer

func (devNull) ReadFrom(r io.Reader) (n int64, err error) {
readSize := 0
for {
readSize, err = r.Read(blackHole[:])
n += int64(readSize)
if err != nil {
if err == io.EOF {
return n, nil
}
return
}
}
}

这次修复依旧没能解决问题,因为用户自定义的 /Reader/,可能在读的过程中,执行写操作,这个时候共享的缓冲区就造成数据污染。

1
2
3
4
5
6
7
8
9
10
11
12
13
type trackDigestReader struct {
r io.Reader
h hash.Hash
}

func (t trackDigestReader) Read(p []byte) (n int, err error) {
n, err = t.r.Read(p) // 这里的p就是代表的就是balckHode
t.h.Write(p[:n])
return
}

tdr := trackDigestReader{r: file, h: sha1.New()}
io.Copy(ioutil.Discard, tdr)

最终还是通过为每次使用的 ioutil.Discard 添加唯一的缓冲区,来消除共享缓冲区的 Race condition 。

4 总结

竞态探测器是检查并发程序正确性的强大工具。 它不会呈现虚假问题,所以请认真地对待。

还在等什么?现在就对你的代码运行“go test -race”吧!

Last Updated 2017-05-23 Tue 22:17.
Render by hexo-renderer-org with Emacs 25.3.2 (Org mode 8.2.10)

Golang的并发模式:Context

发表于 2017-05-19 | 分类于 技术积累 |

在go服务端,每个传入的 request 都在自己的 goroutine 中做后续处理。 request handlers 经常启动其他 goroutines 以访问后端,如数据库和rpc服务。 服务于 request 的一组常用典型的 goroutines 访问特定的请求值,例如最终用户的身份,授权令牌和请求的截止日期。 当 request 被取消或触发超时时,在该 request 上工作的所有 goroutine 应该快速退出,以便系统可以回收所使用的任何资源。

在google内部,开发了一个 context 包,可以轻松地跨越api边界,传递请求范围值,取消信号和截止日期到 request 所涉及的所有 goroutine 。 该包是开源的被称作 context。 本文介绍了如何使用该包并提供了一个完整的工作示例。

1 context

context 包的核心就是 context 类型(这里的描述是精简的,详情可见godoc):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// a context carries a deadline, cancelation signal, and request-scoped values
// across api boundaries. its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
// done returns a channel that is closed when this context is canceled
// or times out.
Done() <-chan struct{}

// err indicates why this context was canceled, after the done channel
// is closed.
Err() error

// deadline returns the time when this context will be canceled, if any.
Deadline() (deadline time.time, ok bool)

// value returns the value associated with key or nil if none.
Value(key interface{}) interface{}
}

Done 方法返回一个 channel ,用于发送取消信号(代表 Context 已关闭)到运行时函数:当 channel 关闭时,函数应该放弃后续流程并返回。 Err 方法返回一个错误,指出为什么 context 被取消。 管道和取消文章更详细地讨论了 done channel 的惯用法。

由于 Done channel 只接收的原因,/Context/ 没有取消方法:接收取消信号的函数通常不应当具备发送信号的功能。 特别是,当父操作启动子操作的 goroutines 时,这些子操作不应该能够取消父操作。 相反, WithCancel 函数(如下所述)提供了一种取消新的 Context 值的方法。

Context 可以安全地同时用于多个 goroutines 。 代码可以将单个 Context 传递给任意数量的 goroutine ,并能发送取消该Context的信号到所有的关联的 goroutine 。

Deadline 方法允许功能确定是否应该开始工作; 如果剩下的时间太少,可能不值得。 代码中也可能会使用截止时间来为I/O操作设置超时。

Value 允许 Context 传送请求数据。 该数据必须能安全的同时用于多个 goroutine 。

2 Context的衍生

context/包提供了从现有 /Context 衍生出新的 Context 的函数。 这些 Context 形成一个树状的层级结构:当一个 Context 被取消时,从它衍生出的所有 Context 也被取消。

Background 是任何Context树的根; 它永远不会被取消:

1
2
3
4
// Background returns an empty Context. It is never canceled, has no deadline,
// and has no values. Background is typically used in main, init, and tests,
// and as the top-level Context for incoming requests.
func Background() Context

WithCancel 和 WithTimeout 返回衍生出的 Context ,衍生出的子 Context 可早于父 Context 被取消。 与传入的 request 相关联的上下文通常在请求处理程序返回时被取消。 WithCancel 也可用于在使用多个副本时取消冗余请求。 WithTimeout 对设置后台服务器请求的最后期限很有用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 // WithCancel returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed or cancel is called.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// A CancelFunc cancels a Context.
type CancelFunc func()

// WithTimeout returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed, cancel is called, or timeout elapses. The new
// Context's Deadline is the sooner of now+timeout and the parent's deadline, if
// any. If the timer is still running, the cancel function releases its
// resources.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)


// WithDeadline returns a copy of the parent context with the deadline adjusted
// to be no later than d. If the parent's deadline is already earlier than d,
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
// context's Done channel is closed when the deadline expires, when the returned
// cancel function is called, or when the parent context's Done channel is
// closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

WithValue 提供了一种将请求范围内的值与 Context 相关联的方法:

1
2
// WithValue returns a copy of parent whose Value method returns val for key.
func WithValue(parent Context, key interface{}, val interface{}) Context

注: 使用context的Value相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数;

掌握如何使用 context 包的最佳方法是通过一个真实完整的示例。

3 Context使用的简单示例

简单的示例,更容易理解 Context 各衍生函数适用的场景,而且编辑本文档使用的是 Org-mode, 在编辑的过程中,即可执行(对org-mode感兴趣的人,可在评论里联系我)。 这里的代码,来源于 context 的godoc。

3.1 WithCancel

WithCancel 的示例, 演示如何使用可取消 context 来防止 goroutine 泄漏。 示例函数的结尾,由gen启动的goroutine将返回而不会发送泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
"context"
"fmt"
)

func main() {
// gen generates integers in a separate goroutine and
// sends them to the returned channel.
// The callers of gen need to cancel the context once
// they are done consuming generated integers not to leak
// the internal goroutine started by gen.
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
return // returning not to leak the goroutine
case dst <- n:
n++
}
}
}()
return dst
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers

for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
}

3.2 WithDeadline

WithDeadline 的示例,通过一个截止日期的 Context 来告知一个阻塞的函数,一旦它到了最终期限,就放弃它的工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"context"
"fmt"
"time"
)

func main() {
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)

// Even though ctx will be expired, it is good practice to call its
// cancelation function in any case. Failure to do so may keep the
// context and its parent alive longer than necessary.
defer cancel()

select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}

}

3.3 Withtimeount

WithTimeount 的示例, 传递具有超时的 Context 以告知阻塞函数,它将在超时过后丢弃其工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"context"
"fmt"
"time"
)

func main() {
// Pass a context with a timeout to tell a blocking function that it
// should abandon its work after the timeout elapses.
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()

select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}

}

3.4 WithValue

WithValue 的简单示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"context"
"fmt"
)

func main() {
type favContextKey string

f := func(ctx context.Context, k favContextKey) {
if v := ctx.Value(k); v != nil {
fmt.Println("found value:", v)
return
}
fmt.Println("key not found:", k)
}

k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")

f(ctx, k)
f(ctx, favContextKey("color"))

}

4 示例:Google Web Search

示例是一个HTTP服务器,通过将查询“golang”转发到 Google Web Search API 并渲染查询结果, 来处理 "/search?q=golang&timeout=1s" 之类的URL。 timeout参数告诉服务器在该时间过去之后取消请求。

示例代码被拆分为三个包:

  • server 提供了 main 函数和 "/search" 的处理函数。
  • userip 提供了从 request 提取用户ip地址和关联一个 Context 的函数。
  • google 提供了把搜索字段发送的 Google 的 Search 函数。

4.1 server

服务器通过为 golang 提供前几个 Google 搜索结果来处理像 "search?q=golang" 之类的请求。 它注册 /handleSearch 来处理 "search"。 处理函数创建一个名为ctx的 /Context ,并在处理程序返回时,一并被取消。 如果 request 包含超时URL参数,则超时时会自动取消上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func handleSearch(w http.ResponseWriter, req *http.Request) {
// ctx is the Context for this handler. Calling cancel closes the
// ctx.Done channel, which is the cancellation signal for requests
// started by this handler.
var (
ctx context.Context
cancel context.CancelFunc
)
timeout, err := time.ParseDuration(req.FormValue("timeout"))
if err == nil {
// The request has a timeout, so create a context that is
// canceled automatically when the timeout expires.
ctx, cancel = context.WithTimeout(context.Background(), timeout)
} else {
ctx, cancel = context.WithCancel(context.Background())
}
defer cancel() // Cancel ctx as soon as handleSearch returns.






}

处理程序从 request 中提取查询关键字,并通过调用 userip 包来提取客户端的IP地址。 后端请求需要客户端的IP地址,因此handleSearch将其附加到ctx:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Check the search query.
query := req.FormValue("q")
if query == "" {
http.Error(w, "no query", http.StatusBadRequest)
return
}

// Store the user IP in ctx for use by code in other packages.
userIP, err := userip.FromRequest(req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ctx = userip.NewContext(ctx, userIP)

处理程序使用ctx和查询关键字调用 google.Search :

1
2
3
4
5
6
7
8
// Run the Google search and print the results.
start := time.Now()
results, err := google.Search(ctx, query)
elapsed := time.Since(start)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

如果搜索成功,处理程序将渲染返回结果:

1
2
3
4
5
6
7
8
9
10
11
if err := resultsTemplate.Execute(w, struct {
Results google.Results
Timeout, Elapsed time.Duration
}{
Results: results,
Timeout: timeout,
Elapsed: elapsed,
}); err != nil {
log.Print(err)
return
}

4.2 userip

userip包提供从请求中提取用户IP地址并将其与 Context 相关联的函数。 Context 提供了 key-value 映射的 map ,其中 key 和 value 均为 interface{} 类型。 key 类型必须支持相等性, value 必须是多个 goroutine 安全的。 userip 这样的包会隐藏 map 的细节,并提供强类型访问特定的 Context 值。

为了避免关键字冲突, userip 定义了一个不导出的类型 key ,并使用此类型的值作为 Context 的关键字:

1
2
3
4
5
6
7
8
// The key type is unexported to prevent collisions with context keys defined in
// other packages.
type key int

// userIPkey is the context key for the user IP address. Its value of zero is
// arbitrary. If this package defined other context keys, they would have
// different integer values.
const userIPKey key = 0

FromRequest 从 http.Request 中提取一个 userIP 值:

1
2
3
4
5
6
7
8
9
10
11
12
func FromRequest(req *http.Request) (net.IP, error) {
ip, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
}

userIP := net.ParseIP(ip)
if userIP == nil {
return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
}
return userIP, nil
}

NewContext返回一个带有userIP的新Context:

1
2
3
func NewContext(ctx context.Context, userIP net.IP) context.Context {
return context.WithValue(ctx, userIPKey, userIP)
}

FromContext 从 Context 中提取 userIP :

1
2
3
4
5
6
func FromContext(ctx context.Context) (net.IP, bool) {
// ctx.Value returns nil if ctx has no value for the key;
// the net.IP type assertion returns ok=false for nil.
userIP, ok := ctx.Value(userIPKey).(net.IP)
return userIP, ok
}

4.3 google

google.Search 函数向 Google Web Search API 发出HTTP请求,并解析JSON编码结果。 它接受Context参数ctx,并且在ctx.Done关闭时立即返回。

Google Web Search API请求包括搜索查询和用户IP作为查询参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func Search(ctx context.Context, query string) (Results, error) {
// Prepare the Google Search API request.
req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil)
if err != nil {
return nil, err
}
q := req.URL.Query()
q.Set("q", query)

// If ctx is carrying the user IP address, forward it to the server.
// Google APIs use the user IP to distinguish server-initiated requests
// from end-user requests.
if userIP, ok := userip.FromContext(ctx); ok {
q.Set("userip", userIP.String())
}
req.URL.RawQuery = q.Encode()

// Issue the HTTP request and handle the response.

}

Search 使用一个辅助函数 httpDo 来发出HTTP请求, 如果在处理请求或响应时关闭 ctx.Done ,取消 httpDo 。 Search 将传递闭包给 httpDo 来处理HTTP响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var results Results
err = httpDo(ctx, req, func(resp *http.Response, err error) error {
if err != nil {
return err
}
defer resp.Body.Close()

// Parse the JSON search result.
// https://developers.google.com/web-search/docs/#fonje
var data struct {
ResponseData struct {
Results []struct {
TitleNoFormatting string
URL string
}
}
}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return err
}
for _, res := range data.ResponseData.Results {
results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL})
}
return nil
})
// httpDo waits for the closure we provided to return, so it's safe to
// read results here.
return results, err

httpDo 函数发起HTTP请求,并在新的 goroutine 中处理其响应。 如果在 goroutine 退出之前关闭了ctx.Done,它将取消该请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
// Run the HTTP request in a goroutine and pass the response to f.
tr := &http.Transport{}
client := &http.Client{Transport: tr}
c := make(chan error, 1)
go func() { c <- f(client.Do(req)) }()
select {
case <-ctx.Done():
tr.CancelRequest(req)
<-c // Wait for f to return.
return ctx.Err()
case err := <-c:
return err
}
}

5 适配Context到已有代码

许多服务器框架提供用于承载请求范围值的包和类型。 可以定义 Context 接口的新实现,以便使得现有的框架和期望Context参数的代码进行适配。

例如,Gorilla的 github.com/gorilla/context 包允许处理程序通过提供从HTTP请求到键值对的映射来将数据与传入的请求相关联。 在 gorilla.go 中,提供了一个 Context 实现,其 Value 方法返回与 Gorilla 包中的特定HTTP请求相关联的值。

其他软件包提供了类似于 Context 的取消支持。 例如,Tomb 提供了一种杀死方法,通过关闭死亡 channel 来发出取消信号。 Tomb还提供了等待 goroutine 退出的方法,类似于sync.WaitGroup。 在 tomb.go 中,提供一个 Context 实现,当其父 Context 被取消或提供的 Tomb 被杀死时,该 Context 被取消。

6 总结

在Google,我们要求Go程序员通过 Context 参数作为传入和传出请求之间的呼叫路径上每个函数的第一个参数。 这允许由许多不同团队开发的Go代码进行良好的互操作。 它提供对超时和取消的简单控制,并确保安全证书等关键值正确转移Go程序。

希望在 Context 上构建的服务器框架应该提供 Context 的实现,以便在它们的包之间和期望 Context 参数的包之间进行适配。 客户端库将接受来自调用代码的 Context 。 通过为请求范围的数据和取消建立通用接口, Context 使得开发人员更容易地共享用于创建可扩展服务的代码。

Last Updated 2017-05-23 Tue 15:30.
Render by hexo-renderer-org with Emacs 25.3.2 (Org mode 8.2.10)

Golang的并发模式:管道和撤销

发表于 2017-05-17 | 分类于 技术积累 |

Go的并发原语可轻松构建有效利用I/O和多CPU的流水线管道。本文介绍了这种管道的示例,突出描述失败时的细微之处,并介绍了优雅地处理故障的技术。

1 什么是管道?

Go没有对于管道的正式定义,它只是并发模式中的一种。 非正式的,管道就是通过 channel 连接的一系列片段,其中每个片段是一组功能相同的 goroutine 。 在每个片段, goroutine 完成如下功能:

  • 通过 inbound channel 从上流片段接受数据
  • 在数据上执行计算,通常产生新数据
  • 把新数据通过 outbound channel 传到下流片段

每个片段都有任意数量的 inbound 和 outbound channel , 当然第一个和最后一个排除在外,因为前者只有 outbound channel, 后者只有 inbound channel 。 第一片段有时称为 source 或 producer ; 最后阶段,/sink/ 或 consumer 。

从一个简单的示例管道开始,解释思路和技巧。 之后,呈现一个更加现实的例子。

2 平方数示例

当前示例,管道包含三个片段。

第一个片段 gen , 把整数数组转换成一个可取出整数的 channel 。 gen 函数启动 goroutine 发生数据到 channel ,发送完成后关闭 channel:

1
2
3
4
5
6
7
8
9
10
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}

第二个片段 sq ,从一个 channel 中接收整数,并返回一个可取出每个接收的整数的平方的 channel 。 在 inbound channel 关闭后,此片段已将所有值发送到下游,然后关闭 outbound channel :

1
2
3
4
5
6
7
8
9
10
  func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}

main 函数 设置流水线并运行最后一个片段:从第二片段接收值并打印,直到 channel 关闭:

1
2
3
4
5
6
7
8
9
 func main() {
// Set up the pipeline.
c := gen(2, 3)
out := sq(c)

// Consume the output.
fmt.Println(<-out) // 4
fmt.Println(<-out) // 9
}

由于 sq 的 inbound channel 和 outbound channel 具有相同的类型,因此可以迭代使用任意次数。 重写 main ,使其像其他片段,循环从 inbound channel 取出数据:

1
2
3
4
5
6
  func main() {
// Set up the pipeline and consume the output.
for n := range sq(sq(gen(2, 3))) {
fmt.Println(n) // 16 then 81
}
}

3 扇出,扇入

多个函数从相同的 channel 读取,直到该 channel 关闭; 这被称为扇出。 这提供了在一组 worker 之间分配工作以并行化CPU使用和 I/O 的方式。

一个函数可以从多个输入接收数据,并进行数据处理,直到所有的输入 channel 多路复合到单个 channel 上,当所有输入都关闭时,复合的 channel 关闭。 这被称为扇入。

改变的管道流程, 运行两个 sq 的实例,每个实例从相同的输入 channel 读取(扇出)。 引入新的函数 merge 以演示扇入:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
in := gen(2, 3)

// Distribute the sq work across two goroutines that both read from in.
c1 := sq(in)
c2 := sq(in)

// Consume the merged output from c1 and c2.
for n := range merge(c1, c2) {
fmt.Println(n) // 4 then 9, or 9 then 4
}
}

merge 函数将 channel 列表转换为单个通道,为每个 inbound channel 启动一个 goroutine , 来将 inbound channel 值复制到唯一 outbound channel 。 一旦所有的输出 goroutines 都已经启动后, 再启动一个 goroutine , 待所有的 channel 发送完成后来关闭 outbound channel 。

往一个 closed channel 发送数据将会 panic ,所以要确保所有的发送都是在 channel 关闭之前完成的。 sync.WaitGroup 提供了一种简单的同步方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func merge(cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)

// Start an output goroutine for each input channel in cs. output
// copies values from c to out until c is closed, then calls wg.Done.
output := func(c <-chan int) {
for n := range c {
out <- n
}
wg.Done()
}
wg.Add(len(cs))
for _, c := range cs {
go output(c)
}

// Start a goroutine to close out once all the output goroutines are
// done. This must start after the wg.Add call.
go func() {
wg.Wait()
close(out)
}()
return out
}

4 稍作停顿

管道函数有如下模式:

  • 当所有发送操作完成时,片段关闭其 outbound channel 。
  • 片段不断接收来自 inbound channel 的值,直到这些 channel 关闭。

该模式允许每个接收片段使用 range loop , 以确保所有值都已成功发送到下游后退出所有 goroutine 。

但是在实际管道应用中,片段并不总是能够收到所有 inbound 值。 有时这是被设计:接收者可能只需要一个子集来进行后续处理。 更常见的情况是,由于 inbound 值显示在早期片段引入了错误,因此片段应该早早的退出。 在以上任一情况下,接收者不必等待剩余的值到达,并且我们希望较早的片段停止生成稍后片段不需要的值。

在上面的管道示例中,如果片段无法使用所有 inbound 值,则尝试发送这些值的 goroutines 将无限期地阻塞下去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
in := gen(2, 3)

// Distribute the sq work across two goroutines that both read from in.
c1 := sq(in)
c2 := sq(in)


// Consume the first value from output.
out := merge(c1, c2)
fmt.Println(<-out) // 4 or 9
return
// Since we didn't receive the second value from out,
// one of the output goroutines is hung attempting to send it.
}

这是一个资源泄漏: goroutines 消耗内存和运行时资源, goroutine 栈中的堆引用使数据不被垃圾回收。 goroutines 不被垃圾收集机制回收; 它们必须自己退出。

即使下游片段没能接收所有 inbound 值,管道的上游片段也可能需要提前退出。 执行此操作的一种方法是将 outbound channel 更改为具有缓冲区。 缓冲区可以保存固定数量的值; 如果缓冲区中有空间,则发送立即完成 :

1
2
3
4
c := make(chan int, 2) // buffer size 2
c <- 1 // succeeds immediately
c <- 2 // succeeds immediately
c <- 3 // blocks until another goroutine does <-c and receives 1

在创建 channel 时,若是知道将要发送的数据量,缓冲区可以简化代码。 例如,可重写 gen 拷贝整数到 channel 的缓冲区中, 避免 goroutine 创建:

1
2
3
4
5
6
7
8
func gen(nums ...int) <-chan int {
out := make(chan int, len(nums))
for _, n := range nums {
out <- n
}
close(out)
return out
}

回到管道中阻塞的 goroutine ,可以考虑为 merge 返回的 outbound channel 添加一个缓冲区:

1
2
3
4
5
func merge(cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int, 1) // enough space for the unread inputs
// ... the rest is unchanged ...
}

虽然上面的代码不再阻塞,但上面的代码依赖于当前是知道将要接收到多少数据的和要往下流发送多少数据。 这样的代码不健壮,如果 gen 接收的数据多于 1 , 或者下流只接收一部分值,那么将会永久的阻塞 ~goroutine~。 固定长度的缓存不可取,相应的在代码中需要为下流片段提供一种通用的方法,来通知上流片段它们将停止接收输入。

5 明确取消

当 main 函数决定不再从上游片段/outbound channel/ 接收数据时, 它需要告诉上流片段的 goroutine 终止发送操作。 下面的代码演示了如何通知, 通过往称为 done 的 channel 上发送值来实现。 发送两个值,因为有两个被阻止的发送者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  func main() {
runtime.GOMAXPROCS(runtime.NumCPU())

in := gen(2, 3)

// Distribute the sq work across two goroutines that both read from in.
c1 := sq(in)
c2 := sq(in)

// Consume the first value from output.
done := make(chan struct{}, 2)
out := merge(done, c1, c2)
fmt.Println(<-out) // 4 or 9

// Tell the remaining senders we're leaving.
done <- struct{}{}
done <- struct{}{}

fmt.Println(<-out) // 4 or 9

go func() {
time.Sleep(2*time.Second)
}()
return
}

发送 goroutines 使用 select 语句替换其发送操作,该语句在发送发生时或从 done 接收到值时继续进行。 done 值类型是空结构体,因为该值无关紧要:它只是接收事件,表示应该放弃后续发送。 输出 goroutine 在其 inbound channel 上继续循环,因此上游片段不被阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
func merge(done <-chan struct{}, cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)

// Start an output goroutine for each input channel in cs. output
// copies values from c to out until c is closed, then calls wg.Done.
output := func(name string, c <-chan int) {
defer func(){
fmt.Println("[goroutine ruturn][", name, "]")
wg.Done()
}()
for n := range c {
fmt.Println("[IN LOOP][BEFORE select][", name, "]")
select {
case out <- n:
fmt.Println("[IN LOOP][FROM c][", name, "]")
case <-done:
fmt.Println("[IN LOOP][FROM done][", name, "]")
return
}
}

fmt.Println("[AFTER LOOP][", name, "]")
}

wg.Add(len(cs))
for index, c := range cs {
name := fmt.Sprintf("goroutine %d", index)
go output(name, c)
}

// Start a goroutine to close out once all the output goroutines are
// done. This must start after the wg.Add call.
go func() {
wg.Wait()
fmt.Println("[AFTER WAIT]")
close(out)
}()
return out
}

上面的方法有一个问题:每个下游接收者都需要知道潜在阻塞的上游发送者的数量, 并安排在提前返回时向这些发送者发信号。 跟踪这些计数是乏味和容易出错的。

其实我们需要一种方法来告诉未知的无限数量的 goroutine 停止往下游发送它们的值。 在 Go 中,可以通过关闭 channel 来执行此操作, 因为关闭 channel 上的接收操作都是立刻完成的,产生相应数据类型的零值。

这意味着 main 函数可以通过关闭 done channel 来解除所有发件人的阻塞。 这个关闭操作实际上是发送者的广播信号。 重新编排管道函数, 添加 done channel 的延迟关闭函数 , 从而使 main 函数的所有返回路径都会发出信号,以使管道片段退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 func main() {
runtime.GOMAXPROCS(runtime.NumCPU())

// Set up a done channel that's shared by the whole pipeline,
// and close that channel when this pipeline exits, as a signal
// for all the goroutines we started to exit.
done := make(chan struct{})
defer close(done)

in := gen(done, 2, 3)

// Distribute the sq work across two goroutines that both read from in.
c1 := sq(done, in)
c2 := sq(done, in)

// Consume the first value from output.
out := merge(done, c1, c2)
fmt.Println(<-out) // 4 or 9

// done will be closed by the deferred call.
}

现在每个管道片段可以在 channel 关闭后轻松的返回, merge 中的 output routine 不用担心 inbound channel 的数据,因为 当 done channel 关闭时,上游发送者会停止数据的发送。 output 通过 defer 语句确保在所有返回路径上调用 wg.Done :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func merge(done <-chan struct{}, cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)

// Start an output goroutine for each input channel in cs. output
// copies values from c to out until c or done is closed, then calls
// wg.Done.
output := func(c <-chan int) {
defer wg.Done()
for n := range c {
select {
case out <- n:
case <-done:
return
}
}
}

wg.Add(len(cs))
for _, c := range cs {
go output(c)
}

// Start a goroutine to close out once all the output goroutines are
// done. This must start after the wg.Add call.
go func() {
wg.Wait()
close(out)
}()
return out

}

类型的, sq 可在 done 关闭后直接返回。 sq 通过 defer 语句确保在所有返回路径上关闭 out channel :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func sq(done <-chan struct{}, in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
select {
case out <- n * n:
case <-done:
return
}
}
}()
return out
}

gen 大体和 sq 类型, 在 done 返回, 通过 defer 语句确保 out channel 关闭:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func gen(done <-chan struct{}, nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
select {
case out <- n:
case <-done:
return
}
}
}()
return out
}

管道构建的指导方针:

  • 当所有发送操作完成时,片段关闭其 outbound channel 。
  • 片段持续从 inbound channel 中接收值,直到这些 channel 关闭或发件人被取消阻塞。

管道有两种方式能解除发送者的阻塞:

  • 确保所有发送的值都有足够的缓冲区, 有足够的缓冲区就不会阻塞了。
  • 当接收方放弃从 channel 接收数据时,显式地发送信号来解除发送者的阻塞

6 MD5摘要示例

现在来看看更真实的管道应用。

MD5 是一种消息摘要算法,可用作文件校验和。 命令行实用程序 md5sum 打印文件列表的摘要值。

% md5sum *.go
d47c2bbc28298ca9befdfbc5d3aa4e65  bounded.go
ee869afd31f83cbb2d10ee81b2b831dc  parallel.go
b88175e65fdcbc01ac08aaf1fd9b5e96  serial.go

示例程序像 md5sum ,但是以单个目录作为参数,并打印该目录下每个常规文件的摘要值,并按路径名排序。

% go run serial.go .
d47c2bbc28298ca9befdfbc5d3aa4e65  bounded.go
ee869afd31f83cbb2d10ee81b2b831dc  parallel.go
b88175e65fdcbc01ac08aaf1fd9b5e96  serial.go

示例程序的 main 函数 调用一个辅助函数 MD5All ,它返回一个从路径名到摘要值的 map ,然后排序并打印结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
// Calculate the MD5 sum of all files under the specified directory,
// then print the results sorted by path name.
m, err := MD5All(os.Args[1])
if err != nil {
fmt.Println(err)
return
}
var paths []string
for path := range m {
paths = append(paths, path)
}
sort.Strings(paths)
for _, path := range paths {
fmt.Printf("%x %s\n", m[path], path)
}
}

6.1 串行版

MD5All 函数是讨论的焦点。 在 serial.go 中的实现不使用并发性,只是在遍历文件树时读取和计算校验和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// MD5All reads all the files in the file tree rooted at root and returns a map
// from file path to the MD5 sum of the file's contents. If the directory walk
// fails or any read operation fails, MD5All returns an error.
func MD5All(root string) (map[string][md5.Size]byte, error) {
m := make(map[string][md5.Size]byte)
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return nil
}
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
m[path] = md5.Sum(data)
return nil
})
if err != nil {
return nil, err
}
return m, nil
}

6.2 并行版

在 parallel.go ,将 MD5All 函数分为两级管道。 第一级, sumFiles ,遍历树, 为每个文件做校验和创建 goroutine , 并将结果发送到 result 类型的 channel 上:

1
2
3
4
5
type result struct {
path string
sum [md5.Size]byte
err error
}

sumFiles 返回两个 channel :一个用于传递结果,另一个用于返回 filepath.Walk 返回的错误。 walk 函数启动一个新的 goroutine 来处理每个常规文件,然后检查 done 。 如果完成关闭, walk 将立即停止:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func sumFiles(done <-chan struct{}, root string) (<-chan result, <-chan error) {
// For each regular file, start a goroutine that sums the file and sends
// the result on c. Send the result of the walk on errc.
c := make(chan result)
errc := make(chan error, 1)
go func() {
var wg sync.WaitGroup
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return nil
}
wg.Add(1)
go func() {
data, err := ioutil.ReadFile(path)
select {
case c <- result{path, md5.Sum(data), err}:
case <-done:
}
wg.Done()
}()
// Abort the walk if done is closed.
select {
case <-done:
return errors.New("walk canceled")
default:
return nil
}
})
// Walk has returned, so all calls to wg.Add are done. Start a
// goroutine to close c once all the sends are done.
go func() {
wg.Wait()
close(c)
}()
// No select needed here, since errc is buffered.
errc <- err
}()
return c, errc
}

MD5All 从 channel 中接受摘要值,返回最早出现的错误,并通过 defer 来关闭 done :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func MD5All(root string) (map[string][md5.Size]byte, error) {
// MD5All closes the done channel when it returns; it may do so before
// receiving all the values from c and errc.
done := make(chan struct{})
defer close(done)

c, errc := sumFiles(done, root)

m := make(map[string][md5.Size]byte)
for r := range c {
if r.err != nil {
return nil, r.err
}
m[r.path] = r.sum
}
if err := <-errc; err != nil {
return nil, err
}
return m, nil
}

6.3 受限的并行

在 parallel.go 的 MD5All 实现中为每个文件创建一个 goroutine 。 试想一下,若是一个目录中有许多大文件,上面的实现,很可能导致资源枯竭。 可以通过限制同时打开的问题个数来解决资源占用的问题。在 bounded.go 中, 创建固定数量的用于读取文件的 goroutine 。 重新设计流程,包含三个片段: 遍历目录树, 读取文件生成摘要,收集摘要。

第一个片段 walkFiles , 过滤出常规文件路径,同时往下游发送:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func walkFiles(done <-chan struct{}, root string) (<-chan string, <-chan error) {
paths := make(chan string)
errc := make(chan error, 1)
go func() {
// Close the paths channel after Walk returns.
defer close(paths)
// No select needed for this send, since errc is buffered.
errc <- filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return nil
}
select {
case paths <- path:
case <-done:
return errors.New("walk canceled")
}
return nil
})
}()
return paths, errc
}

中间片段启动固定数量的 digester goroutine 从 paths channel 中接受文件名, 通过 c channel 来回写摘要值:

1
2
3
4
5
6
7
8
9
10
func digester(done <-chan struct{}, paths <-chan string, c chan<- result) {
for path := range paths {
data, err := ioutil.ReadFile(path)
select {
case c <- result{path, md5.Sum(data), err}:
case <-done:
return
}
}
}

与以前的示例不同,/digester/ 不会关闭其输出 channel ,因为多个 goroutine 正在共享 channel 上发送。 相反,当所有的 digester 完成时,MD5All中的代码会关闭 channel :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// MD5All reads all the files in the file tree rooted at root and returns a map
// from file path to the MD5 sum of the file's contents. If the directory walk
// fails or any read operation fails, MD5All returns an error. In that case,
// MD5All does not wait for inflight read operations to complete.
func MD5All(root string) (map[string][md5.Size]byte, error) {
// MD5All closes the done channel when it returns; it may do so before
// receiving all the values from c and errc.
done := make(chan struct{})
defer close(done)

paths, errc := walkFiles(done, root)

// Start a fixed number of goroutines to read and digest files.
c := make(chan result) // HLc
var wg sync.WaitGroup
const numDigesters = 20
wg.Add(numDigesters)
for i := 0; i < numDigesters; i++ {
go func() {
digester(done, paths, c) // HLc
wg.Done()
}()
}
go func() {
wg.Wait()
close(c) // HLc
}()
// End of pipeline. OMIT

m := make(map[string][md5.Size]byte)
for r := range c {
if r.err != nil {
return nil, r.err
}
m[r.path] = r.sum
}
// Check whether the Walk failed.
if err := <-errc; err != nil { // HLerrc
return nil, err
}
return m, nil
}

可以让每个 digester 创建并返回自己的输出 channel ,但是后面需要额外的 goroutine 来扇入结果。

最后片段接收 c 的所有结果,然后从 errc 检查错误。 此检查不能早于从 c 中接受数据,因为在此之前, walkFiles 可能会被下游阻塞。

7 总结

本文介绍了Go中构建管道流的技术。 处理这种管道中的故障是很棘手的,因为管道中的任一片段都可能被尝试发送下游值而阻塞,并且下游片段可能也不再关心或者需要输入数据。 本文展示了如何关闭 channel 方式来向管道启动的所有 goroutines 广播“完成”信号,并且正确地定义了管道构建的准则。

Last Updated 2017-05-23 Tue 13:28.
Render by hexo-renderer-org with Emacs 25.3.2 (Org mode 8.2.10)

一周回顾--2017-05-12

发表于 2017-05-14 | 分类于 review |

参加了 Gopher China 2017 后,感触很多,想了很多,看了很多。 毕业后的第一个五年,很快就要过去了,回首来时路,发现自己要做的还很多。 没有完备的知识体系,没有拿得出作品,除了工作中的成果外,没有任何产出。 想到这些后背一凉,是时候该梳理一下,找寻一下,拼搏一下了。

说完了,题外话,就来点真是的了,第一篇每周回顾,回顾一周自己读的文章和书籍,写的blog,在专业上的长进,对于一些东西的认识。

1 微服务

在Gopher China 2017的活动中,听到的最多就是微服务这个概念,对我来说这个概念是如此之新,又如此的激发我的好奇心。 回来之后,找各种文章来了解微服务具体是怎么样的东东的,优点是什么,有那些缺点,以及怎样实践。 把研习的文章列于此,作为技术积累,以馈日后。

实施微服务,我们需要哪些基础框架?
从全局介绍了微服务,并给出了业界最近实践– Netflix的开源框架组件, 入门扫盲的一篇不二的佳文。
Introduction to Microservices
讨论了使用微服务的好处和缺点,尽管微服务的有相当大的复杂性,但通常它都是复杂应用的理想选择
Building Microservices: Using an API Gateway
应用程序的客户端如何通过 Api Gateway 与微服务进行通信
Building Microservices: Inter-Process Communication in a Microservices Architecture
微服务架构中服务之间的IPC机制
Service Discovery in a Microservices Architecture
微服务架构中的服务发现和服务注册
Event‑Driven Data Management for Microservices
微服务架构中的事件驱动数据管理机制
Choosing a Microservices Deployment Strategy
微服务的发布策略
Refactoring a Monolith into Microservices
单体程序迁移到微服务的策略
微服务那么热,创业公司怎么选用实践?
映兔科技在微服务中的实践
今日头条Go建千亿级微服务的实践
今日头条在微服务的实践,较为全面,涵盖了方方面面
饿了么的架构设计及演进之路
饿了么在架构方面的实践
企业的应用架构到底该怎么演变才能合了CEO的愿?
推演一个企业级应用架构的演变之路,由小及大,由浅入深,很有深意

2 Golang

用了两年golang了,却依旧觉得没有掌握正确的姿势,所以最近也是一通研习。

理解Go Context机制
由于我的目标是搭一套微服务出来,初步选定服务间的通信机制选用grpc,而grpc生成的代码中, 第一个参数都是 ctx context.Context ,所以务必要搞清楚 context 背后的原理。
grpc服务发现&负载均衡
grpc的服务发现和负载均衡的实现
Load Balancing in grpc
grpc中的负载平衡–实现自己的服务发现,需要阅读此文档
grpc Health Checking Protocol
grpc的健康监测机制

3 无关技术

3.1 如何提升你的能力?给年轻程序员的几条建议

无关技术,却给出技术人该有的修为,以及怎样更好的成长,准备随后写篇读后感,写写自己接下来要怎么做。

3.2 不要自称是程序员,我十多年的 IT 职场总结

别人雇用你的目的,是让你创造价值,而不是让你编程 一位老司机的肺腑之言,值得每个初入职场的IT从业人员深思, 你对于企业的价值是什么,无非就是创造价值与节约成本,编程只是实现的一种途径,放大你的劳动成果,为公司创造更好的利润,节约了公司的成本支出,你才是有价值的。 对于此文,我也准备写篇读后感。

4 认知

无意之间翻到了 傅盛的认知三部曲 ,和我一直以来的想法不谋而合,我觉得人和人之间的差别,就是看待这个世界的角度或者视角不同,或者说对于这个世界的认知层次不同。 我一直以来最想要的一种能力就是有明锐的洞察力,能看到技术发展趋势,能看到下一个风口,但是我不能,至少目前的我不能,因为我没有那么高层次的认知。大学时期, 反复看过一个有关乔布斯采访的报道,当时乔布斯离开苹果,自己创立公司。采访中,有一个关于未来的技术发展趋势的问题,乔布斯指出自己看好 互联网 和 Web 。 这个采访发生在1995年, 当时中国互联网可能都还没,即便是大洋彼岸的美国互联网也还处于萌芽阶段,但是老乔却已精准的认知到了未来的机遇与趋势。

4.1 傅盛认知三部曲之一:所谓成长就是认知升级

<img src="/images/ren-zhi-four-status.jpg" />

人有四种认知状态:

  • 不知道自己不知道——以为自己什么都知道,自以为是的认知状态
  • 知道自己不知道——有敬畏之心,开始空杯心态,准备丰富自己的认知。
  • 知道自己知道——抓住了事情的规律,提升了自己的认知。
  • 不知道自己知道——永远保持空杯心态,认知的最高境界。

95%的人都处在第一个状态,甚至更多。这也就是为什么碌碌无为的人是大多数。 视而不见,只会失去升级的可能性。只有自我否定,保持空杯心态,一个人才有可能真正成长,实现跨越。

真正的认知需要通过行动展现,行动一旦缺失,认知容易陷入误区。 博盛总结了两个可能的误区:

  • 以为自己知道,远远不如以为自己不知道 自以为是,是自我认知升级的死敌。 自我否定,假设自己无知,是自我认知升级的唯一路径。
  • 以为自己认为重要和真的认为重要,往往不是一回事 有一个词叫自我迷惑。自认为觉得很重要,但根本没把它转化成真正的行动。 不行动的认知,就是伪认知。炫耀自己知道,有什么用?一个浪潮打过来,认知就没了,如同没有。真正的认知,必须知行结合。

人和人最大的差别是认知 , 但认知可以升级,你可以去拟合差别, 认知升级的三剂良药:

  • 坚信大趋势
  • 对外求教,不做井底之蛙
  • 活在当下,面向未来

如果一个人,不断想学习,想了解,去反思;空杯心态,放下恐惧,不拒绝改变。认知升级,其实也就是捅破那层窗户纸。成长如是。

4.2 傅盛认知三部曲之二:管理本质就是认知管理

这个时代,管理不是执行管理,不是组织结构管理,而是你比别人更理解一件事情。管理的本质就是一种认知管理。 领导力的核心不是所谓的高情商,而是在大格局下构建对整个行业的认知体系,用大趋势做出正确的判断和聪明的决策。 在这个大的认知体系下,管理又可被细化为“信息、时间、人”三个维度:怎样利用“信息”做出正确的决定,怎样通过抓关键让“时间”更高效,怎样运用简化管理“人”。

博盛总结了“一体两翼”和“三个管理维度”,来回答以上的几个怎样的问题。

  • 一体:构建领导者的认知体系 一个优秀的领导,必须在核心点上拥有覆盖队伍的认知体系。 本质上,就是这个人,在这个点上的认知体系,超越了一个庞大的队伍。 所谓认知体系,是在脑海里有完整的认知框架,才能做出正确的判断。 怎么建立这种认知架构?
    • 首先,对市场和产品的深入了解是认知体系的基础。
    • 其次,真的要去和市场上吃过猪肉的人多聊天。
    • 其三,切忌以听报告的方式建立认知。
  • 两翼:认知管理的两剂良药 大的认知体系构建之后,具体问题是否有具体方法论作支撑——比如,事情太多管不过来怎么办?做了那么多总被老板批怎么办?做得辛苦不出绩效怎么办? 对此,博盛开了两剂良药:
    • 解药一:学会逆向思考,如果花的时间少一半,事情能否做得更好 怎么让管理变得更有效率? 本质是减少真正所谓管理的量,增加判断的量。 核心是转换思维,培养做判断的能力。而不是勤勉工作的能力。勤勉工作只是基础。 假设一下,如果只花一半时间,事情能不能做得更好?顺着这个方向想,很多事情就会不断要求去划分优先级。
    • 解药二:战略的略是忽略,不敢忽略,本质是分不清优先级 不敢忽略,本质就是分不清优先级。怎么去建立优先级?分清优先级的前提是认知清晰。 人和人最大的区别就在于思维格局。什么是中层?什么是创始人? 两者区别就在于:一个是迷恋具体情况,我在努力工作;一个是高低结合,我既能努力工作,又能不断花时间去反思,去判断,去拿到认知。 而且清楚知道,低的目的是高。即我的每一个执行,本质上又在建立我的认知。
  • 三个管理维度:信息、时间、人 宏观层面,领导者要构建对行业的认知体系;那么微观层面,执行操作时,怎样才能做到更聪明的工作?怎样找到那件最重要的事? 博盛从信息、时间、人三个维度剖析管理方法。
    • 先说信息维度。人的本质就是一个CPU。有足够大的信息输入,足够高的反思频度,你才会有足够的信息输出,也才会产生格局,做出正确判断。 信息怎么输入?第一,深入分析对手。 第二,定期遍访行业。第三,不断招聘行业里的人。
    • 再来说时间维度,管理上最重要的资源就是领导人时间。时间的分配,表明了一个领导者对实际情况的优先级判断。
    • 讲完信息和时间,回到人的维度。一句话:学会通过管一个人达到管一片人的目的。 管理一个人,解决一大片。要简化对人的管理,找到关键人。其次就是简化目标。团队目标越简单,越明确,越容易达成一致。

4.3 傅盛认知三部曲之三:战略就是格局+破局

经过二十年的发展,今天,互联网已经是一个传统行业。风停了,放眼望去,到处是血海竞争,乌压压一片创业大军。勤奋依然很重要,但聪明的勤奋才是关键。这个时候,就要求我们想清楚,行业里的大风在哪里,并做出预测。

因此,你的脑海里必须有一个对于这个行业越来越清晰的认知格局脑图。哪里已经是过度竞争,哪里刚兴起却没人察觉,三四线城市网民的不同在哪,互联网与哪个行业、以哪种形式的结合会有机会等等。

我们需要在这样的大格局下,在过去积累的认知红利之上,重新构建新的认知体系,制定战略的新打法,去更大的空间,寻找新的破局点和机会。

现象即规律–没有偶然,只有必然,所有单点都是大趋势下的必然。

美国人强调“think different”很有情怀,后来才发现,本质不是情怀,而是为了减少竞争成本。 因为美国创业者们比我们更早进入血海竞争阶段,“勤奋+努力+不要命”已经很难产生质的差别了,才逼迫他们用“更勤奋的思考”来避免高成本的竞争,从而降低失败概率。

创业必须讲究方法论,必须讲究不同情形下的不同方法。今天互联网的竞争格局,远远不是十年前的样子。我们必须think different,而think different的前提,就是要有行业格局认知,看清大趋势,在大趋势下做判断。 所谓战略,就是在这样的格局认知下,找到破局点,制定路线图,投入资源。如果不去建立这样的认知,公司很容易陷入一些误区。

战略认知的两个误区

  • 第一个误区是:见招拆招,啥热做啥,啥熟悉做啥。 这是懒惰思考,不愿意认知升级的表现。结果就是越做越多,越做越累,越做越委屈。 如果每个单点,不是在一个大格局下的累加,以致每个单点都会遇到对手强大的竞争,很难长大。
  • 第二个误区是:做产品的方法论依然停留在5年前,认为抓一个简单功能热点就颠覆格局。 只把一个单点做到极致就能创造奇迹的时代,真的过去了。你必须结合趋势,结合整个战略思考,把所有东西累加进去。 在看上去繁杂纷扰的信息中,不断深度思考,加大自己的认知优势,然后在熙熙攘攘的人流中找到不为人知的机会,趁着大家还不够懂,突然发起战役,全力以赴。

怎样做战略?

  • 首先,脑海要有大格局。大格局就是对这个行业深入的、清晰的认知。我们需要花足够的时间去了解行业,去思考对手,去观察现象。在获取大量信息后,不断在脑海里做思维推演,去判断。
  • 其次,养成格局和破局结合的思维习惯。 互联网竞争已经白热化的形势下,做战略的关键点,就在于不断加深自己的认知,找到已经存在但不为人知的那个秘密。 而且,这个秘密所能孕育的机会,要足够大;离现有领先者的区域,要足够远。核心是你能否具备超出对手的、对行业的、与众不同的认知。基于这个格局认知,为自己撕开一道突破口。

简单一句话概括——经过充分思考和认真研究后,制定清晰目标以及持续推进的路线图,这应该就是战略的全貌。

战略是在这个路线图下的势能的累加,不能累加势能的,再有效果的执行,本质都是增加成本。

回到战略,它的本质是什么?我认为,战略就是一个杠杆。它让你做的每一件事,都放大几倍,几十倍。一旦远离这个杠杆,就变成小公司创业模式。关键是,这种创业模式,又比不过真正的创业公司。

4.4 傅盛认知三部曲后记:到底什么是认知?

一幅图来描述认知产生的过程。

<img src="https://pic.36krcnd.com/201704/24130424/givz9ck2miuxx5x7!1200" />

认知三要素:

  • 认知第一要素:信息输入与挖掘
  • 认知第二要素:思维模式训练
  • 认知第三要素:自我博弈与输出判断
Last Updated 2017-05-23 Tue 13:28.
Render by hexo-renderer-org with Emacs 25.3.2 (Org mode 8.2.10)

Gopher面试中的Coding

发表于 2017-05-14 | 分类于 技术积累 |

从四月份下半月开始,陆陆续续面试了几家公司,都是golang的岗位。每一次面试,侧重点都会有不同,有的会直接给过来一道试题, 然后边解题,边讲述自己的思路,然后面试官根据你的思路和你交流沟通;有的呢,让讲述自己最近做过的项目,遇到的难点, 自己怎么解决的问题思路,而无独有偶的呢,这样的面试中,都要需要展示编码能力。这篇文章就把自己最近面试中遇到的每一个编程问题, 分三步阐述出来:问题描述,解题思路,实际编程。

1 交替打印数字和字母

1.1 问题描述

使用两个 goroutine 交替打印序列,一个 goroutinue 打印数字, 另外一个goroutine打印字母, 最终效果如下 12AB34CD56EF78GH910IJ 。

1.2 解题思路

问题很简单,使用 channel 来控制打印的进度。使用两个 channel ,来分别控制数字和字母的打印序列, 数字打印完成后通过 channel 通知字母打印, 字母打印完成后通知数字打印,然后周而复始的工作。

1.3 实际编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
runtime.GOMAXPROCS(runtime.NumCPU())
chan_n := make(chan bool)
chan_c := make(chan bool, 1)
done := make(chan struct{})

go func() {
for i := 1; i < 11; i += 2 {
<-chan_c
fmt.Print(i)
fmt.Print(i + 1)
chan_n <- true
}
}()

go func() {
char_seq := []string{"A","B","C","D","E","F","G","H","I","J","K"}
for i := 0; i < 10; i += 2 {
<-chan_n
fmt.Print(char_seq[i])
fmt.Print(char_seq[i+1])
chan_c <- true
}
done <- struct{}{}
}()

chan_c <- true
<-done

代码执行结果:

12AB34CD56EF78GH910IJ

看完上面的代码,是不是会有些疑惑,为什么 chan_c 需要缓存,而 chan_n 不需要呢?
当两个打印 goroutine 无限交替运行时,没有缓存是OK的, 但很明显上面的示例不是,打印数字的 goroutine 先退出,也就失去了 goroutine 来读取 chan_c 中的内容了, 而打印字母的goroutine就会阻塞在 chan_c <- true 这里,这样就导致了死锁。

当然无缓存版也是可以,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
runtime.GOMAXPROCS(runtime.NumCPU())
chan_n := make(chan bool)
chan_c := make(chan bool)
done := make(chan struct{})

go func() {
for i := 1; i < 11; i += 2 {
fmt.Print(i)
fmt.Print(i + 1)
chan_n <- true
<-chan_c
}

done <- struct{}{}
}()

go func() {
char_seq := []string{"A","B","C","D","E","F","G","H","I","J","K"}
for i := 0; i < 10; i += 2 {
<-chan_n
fmt.Print(char_seq[i])
fmt.Print(char_seq[i+1])
chan_c <- true
}

done <- struct{}{}
}()

<-done
<-done

2 随机抽奖

2.1 问题描述

用户随机抽奖,数据结构如下所示:

1
2
3
4
5
6
7
8
// map中,key代表名称,value代表成交单数
var users map[string]int64 = map[string]int64{
"a": 10,
"b": 6,
"c": 3,
"d": 12,
"f": 1,
}

2.2 解决思路

从map中选取随机用户,拿到这个编码问题,有点懵逼,但仔细一想,只需把关注用户的区间,转变一下数据结构即解题。 把map转成array,思考起来就简单多了,原有问题变成了从0至n-1中选取一个数字,数字对应的用户即中奖用户。

2.3 实际编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

import (
"fmt"
"math/rand"
"time"
)

func GetAwardUserName(users map[string]int64) (name string) {
sizeOfUsers := len(users)
award_index := rand.Intn(sizeOfUsers)

var index int
for u_name, _ := range users {
if index == award_index {
name = u_name
return
}
index += 1
}
return
}

func main() {
var users map[string]int64 = map[string]int64{
"a": 10,
"b": 6,
"c": 3,
"d": 12,
"e": 20,
"f": 1,
}

rand.Seed(time.Now().Unix())
award_stat := make(map[string]int64)
for i := 0; i < 1000; i += 1 {
name := GetAwardUserName(users)
if count, ok := award_stat[name]; ok {
award_stat[name] = count + 1
} else {
award_stat[name] = 1
}
}

for name, count := range award_stat {
fmt.Printf("user: %s, award count: %d\n", name, count)
}

return
}

代码执行结果:

user: f, award count: 178
user: d, award count: 152
user: b, award count: 159
user: e, award count: 182
user: c, award count: 170
user: a, award count: 159

3 权重抽奖

3.1 问题描述

数据结构和上面一致,只是问题发生变化,需要更加用户的成单数来抽奖,用户成单越多,中奖概率越高,结构如下所示:

1
2
3
4
5
6
7
8
// map中,key代表名称,value代表成交单数
var users map[string]int64 = map[string]int64{
"a": 10,
"b": 6,
"c": 3,
"d": 12,
"f": 1,
}

3.2 解决思路

这一题是上一题的延伸,加了订单数进去,做为权重来为用户抽奖。此题和上面的问题如此的相似,可把上面的问题, 理解成所有的用户权重都相同的抽奖,而此题是权重不同的抽奖。解决此问题,依旧是把map转为数组来思考, 把各用户的权重,从前到后依次拼接到数轴上,数轴的起点到终点即时中奖区间,而随机数落到的那个用户的区间,那个用户即为中奖用户。

3.3 实际编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package main

import (
"fmt"
"math/rand"
"time"
)

func GetAwardUserName(users map[string]int64) (name string) {
type A_user struct {
Name string
Offset int64
Num int64
}

a_user_arr := make([]*A_user, 0)
var sum_num int64
for name, num := range users {
a_user := &A_user{
Name: name,
Offset: sum_num,
Num: num,
}
a_user_arr = append(a_user_arr, a_user)
sum_num += num
}

award_num := rand.Int63n(sum_num)

for index, _ := range a_user_arr {
a_user := a_user_arr[index]
if a_user.Offset+a_user.Num > award_num {
name = a_user.Name
return
}
}
return
}

func main() {
var users map[string]int64 = map[string]int64{
"a": 10,
"b": 5,
"c": 15,
"d": 20,
"e": 10,
"f": 30,
}

rand.Seed(time.Now().Unix())
award_stat := make(map[string]int64)
for i := 0; i < 10000; i += 1 {
name := GetAwardUserName(users)
if count, ok := award_stat[name]; ok {
award_stat[name] = count + 1
} else {
award_stat[name] = 1
}
}

for name, count := range award_stat {
fmt.Printf("user: %s, award count: %d\n", name, count)
}

return
}

代码执行结果:

user: c, award count: 1667
user: f, award count: 3310
user: e, award count: 1099
user: d, award count: 2276
user: b, award count: 549
user: a, award count: 1099

感谢各位的评论,让我受益匪浅,上面代码确实有太多的槽点,感谢吐槽,代码更正如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func GetAwardUserName(users map[string]int64) (name string) {
var sum_num int64
for _, num := range users {
sum_num += num
}

award_num := rand.Int63n(sum_num)

var offset_num int64
for _name, num := range a_user_arr {
offset_num += num
if award_num < offset_num {
name = _name
return
}
}
return
}

由于一直以为Golang的map for range 是可重入的,但现实是前后两轮遍历到的 key 的顺序居然是被随机化的, 代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
n_map := make(map[int]bool)
for i := 1; i <= 10; i++ {
n_map[i] = true
}

for num, _ := range n_map {
fmt.Print(num)
}
fmt.Print("\n")
for num, _ := range n_map {
fmt.Print(num)
}
91257103468
46810325791

由于map的不可重入性, 以及 liguoqinjim 给出的 示例代码 和 运行结果 证明了map的 for range 的伪随机性, 代码修改如下(在Playground 中可查看完整代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func GetAwardUserName(users map[string]int64) (name string) {
var sum_num int64
name_arr := make([]string, len(users))
for u_name, num := range users {
sum_num += num
name_arr = append(name_arr, u_name)
}

award_num := rand.Int63n(sum_num)

var offset_num int64
for _, u_name := range name_arr {
offset_num += users[u_name]
if award_num < offset_num {
name = u_name
return
}
}
return
}

上面代码,对于多次调用会有性能问题,每次都要重新计算 sum_num 和创建 name_arr, 使用闭包优化实现, 代码如下(在Playground 中可查看完整代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func GetAwardGenerator(users map[string]int64) (generator func() string) {
var sum_num int64
name_arr := make([]string, len(users))
for u_name, num := range users {
sum_num += num
name_arr = append(name_arr, u_name)
}

generator = func() string {
award_num := rand.Int63n(sum_num)

var offset_num int64
for _, u_name := range name_arr {
offset_num += users[u_name]
if award_num < offset_num {
return u_name
}
}
// 缺省返回,正常情况下,不会运行到此处
return name_arr[0]
}
return
}

上面代码使用了闭包避免了多次抽奖时频繁的初始化, 但每次抽奖的复杂度O(n),很明显依旧有可优化的空间,可使用二分搜索来使复杂度降到 O(log n), 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
func GetAwardGenerator(users map[string]int64) (generator func() string) {
var sum_num int64
name_arr := make([]string, len(users))
offset_arr := make([]int64, len(users))
var index int
for u_name, num := range users {
name_arr[index] = u_name
offset_arr[index] = sum_num
sum_num += num
index += 1
}

generator = func() string {
award_num := rand.Int63n(sum_num)
return name_arr[binary_search(offset_arr, award_num)]
}
return
}

func binary_search(nums []int64, target int64) int {
start, end := 0, len(nums)-1
for start <= end {
mid := start + (end-start)/2
if nums[mid] > target {
end = mid - 1
} else if nums[mid] < target {
if mid+1 == len(nums) { // 最后一名中奖
return mid
}
if nums[mid+1] > target {
return mid
}
start = mid + 1
} else {
return mid
}
}

return -1
}

在已知长度的情况下,应使用 array[index]=num 而避免使用 array=append(array, num), 代码和测试如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"time"
)

func main() {
test_len := 10000000
start := time.Now()
s := make([]int, test_len, test_len)
for i := 0; i < test_len; i++ {
s = append(s, i)
}
fmt.Println(time.Now().Sub(start).String())

start = time.Now()
s1 := make([]int, test_len)
for i := 0; i < test_len; i++ {
s1[i] = i
}
fmt.Println(time.Now().Sub(start).String())
}
132.123121ms
27.453897ms

4 总结

问题一来自一家公司 , 侧重于语言特性;问题二三来自另外一家公司 ,侧重于解决问题的思路;本人更喜欢第二种,很有启发性。 我之后会把其他自己认为比较有趣的编程任务,整理到此篇文章中,敬请期待。

Last Updated 2017-10-18 Wed 14:50.
Render by hexo-renderer-org with Emacs 25.3.2 (Org mode 8.2.10)

consul的docker环境配置

发表于 2017-05-13 |

构造consul的docker集群

sudo docker run -d --name=node0 consul agent -server -client=0.0.0.0 -node=node0 -bootstrap-expect=1 -bind=172.17.0.2 -data-dir=/tmp/consul -ui-dir /ui
sudo docker run -d --name=node1 consul agent -node-id=$(uuidgen | awk '{print tolower($0)}') -server -client=0.0.0.0 -node=node1 -bind=172.17.0.3 -join=172.17.0.2
sudo docker run -d --name=node2 consul agent -node-id=$(uuidgen | awk '{print tolower($0)}') -server -client=0.0.0.0 -node=node2 -bind=172.17.0.4 -join=172.17.0.2
sudo docker run -d --name=node3 consul agent -node-id=$(uuidgen | awk '{print tolower($0)}') -client=0.0.0.0 -node=node3 -bind=172.17.0.5 -join=172.17.0.2

参看集群状态

sudo docker exec -t node0 consul info

Network Coordinates in consul

curl http://172.17.0.2:8500/v1/coordinate/datacenters
curl http://172.17.0.2:8500/v1/coordinate/nodes

健康检查

curl http://172.17.0.3:8500/v1/health/service/consul
Last Updated 2017-06-01 Thu 13:40.
Render by hexo-renderer-org with Emacs 25.3.2 (Org mode 8.2.10)

Org-mode的列视图简明教程

发表于 2017-04-14 | 分类于 org-mode |

原文Emacs Org's Column View, 由 Bastien Guerry 编辑,维护。本文只做学习之用。

简介:默认列视图

首先按 C-c C-x C-c 打开默认列视图, 将每个 outline item 转换成一个显示其某些属性的表格行。

org-col-default-view.png

只可以在列视图起作用的条目上,按 q 关闭列视图,返回到普通视图, 但可以从缓冲区中的任何位置打开列视图。

第一个标题现在是一列显示属性的可浏览列。 缓冲区的第一个突出显示的行简要地告诉你在每个列中显示什么属性。 在这个截图中,它显示:

ITEM for the headline title
  T for the TODO keyword
  P for the priority cookie
  T for the tags

默认列仅显示 当前条目的内容(标题内容) ,/TODO/ 状态, 当前条目的优先级及其标签,稍后将看到如何添加自己的其他属性。

此默认设置由变量 org-columns-default-format 所定义的,该全局值为:

#+COLUMNS: %25ITEM %TODO %3PRIORITY %TAGS
Element Description
%25ITEM display the item in a 25-characters-width field
%TODO display the TODO state of the item
%3PRIORITY display the priority in a 3-chars-width field
%TAGS display the tags of the entry

<!– more –>

自定义默认列视图

好的,现在让我们来自定义列视图。

例如,我们要更改 PRIORITY 字段和 TAGS 字段的宽度:

#+COLUMNS: %25ITEM %5TODO %1PRIORITY %10TAGS

org-col-default-customized-view1.png

TODO字段(%5TODO)现在为5个字符,而优先级和标签字段为1和10。

现在我们要更改列的标题。 例如 - 由于我们是这样的勤奋工作 - 每个项目其实就是一个 /Task/:

#+COLUMNS: %25ITEM(Task) %5TODO(To-do) %1PRIORITY %10TAGS

org-col-default-customized-view2.png

以上还添加了一个 To-do 的别名,用于显示此条目的TODO状态。

列视图中添加其他属性

要怎么在列视图中添加其他属性呢? 例如,我们想要添加 SCHEDULED 属性。 那么只需要重新定义全局 #+COLUMNS 选项,如下所示:

#+COLUMNS: %30ITEM %10SCHEDULED %TODO %3PRIORITY %TAGS

刷新 Org 缓冲区来使配置生效,然后再次输入 C-c C-x C-c 。 现在列视图中显示SCHEDULED属性。

org-col-default-customized-view3.png

** Exemple of outline item with a SCHEDULED property
   SCHEDULED: <2007-10-14 dim>

可用在列视图中可用属性如下所示:

ITEM         The content of the headline.
TODO         The TODO keyword of the entry.
TAGS         The tags defined directly in the headline.
ALLTAGS      All tags, including inherited ones.
PRIORITY     The priority of the entry, a string with a single letter.
DEADLINE     The deadline time string, without the angular brackets.
SCHEDULED    The scheduling time stamp, without the angular brackets.

以上属性都是特殊属性,但是可以定义自己的属性。

在自定义属性进行复杂的操作之前,让我们来学习如何为不同的子树使用不同的列视图。

定义子树的列视图

要定义特定条目的列视图,只需添加特殊属性 :COLUMNS: 即可:

** Top node for columns view
   :PROPERTIES:
   :COLUMNS:  %25ITEM %TAGS %PRIORITY %TODO
   :END:

此视图将用于条目及其整个子树 - 除非其子节点有其自己的列视图。

看下面的示例:

** Top node for columns view
   :PROPERTIES:
   :COLUMNS: %25ITEM %TAGS %PRIORITY %TODO
   :END:
*** TODO Example 1
*** TODO Example 2
*** DONE Example 3

org-col-default-customized-view4.png

但是,如果你突然喜欢 %TAGS 在 %TODO 的右边呢? 将光标放在 %TAGS 字段中,然后按 M-right ,它会将该字段向右移动。

如果你想让一个区域变得更宽? 没问题。 只要去那个字段,然后按`>'来扩大字段(或'<'缩小它)。

如果要交互定义属性的列元素,请转到其字段并按's'。

已知道如何自定义每个条目的列视图,接下来就到自定义属性了。

为某些属性添加 summary-types

来定义一个包含自己的列视图和一些属性的新条目:

** My project
   :PROPERTIES:
   :COLUMNS:  %20ITEM %9Approved(Approved?){X} %Owner %11Status %10Time_Spent{:}
   :END:

org-col-default-customized-view5.png

有点复杂,这里解说一下。 一个 :COLUMNS: 属性,定义了列视图,具体每个元素具体含义如下:

Element Description
%20ITEM display the item (20 characters for this field)
%9Approved(Approved?){X} display the "Approved" property
%Owner display the "Owner" property
%11Status display the "Status" property
%10TimeSpent{:} display the "Timespent" property

{X} 和 {:} 具体代表了什么,有什么含义呢? 它们定了 summary-types.

{X} 表示:如果所有条目的 Approved 属性都具有 [X] 值,才最终显示 [X] (否则显示 [-] 或 [ ] )。

{:} 表示:通过把 Timespent 属性中找到的所有时间值求和,来显示总的时间支出。

一旦有了 :COLUMN: 属性定义,可以通过 C-c C-x p 交互地添加任何属性。 它将提示输入属性的名称,并根据属性(如果有)的 _ALL 关联属性或缓冲区中找到的值提供默认的可能值。

定义属性的所有可能值

定义 summary-types 类型意味着需要为某些属性设置一组有限的可能值。

例如,上面讨论的 Approved 应该只有两个可能的值: [ ] 和 [X] 。

Status 属性也是同样的:你可能只想定义一些状态, 如 "In progress" "Not started yet" "Finished"。

可以使用 _ALL 后缀来限制任何属性的允许值,如下所示:

** My project
   :PROPERTIES:
   :COLUMNS:  %20ITEM %9Approved(Approved?){X} %Owner %11Status %10Time_Spent{:}
   :Owner_ALL:    Tammy Mark Karl Lisa Don
   :Status_ALL:   "In progress" "Not started yet" "Finished" ""
   :Approved_ALL: "[ ]" "[X]"
   :END:
| Owner_ALL    | only accept Tammy Mark Karl Lisa Don                   |
| Status_ALL   | only accept "In progress" "Not started yet" "Finished" |
| Approved_ALL | only accept "[ ]" "[X]"                                |

注意:* _ALL属性是元属性,定义了如何使用属性本身的规则。

当位于列的字段中时,可以通过按 a 来定义关联属性的所有可能值: 它将提示当前的一组允许的值,你可以编辑它。

在子树中有三个条目的完整的例子

下面是一个关于列视图如何影响条目及其子树的显示的示例。 好好观察并测试它。

** My project
   :PROPERTIES:
   :COLUMNS:  %20ITEM %9Approved(Approved?){X} %Owner %11Status %10Time_Spent{:}
   :Owner_ALL:    Tammy Mark Karl Lisa Don
   :Status_ALL:   "In progress" "Not started yet" "Finished" ""
   :Approved_ALL: "[ ]" "[X]"
   :END:

*** Item 1
    :PROPERTIES:
    :Owner:    Tammy
    :Time_spent:   1:45
    :Status:   Finished
    :END:

*** Item 2
    :PROPERTIES:
    :Owner:    Tammy
    :Status:   In progress
    :Time_spent:   0:15
    :END:

*** Item 3
    :PROPERTIES:
    :Owner:    Lisa
    :Status:   Not started yet
    :Approved: [X]
    :END:

从列视图编辑属性

到现在为止还挺好。 但是,列视图的一个好处是它可以让您快速访问和编辑任何属性。

使用 v 在minibuffer中显示字段值。

使用 e 来交互地选择/编辑值。

使用 S-left/right 循环遍历字段中的允许值。

使用 a 编辑此属性的允许值。

结论: 能做的还有更多更多

好的,以上就是今天的全部了。 但是让我告诉你最后两个提示,让你进一步探索的列视图:

  1. 您可以使用列视图并循环浏览可见性。
  2. 列视图也适用于议程缓冲区。
  • http://orgmode.org/
  • http://orgmode.org/org.html#Column-view
Last Updated 2017-05-23 Tue 13:28.
Render by hexo-renderer-org with Emacs 25.3.2 (Org mode 8.2.10)
1234
Brantou

Brantou

39 日志
5 分类
46 标签
RSS
GitHub Twitter E-Mail Jianshu
© 2017 — 2018 Brantou
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.3
Hosted by GitHub Pages