1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-05-24 12:32:21 +00:00

Merge branch 'master' of github.com:chai2010/advanced-go-programming-book

This commit is contained in:
Xargin 2018-08-08 19:57:27 +08:00
commit 6161c18bbc
16 changed files with 37 additions and 37 deletions

View File

@ -149,7 +149,7 @@ func main() {
cgo中的pkg-config只需要两个参数`--cflags``--libs`。其中`--libs`选项的输出我们采用的是`python3-config --ldflags`的输出,因为`--libs`选项没有包含库的检索路径,而`--ldflags`选项则是在指定链接库参数的基础上增加了库的检索路径。
基于py3-config.go可以创建一个py3-config命令。然后通过PKG_CONFIG环境变量将cgo使用的pkg-config命令指定为我们制的命令:
基于py3-config.go可以创建一个py3-config命令。然后通过PKG_CONFIG环境变量将cgo使用的pkg-config命令指定为我们制的命令:
```
PKG_CONFIG=./py3-config go build -buildmode=c-shared -o gopkg.so main.go

View File

@ -172,7 +172,7 @@ void main(void) {
}
```
程序开头的`#include <alef.h>`语句用于包含Alef语言的运行时库。`receive`是一个普通函数,程序中用作每个并发体的入口函数;`main`函数中的`alloc c`语句先创建一个`chan(byte*)`类型的管道类似Go语言的`make(chan []byte)`语句;然后分别启动以进程和线程的方式启动`receive`函数;启动并发体之后,`main`函数向`c`管道发送了两个字符串数据; 而进程和线程状态运行的`receive`函数会以不确定的顺序先后从管道收到数据后,然后分别打印字符串;最后每个并发体都通过调用`terminate(nil)`来结束自己。
程序开头的`#include <alef.h>`语句用于包含Alef语言的运行时库。`receive`是一个普通函数,程序中用作每个并发体的入口函数;`main`函数中的`alloc c`语句先创建一个`chan(byte*)`类型的管道类似Go语言的`make(chan []byte)`语句;然后分别以进程和线程的方式启动`receive`函数;启动并发体之后,`main`函数向`c`管道发送了两个字符串数据; 而进程和线程状态运行的`receive`函数会以不确定的顺序先后从管道收到数据后,然后分别打印字符串;最后每个并发体都通过调用`terminate(nil)`来结束自己。
Alef的语法和C语言基本保持一致可以认为它是在C语言的语法基础上增加了并发编程相关的特性可以看作是另一个维度的C++语言。

View File

@ -253,7 +253,7 @@ for i := 0; i < len(s); i++ {
Go语言除了`for range`语法对UTF8字符串提供了特殊支持外还对字符串和`[]rune`类型的相互转换提供了特殊的支持。
```go
fmt.Printf("%#v\n", []rune("Hello, 世界")) // []int32{19990, 30028}
fmt.Printf("%#v\n", []rune("世界")) // []int32{19990, 30028}
fmt.Printf("%#v\n", string([]rune{'世', '界'})) // 世界
```
@ -410,7 +410,7 @@ a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
```
不过要注意的是,在容量不足的情况下,`append`的操作会导致重新分配内存,从而导致巨大的内存分配和复制数据代价。即使容量足够,依然需要用`append`函数的返回值来更新切片本身,因为新切片的长度已经发生了变化。
不过要注意的是,在容量不足的情况下,`append`的操作会导致重新分配内存,可能导致巨大的内存分配和复制数据代价。即使容量足够,依然需要用`append`函数的返回值来更新切片本身,因为新切片的长度已经发生了变化。
除了在切片的尾部追加,我们还可以在切片的开头添加元素:
@ -450,11 +450,11 @@ copy(a[i+len(x):], a[i:]) // a[i:]向后移动len(x)个位置
copy(a[i:], x) // 复制新添加的切片
```
稍显不足的是,在第一句扩展切片容量的时候,扩展空间部分的元素复制是没有必要的。并没专门有内置的函数用于扩展切片的容量,`append`本质是用于追加元素而不是扩展容量,扩展切片容量只是`append`的一个副作用。
稍显不足的是,在第一句扩展切片容量的时候,扩展空间部分的元素复制是没有必要的。没有专门的内置函数用于扩展切片的容量,`append`本质是用于追加元素而不是扩展容量,扩展切片容量只是`append`的一个副作用。
**删除切片元素**
根据要删除元素的位置有三种类型:从开头位置删除,从中间位置删除,从尾部删除。其中删除切片尾部的元素最快:
根据要删除元素的位置有三种情况:从开头位置删除,从中间位置删除,从尾部删除。其中删除切片尾部的元素最快:
```go
a = []int{1, 2, 3}
@ -551,7 +551,7 @@ func FindPhoneNumber(filename string) []byte {
这段代码返回的`[]byte`指向保存整个文件的数组。因为切片引用了整个原始数组,导致自动垃圾回收器不能及时释放底层数组的空间。一个小的需求可能导致需要长时间保存整个文件数据。这虽然这并不是传统意义上的内存泄漏,但是可能会拖慢系统的整体性能。
要修复这个问题可以将感兴趣的数据复制到一个新的切片中数据的传值是Go语言编程的一个哲学虽然传值有一定的代价但是换取好处是切断了对原始数据的依赖
要修复这个问题可以将感兴趣的数据复制到一个新的切片中数据的传值是Go语言编程的一个哲学虽然传值有一定的代价但是换取好处是切断了对原始数据的依赖):
```go
func FindPhoneNumber(filename string) []byte {

View File

@ -24,10 +24,10 @@ var Add = func(a, b int) int {
}
```
Go语言中的函数可以有多个输入参数和多个返回值,输入参数和返回值都是以传值的方式和被调用者交换数据。在语法上,函数还支持可变数量的参数,可变数量的参数必须是最后出现的参数,可变数量的参数其实是一个切片类型的参数。
Go语言中的函数可以有多个参数和多个返回值参数和返回值都是以传值的方式和被调用者交换数据。在语法上函数还支持可变数量的参数可变数量的参数必须是最后出现的参数可变数量的参数其实是一个切片类型的参数。
```go
// 多个输入参数和多个返回值
// 多个参数和多个返回值
func Swap(a, b int) (int, int) {
return b, a
}
@ -57,9 +57,9 @@ func Print(a ...interface{}) {
}
```
第一个`Print`调用时传入的参数是`a...`,等价于直接调用`Print(123, "abc")`。第二个`Print`调用传入的是解包的`a`,等价于直接调用`Print([]interface{}{123, "abc"})`
第一个`Print`调用时传入的参数是`a...`,等价于直接调用`Print(123, "abc")`。第二个`Print`调用传入的是解包的`a`,等价于直接调用`Print([]interface{}{123, "abc"})`
不仅函数的输入参数可以有名字,也可以给函数的返回值命名:
不仅函数的参数可以有名字,也可以给函数的返回值命名:
```go
func Find(m map[int]int, key int) (value int, ok bool) {
@ -114,7 +114,7 @@ func main() {
}
```
第一种方法是在循环体内部再定义一个局部变量,这样每次迭代`defer`语句的闭包函数捕获的都是不同的变量,这些变量的值对应迭代时的值。第二种方式是将迭代变量通过闭包函数的参数传入,`defer`语句会马上对调用参数求值。两种方式都是可以工作的。不过一般来说,在`for`循环内部执行`defer`语句并不是一个好的习惯,此处仅为示例,不建议使用
第一种方法是在循环体内部再定义一个局部变量,这样每次迭代`defer`语句的闭包函数捕获的都是不同的变量,这些变量的值对应迭代时的值。第二种方式是将迭代变量通过闭包函数的参数传入,`defer`语句会马上对调用参数求值。两种方式都是可以工作的。不过一般来说,在`for`循环内部执行`defer`语句并不是一个好的习惯,此处仅为示例,不建议使用
Go语言中如果以切片为参数调用函数时有时候会给人一种参数采用了传引用的方式的假象因为在被调用函数内部可以修改传入的切片的元素。其实任何可以通过函数参数修改调用参数的情形都是因为函数参数中显式或隐式传入了指针参数。函数参数传值的规范更准确说是只针对数据结构中固定的部分传值例如字符串或切片对应结构体中的指针和字符串长度结构体传值但是并不包含指针间接指向的内容。将切片类型的参数替换为类似`reflect.SliceHeader`结构体就很好理解切片传值的含义了:
@ -159,7 +159,7 @@ func g() int {
## 1.4.2 方法
方法一般是面向对象编程(OOP)的一个特性在C++语言中方法对应一个类对象的成员函数是关联到具体对象上的虚表中的。但是Go语言的方法却是关联到类型的这样可以在编译阶段完成方法的静态绑定。一个面向对象的程序会用方法来表达其属性对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情。面向对象编程(OOP)进入主流开发领域一般认为是从C++开始的C++就是在兼容C语言的基础之上支持了class等面向对象的特性。然后Java编程则号称是纯粹的面向对象语言因为Java中函数是不能独立存在的每个函数都必然是属于某个类的。
方法一般是面向对象编程(OOP)的一个特性在C++语言中方法对应一个类对象的成员函数是关联到具体对象上的虚表中的。但是Go语言的方法却是关联到类型的这样可以在编译阶段完成方法的静态绑定。一个面向对象的程序会用方法来表达其属性对应的操作这样使用这个对象的用户就不需要直接去操作对象而是借助方法来做这些事情。面向对象编程(OOP)进入主流开发领域一般认为是从C++开始的C++就是在兼容C语言的基础之上支持了class等面向对象的特性。然后Java编程则号称是纯粹的面向对象语言因为Java中函数是不能独立存在的每个函数都必然是属于某个类的。
面向对象编程更多的只是一种思想很多号称支持面向对象编程的语言只是将经常用到的特性内置到语言中了而已。Go语言的祖先C语言虽然不是一个支持面向对象的语言但是C语言的标准库中的File相关的函数也用到了的面向对象编程的思想。下面我们实现一组C语言风格的File函数
@ -321,7 +321,7 @@ func (p *Cache) Lookup(key string) string {
## 1.4.3 接口
Go语言之父Rob Pike曾说过一句名言那些试图避免白痴行为的语言最终自己变成了白痴语言Languages that try to disallow idiocy become themselves idiotic。一般静态编程语言都有着严格的类型系统这使得编译器可以深入检查程序员没有作出什么出格的举动。但是过于严格的类型系统却会使得编程太过繁琐让程序员把大好的青春都浪费在了和编译器的斗争中。Go语言试图让程序员能在安全和灵活的编程之间取得一个平衡。它在提供严格的类型检查的同时通过接口类型实现了对鸭子类型的支持使得安全动态的编程变得相对容易。
Go语言之父Rob Pike曾说过一句名言那些试图避免白痴行为的语言最终自己变成了白痴语言Languages that try to disallow idiocy become themselves idiotic。一般静态编程语言都有着严格的类型系统这使得编译器可以深入检查程序员没有作出什么出格的举动。但是过于严格的类型系统却会使得编程太过繁琐让程序员把大好的青春都浪费在了和编译器的斗争中。Go语言试图让程序员能在安全和灵活的编程之间取得一个平衡。它在提供严格的类型检查的同时通过接口类型实现了对鸭子类型的支持使得安全动态的编程变得相对容易。
Go的接口类型是对其它类型行为的抽象和概括因为接口类型不会和特定的实现细节绑定在一起通过这种抽象的方式我们可以让对象更加灵活和更具有适应能力。很多面向对象的语言都有相似的接口概念但Go语言中接口类型的独特之处在于它是满足隐式实现的鸭子类型。所谓鸭子类型说的是只要走起路来像鸭子、叫起来也像鸭子那么就可以把它当作鸭子。Go语言中的面向对象就是如此如果一个对象只要看起来像是某种接口类型的实现那么它就可以作为该接口类型使用。这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不用去破坏这些类型原有的定义当我们使用的类型来自于不受我们控制的包时这种设计尤其灵活有用。Go语言的接口类型是延迟绑定可以实现类似虚函数的多态功能。

View File

@ -71,7 +71,7 @@ func main() {
如果使用的是第三方的静态库,我们需要先下载安装静态库到合适的位置。然后在#cgo命令中通过CFLAGS和LDFLAGS来指定头文件和库的位置。对于不同的操作系统甚至同一种操作系统的不同版本来说,这些库的安装路径可能都是不同的,那么如何在代码中指定这些可能变化的参数呢?
在Linux环境有一个pkg-config命令可以查询要使用某个静态库或动态库时的编译和链接参数。我们可以在#cgo命令中直接使用pkg-config命令来生成编译和链接参数。而且还可以通过PKG_CONFIG环境变量制pkg-config命令。因为不同的操作系统对pkg-config命令的支持不尽相同通过该方式很难兼容不同的操作系统下的构建参数。不过对于Linux等特定的系统pkg-config命令确实可以简化构建参数的管理。关于pkg-config的使用细节在此我们不深入展开大家可以自行参考相关文档。
在Linux环境有一个pkg-config命令可以查询要使用某个静态库或动态库时的编译和链接参数。我们可以在#cgo命令中直接使用pkg-config命令来生成编译和链接参数。而且还可以通过PKG_CONFIG环境变量制pkg-config命令。因为不同的操作系统对pkg-config命令的支持不尽相同通过该方式很难兼容不同的操作系统下的构建参数。不过对于Linux等特定的系统pkg-config命令确实可以简化构建参数的管理。关于pkg-config的使用细节在此我们不深入展开大家可以自行参考相关文档。
## 2.9.2 使用C动态库

View File

@ -2,7 +2,7 @@
Go语言中很多设计思想和工具都是传承自Plan9操作系统Go汇编语言也是基于Plan9汇编演化而来。根据Rob Pike的介绍大神Ken Thompson在1986年为Plan9系统编写的C语言编译器输出的汇编伪代码就是Plan9汇编的前身。所谓的Plan9汇编语言只是便于以手工方式书写该C语言编译器输出的汇编伪代码而已。
无论高级语言如何发展作为最接近CPU的汇编语言的地依然是无法彻底被替代的。只有通过汇编语言才能彻底挖掘CPU芯片的全部功能因此操作系统的引导过程必须要依赖汇编语言的帮助。只有通过汇编语言才能彻底榨干CPU芯片的性能因此很多底层的加密解密等对性能敏感的算法会考虑通过汇编语言进行性能优化。
无论高级语言如何发展作为最接近CPU的汇编语言的地依然是无法彻底被替代的。只有通过汇编语言才能彻底挖掘CPU芯片的全部功能因此操作系统的引导过程必须要依赖汇编语言的帮助。只有通过汇编语言才能彻底榨干CPU芯片的性能因此很多底层的加密解密等对性能敏感的算法会考虑通过汇编语言进行性能优化。
对于每一个严肃的GopherGo汇编语言都是一个不可忽视的技术。因为哪怕只懂一点点汇编也便于更好地理解计算机原理也更容易理解Go语言中动态栈/接口等高级特性的实现原理。而且掌握了Go汇编语言之后你将重新站在编程语言鄙视链的顶端不用担心再被任何其它所谓的高级编程语言用户鄙视。

View File

@ -68,7 +68,7 @@ func main() {
为了避免证书的传递过程中被篡改,可以通过一个安全可靠的根证书分别对服务器和客户端的证书进行签名。这样客户端或服务器在收到对方的证书后可以通过根证书进行验证证书的有效性。
根证书的生成方式和签名的自签名证书的生成方式类似:
根证书的生成方式和自签名证书的生成方式类似:
```
$ openssl genrsa -out ca.key 2048
@ -326,7 +326,7 @@ func filter(
}
```
不过GRPC框架中只能为每个服务设置一个截取器因此所有截取工作只能在一个函数中完成。开源的grpc-ecosystem项目中的go-grpc-middleware包已经基于GRPC对截取器实现了链式截取器的支持。
不过GRPC框架中只能为每个服务设置一个截取器因此所有截取工作只能在一个函数中完成。开源的grpc-ecosystem项目中的go-grpc-middleware包已经基于GRPC对截取器实现了链式截取器的支持。
以下是go-grpc-middleware包中链式截取器的简单用法

View File

@ -248,7 +248,7 @@ func main() {
}
```
首先通过runtime.NewServeMux()函数创建路由处理器然后通过RegisterRestServiceHandlerFromEndpoint函数将RestService服务相关的REST接口中转到后面的GRPC服务。grpc-gateway提供的runtime.ServeMux类也实现了http.Handler接口因此可以标准库中的相关函数配合使用。
首先通过runtime.NewServeMux()函数创建路由处理器然后通过RegisterRestServiceHandlerFromEndpoint函数将RestService服务相关的REST接口中转到后面的GRPC服务。grpc-gateway提供的runtime.ServeMux类也实现了http.Handler接口因此可以标准库中的相关函数配合使用。
档GRPC和REST服务全部启动之后就可以用curl请求REST服务了

View File

@ -119,7 +119,7 @@ err := validate.Struct(mystruct)
fmt.Println(err) // Key: 'RegisterReq.PasswordRepeat' Error:Field validation for 'PasswordRepeat' failed on the 'eqfield' tag
```
如果觉得这个 validator 提供的错误信息不够人性化,例如要把错误信息返回给用户,那就不应该直接显示英文了。可以针对每种 tag 进行错误信息制,读者可以自行探索。
如果觉得这个 validator 提供的错误信息不够人性化,例如要把错误信息返回给用户,那就不应该直接显示英文了。可以针对每种 tag 进行错误信息制,读者可以自行探索。
## 原理

View File

@ -6,7 +6,7 @@ ip+port 的组合往往被称为 endpoint。通过“订单服务”去找到这
## 为什么不把 ip+port 写死在自己的配置文件中
在大型网站的语境下,为了高可用每一个服务都一定会有多个节点来分担压力和风险,而现在随着 docker 之类的工具的盛行,依赖服务的节点是存在迁移的可能性的。所以对于依赖服务会慢慢不将其 ip+port 写死在自己的服务的配置文件中。否则在依赖服务迁移时,就需要跟着依赖服务修改配置,耗时费力。
在大型网站的语境下,为了高可用每一个服务都一定会有多个节点来分担压力和风险,而现在随着 docker 之类的工具的盛行,依赖服务的节点是存在迁移的可能性的。所以对于依赖服务会慢慢不将其 ip+port 写死在自己的服务的配置文件中。否则在依赖服务迁移时,就需要跟着依赖服务修改配置,耗时费力。
如果依赖服务所在的机器挂掉了,也就意味着那个 ip+port 不可用了。这时候上游还需要有某种反馈机制能够及时知晓,并且能够在知晓之后,不经过上线就能将已经失效的 ip+port 自动从依赖中摘除。
@ -69,14 +69,14 @@ redis-cli> sadd order_service.http 100.10.100.11:1002
## 故障节点摘除
上一小节讲的是存储的问题,在服务发现中,还有一个比较重要的命题,就是故障摘除。之所以开源界有很多服务发现的轮子,也正是因为这件事情并不是把 kv 映射存储下来这么简单。更重要的是我们能够在某个服务节点宕机时,能够让依赖该节点的其它服务感知得到这个“宕机”的变化,从而不再向其发送任何请求。
上一小节讲的是存储的问题,在服务发现中,还有一个比较重要的命题,就是故障摘除。之所以开源界有很多服务发现的轮子,也正是因为这件事情并不是把 kv 映射存储下来这么简单。更重要的是我们能够在某个服务节点宕机时,让依赖该节点的其它服务感知得到这个“宕机”的变化,从而不再向其发送任何请求。
故障摘除有两种思路:
1. 客户端主动的故障摘除
2. 客户端被动故障摘除。
主动的故障摘除是指,我作为依赖其人的上游,在下游一台机器挂掉的时候,我可以自己主动把它从依赖的节点列表里摘掉。常见的手段也有两种,一种是靠应用层心跳,还有一种靠请求投票。下面是一种根据请求时是否出错,对相应的服务节点进行投票的一个例子:
主动的故障摘除是指,我作为依赖其人的上游,在下游一台机器挂掉的时候,我可以自己主动把它从依赖的节点列表里摘掉。常见的手段也有两种,一种是靠应用层心跳,还有一种靠请求投票。下面是一种根据请求时是否出错,对相应的服务节点进行投票的一个例子:
```go
// 对下游的请求正常返回时:

View File

@ -129,7 +129,7 @@ func main() {
}
```
因为我们的逻辑限定每个 goroutine 只成功执行了 Lock 才会继续执行后续逻辑,因此在 Unlock 时可以保证 Lock struct 中的 channel 一定是空,从而不会阻塞,也不会失败。
因为我们的逻辑限定每个 goroutine 只成功执行了 Lock 才会继续执行后续逻辑,因此在 Unlock 时可以保证 Lock struct 中的 channel 一定是空,从而不会阻塞,也不会失败。
在单机系统中trylock 并不是一个好选择。因为大量的 goroutine 抢锁可能会导致 cpu 无意义的资源浪费。有一个专有名词用来描述这种抢锁的场景:活锁。
@ -221,7 +221,7 @@ current counter is 2028
unlock success!
```
通过代码和执行结果可以看到,我们远程调用 setnx 实际上和单机的 trylock 非常相似,如果获取锁失败,那么相关的任务逻辑就不应该继续向执行。
通过代码和执行结果可以看到,我们远程调用 setnx 实际上和单机的 trylock 非常相似,如果获取锁失败,那么相关的任务逻辑就不应该继续向执行。
setnx 很适合在高并发场景下,用来争抢一些“唯一”的资源。比如交易撮合系统中卖家发起订单,而多个买家会对其进行并发争抢。这种场景我们没有办法依赖具体的时间来判断先后,因为不管是用户设备的时间,还是分布式场景下的各台机器的时间,都是没有办法在合并后保证正确的时序的。哪怕是我们同一个机房的集群,不同的机器的系统时间可能也会有细微的差别。

View File

@ -1,6 +1,6 @@
# 6.2 分布式 id 生成器
有时我们需要能够生成类似 MySQL 自增 ID 这样不断增大,同时又不会重复的 id。以支持业务中的高并发场景。比较典型的电商促销时短时间内会有大量的订单涌入到系统比如每秒 10w+。明星出轨时,会有大量热情的粉丝发微薄以表心意,同样产生短时间大量的消息。
有时我们需要能够生成类似 MySQL 自增 ID 这样不断增大,同时又不会重复的 id。以支持业务中的高并发场景。比较典型的电商促销时短时间内会有大量的订单涌入到系统比如每秒 10w+。明星出轨时,会有大量热情的粉丝发微博以表心意,同样会在短时间内产生大量的消息。
在插入数据库之前,我们需要给这些消息/订单先打上一个 ID然后再插入到我们的数据库。对这个 id 的要求是希望其中能带有一些时间信息,这样即使我们后端的系统对消息进行了分库分表,也能够以时间顺序对这些消息进行排序。

View File

@ -24,7 +24,7 @@ elasticsearch 是开源分布式搜索引擎的霸主,其依赖于 Lucene 实
### 倒排列表
虽然 es 是针对搜索场景来制的,但如前文所言,实际应用中常常用 es 来作为 database 来使用,就是因为倒排列表的特性。可以用比较朴素的观点来理解倒排索引:
虽然 es 是针对搜索场景来制的,但如前文所言,实际应用中常常用 es 来作为 database 来使用,就是因为倒排列表的特性。可以用比较朴素的观点来理解倒排索引:
```
┌─────────────────┐ ┌─────────────┬─────────────┬─────────────┬─────────────┐
@ -458,4 +458,4 @@ select * from wms_orders where update_time >= date_sub(now(), interval 11 minute
由下游的 kafka 消费者负责把上游数据表的自增主键作为 es 的 document 的 id 进行写入,这样可以保证每次接收到 binlog 时,对应 id 的数据都被覆盖更新为最新。MySQL 的 row 格式的 binlog 会将每条记录的所有字段都提供给下游,所以实际上在向异构数据目标同步数据时,不需要考虑数据是插入还是更新,只要一律按 id 进行覆盖即可。
这种模式同样需要业务遵守一条数据表规范,即表中必须有唯一主键 id 来保证我们进入 es 的数据不会发生重复。一旦不遵守该规范,那么就会在同步时导致数据重复。当然,你也可以为每一张需要的表去制消费者的逻辑,这就不是通用系统讨论的范畴了。
这种模式同样需要业务遵守一条数据表规范,即表中必须有唯一主键 id 来保证我们进入 es 的数据不会发生重复。一旦不遵守该规范,那么就会在同步时导致数据重复。当然,你也可以为每一张需要的表去制消费者的逻辑,这就不是通用系统讨论的范畴了。

View File

@ -109,7 +109,7 @@ func shuffle(n int) []int {
本节中的场景是从 N 个节点中选择一个节点发送请求,初始请求结束之后,后续的请求会重新对数组洗牌,所以每两个请求之间没有什么关联关系。因此我们上面的洗牌算法,理论上不初始化随机库的种子也是不会出什么问题的。
但在一些特殊的场景下,例如使用 zk 时,客户端初始化从多个服务节点中挑选一个节点后,是会向该节点建立长连接的。并且之后如果有请求,也都会发送到该节点去。直到该节点不可用,才会在 endpoints 列表中挑选下一个节点。在这种场景下,我们的初始连接节点选择就要求必须是“真”随机了。否则,所客户端起动时,都会去连接同一个 zk 的实例,根本无法起到负载均衡的目的。如果在日常开发中,你的业务也是类似的场景,也务必考虑一下是否会发生类似的情况。为 rand 库设置种子的方法:
但在一些特殊的场景下,例如使用 zk 时,客户端初始化从多个服务节点中挑选一个节点后,是会向该节点建立长连接的。并且之后如果有请求,也都会发送到该节点去。直到该节点不可用,才会在 endpoints 列表中挑选下一个节点。在这种场景下,我们的初始连接节点选择就要求必须是“真”随机了。否则,所客户端起动时,都会去连接同一个 zk 的实例,根本无法起到负载均衡的目的。如果在日常开发中,你的业务也是类似的场景,也务必考虑一下是否会发生类似的情况。为 rand 库设置种子的方法:
```go
rand.Seed(time.Now().UnixNano())
@ -177,4 +177,4 @@ map[0:224436 1:128780 5:129310 6:129194 2:129643 3:129384 4:129253]
map[6:143275 5:143054 3:143584 2:143031 1:141898 0:142631 4:142527]
```
分布结果和我们推导出的论是一致的。
分布结果和我们推导出的论是一致的。

View File

@ -34,8 +34,8 @@
中国的Go语言社区是全球最大的Go语言社区我们不仅仅从一开始就始终紧跟着Go语言的发展脚步同时也为Go语言的发展作出了自己的巨大贡献。来自中国深圳的韦光京vcc.163@gmail.com在2010年前后关于MinGW的工作奠定了Go语言对Windows平台的支持同时也奠定了CGO对Windows平台的支持。同样来自中国的Minuxminux.ma@gmail.com则作为Go语言核心团队的成员他参与了大量的Go语言核心设计和开发评审工作。同时还有大量的国内Go语言爱好者积极参与了BUG的汇报和修复等工作作者也是其中之一
截至2018年中国出版的Go语言相关教程有近15本之多内容主要涵盖Go语言基础编程、Web编程、并发编程和内部源码剖析等诸多领域。但作为Go语言的资深用户作者关注的Go语言话题远远不止这些内容。其中CGO特性实现了Go语言对C语言和C++语言的支持使得Go语言可以无缝继承C/C++世界数十年来积累的巨大软件资产。Go汇编语言更是提供了直接访问底层机器指令的方法让我们可以无限压榨程序中热点代码的性能。目前国内互联网公司的新兴相互已经在逐渐向Go语言生态转移大型分布式系统的开发实战经验也是大家关心的技术。这些高阶或前沿特性都是作者和本书所关注的话题。
截至2018年中国出版的Go语言相关教程有近15本之多内容主要涵盖Go语言基础编程、Web编程、并发编程和内部源码剖析等诸多领域。但作为Go语言的资深用户作者关注的Go语言话题远远不止这些内容。其中CGO特性实现了Go语言对C语言和C++语言的支持使得Go语言可以无缝继承C/C++世界数十年来积累的巨大软件资产。Go汇编语言更是提供了直接访问底层机器指令的方法让我们可以无限压榨程序中热点代码的性能。目前国内互联网公司的新兴项目已经在逐渐向Go语言生态转移大型分布式系统的开发实战经验也是大家关心的。这些高阶或前沿特性都是作者和本书所关注的话题。
本书针对Go语言有一定经验想深入了解Go语言各种高级用法的开发人员。对于Go语言新手在阅读本书前建议先熟读D&K的[《The Go Programming Language》](https://gopl.io/)。最后希望这本书能够帮助大家更深入地了解Go语言。
本书针对有一定Go语言经验想深入了解Go语言各种高级用法的开发人员。对于Go语言新手在阅读本书前建议先熟读D&K的[《The Go Programming Language》](https://gopl.io/)。最后希望这本书能够帮助大家更深入地了解Go语言。
chai2010 - 2018年 8 月 于 武汉