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

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

This commit is contained in:
Xargin 2018-07-07 20:56:35 +08:00
commit b776a437bb
288 changed files with 1464 additions and 531 deletions

View File

@ -4,7 +4,7 @@
![](cover.png) ![](cover.png)
- 作者:柴树杉 (chai2010, chaishushan@gmail.com) - 作者:柴树杉 (chai2010, chaishushan@gmail.com), 曹春晖 (cch123, https://github.com/cch123)
- 网址https://github.com/chai2010/advanced-go-programming-book - 网址https://github.com/chai2010/advanced-go-programming-book
## 在线阅读 ## 在线阅读

View File

@ -34,12 +34,9 @@
* [第四章 RPC和Protobuf](ch4-rpc/readme.md) * [第四章 RPC和Protobuf](ch4-rpc/readme.md)
* [4.1. RPC入门](ch4-rpc/ch4-01-rpc-intro.md) * [4.1. RPC入门](ch4-rpc/ch4-01-rpc-intro.md)
* [4.2. Protobuf](ch4-rpc/ch4-02-pb-intro.md) * [4.2. Protobuf](ch4-rpc/ch4-02-pb-intro.md)
* [4.3. 玩转RPC(TODO)](ch4-rpc/ch4-03-netrpc-hack.md) * [4.3. 玩转RPC](ch4-rpc/ch4-03-netrpc-hack.md)
* [4.4. GRPC入门(TODO)](ch4-rpc/ch4-04-grpc.md) * [4.4. GRPC入门](ch4-rpc/ch4-04-grpc.md)
* [4.5. GRPC进阶(TODO)](ch4-rpc/ch4-05-grpc-hack.md) * [4.5. 补充说明](ch4-rpc/ch4-05-faq.md)
* [4.6. Protobuf扩展语法和插件(TODO)](ch4-rpc/ch4-06-pb-option.md)
* [4.7. 其它RPC系统(TODO)](ch4-rpc/ch4-07-other-rpc.md)
* [4.8. 补充说明(TODO)](ch4-rpc/ch4-08-faq.md)
* [第五章 Go和Web](ch5-web/readme.md) * [第五章 Go和Web](ch5-web/readme.md)
* [5.1. Web开发简介](ch5-web/ch5-01-introduction.md) * [5.1. Web开发简介](ch5-web/ch5-01-introduction.md)
* [5.2. Router请求路由](ch5-web/ch5-02-router.md) * [5.2. Router请求路由](ch5-web/ch5-02-router.md)

View File

@ -1,4 +1,4 @@
# 附录C作者简介 # 附录C作者简介
- [柴树杉网络IDchai2010](https://github.com/chai2010) 国内第一批Go语言爱好者创建了最早的QQ讨论组和golang-china邮件列表组织 [Go语言官方文档](https://github.com/golang-china) 和 [《Go语言圣经》](https://github.com/golang-china/gopl-zh) 的翻译工作Go语言代码的贡献者。目前在[青云QingCloud](https://www.qingcloud.com/)从事开源的多云应用管理平台[OpenPitrix](https://github.com/openpitrix/openpitrix)开发工作。 - [柴树杉网络IDchai2010](https://github.com/chai2010) 国内第一批Go语言爱好者创建了最早的QQ讨论组和golang-china邮件列表组织 [Go语言官方文档](https://github.com/golang-china) 和 [《Go语言圣经》](https://github.com/golang-china/gopl-zh) 的翻译工作Go语言代码的贡献者。目前在[青云QingCloud](https://www.qingcloud.com/)从事开源的多云应用管理平台[OpenPitrix](https://github.com/openpitrix/openpitrix)开发工作。
- [cch123](https://github.com/cch123): TODO - [曹春晖网络IDcch123](https://github.com/cch123) 在 web 领域工作多年,开源爱好者。对大型网站系统的架构和相关工具的实现很感兴趣,并且有一些研究成果。目前在滴滴平台技术部工作。

View File

@ -177,7 +177,7 @@ s1 := "hello, world"[:5]
s2 := "hello, world"[7:] s2 := "hello, world"[7:]
``` ```
和数组一样,内置的`len``cap`函数返回相同的结果,都对应字符串的长度。也可以通过`reflect.StringHeader`结构访问字符串的长度(这里只是为了演示字符串的结构,并不是推荐的做法): 字符串和数组类似,内置的`len`函数返回字符串的长度。也可以通过`reflect.StringHeader`结构访问字符串的长度(这里只是为了演示字符串的结构,并不是推荐的做法):
```go ```go
fmt.Println("len(s):", (*reflect.StringHeader)(unsafe.Pointer(&s)).Len) // 12 fmt.Println("len(s):", (*reflect.StringHeader)(unsafe.Pointer(&s)).Len) // 12

View File

@ -1,394 +1,394 @@
# 1.5. 面向并发的内存模型 # 1.5. 面向并发的内存模型
在早期CPU都是以单核的形式顺序执行机器指令。Go语言的祖先C语言正是这种顺序编程语言的代表。顺序编程语言中的顺序是指:所有的指令都是以串行的方式执行在相同的时刻有且仅有一个CPU在顺序执行程序的指令。 在早期CPU都是以单核的形式顺序执行机器指令。Go语言的祖先C语言正是这种顺序编程语言的代表。顺序编程语言中的顺序是指:所有的指令都是以串行的方式执行在相同的时刻有且仅有一个CPU在顺序执行程序的指令。
随着处理器技术的发展单核时代以提升处理器频率来提高运行效率的方式遇到了瓶颈目前各种主流的CPU频率基本被锁定在了3GHZ附近。单核CPU的发展的停滞给多核CPU的发展带来了机遇。相应地编程语言也开始逐步向并行化的方向发展。Go语言正是在多核和网络化的时代背景下诞生的原生支持并发的编程语言。 随着处理器技术的发展单核时代以提升处理器频率来提高运行效率的方式遇到了瓶颈目前各种主流的CPU频率基本被锁定在了3GHZ附近。单核CPU的发展的停滞给多核CPU的发展带来了机遇。相应地编程语言也开始逐步向并行化的方向发展。Go语言正是在多核和网络化的时代背景下诞生的原生支持并发的编程语言。
常见的并行编程有多种模型主要有多线程、消息传递等。从理论上来看多线程和基于消息的并发编程是等价的。由于多线程并发模型可以自然对应到多核的处理器主流的操作系统因此也都提供了系统级的多线程支持同时从概念上讲多线程似乎也更直观因此多线程编程模型逐步被吸纳到主流的编程语言特性或语言扩展库中。而主流编程语言对基于消息的并发编程模型支持则相比较少Erlang语言是支持基于消息传递并发编程模型的代表者它的并发体之间不共享内存。Go语言是基于消息并发模型的集大成者它将基于CSP模型的并发编程内置到了语言中通过一个go关键字就可以轻易地启动一个Goroutine与Erlang不同的是Go语言的Goroutine之间是共享内存的。 常见的并行编程有多种模型主要有多线程、消息传递等。从理论上来看多线程和基于消息的并发编程是等价的。由于多线程并发模型可以自然对应到多核的处理器主流的操作系统因此也都提供了系统级的多线程支持同时从概念上讲多线程似乎也更直观因此多线程编程模型逐步被吸纳到主流的编程语言特性或语言扩展库中。而主流编程语言对基于消息的并发编程模型支持则相比较少Erlang语言是支持基于消息传递并发编程模型的代表者它的并发体之间不共享内存。Go语言是基于消息并发模型的集大成者它将基于CSP模型的并发编程内置到了语言中通过一个go关键字就可以轻易地启动一个Goroutine与Erlang不同的是Go语言的Goroutine之间是共享内存的。
## Goroutine和系统线程 ## Goroutine和系统线程
Goroutine是Go语言特有的并发体是一种轻量级的线程由go关键字启动。在真实的Go语言的实现中goroutine和系统线程也不是等价的。尽管两者的区别实际上只是一个量的区别但正是这个量变引发了Go语言并发编程质的飞跃。 Goroutine是Go语言特有的并发体是一种轻量级的线程由go关键字启动。在真实的Go语言的实现中goroutine和系统线程也不是等价的。尽管两者的区别实际上只是一个量的区别但正是这个量变引发了Go语言并发编程质的飞跃。
首先每个系统级线程都会有一个固定大小的栈一般默认可能是2MB这个栈主要用来保存函数递归调用时参数和局部变量。固定了栈的大小这导致了两个问题一是对于很多只需要很小的栈空间的线程来说是一个巨大的浪费二是对于少数需要巨大栈空间的线程来说又面临栈溢出的风险。针对这两个问题的解决方案是要么降低固定的栈大小提升空间的利用率;要么增大栈的深度以允许更深的函数递归调用但这两者是没法同时兼得的。相反一个Goroutine会以一个很小的栈启动可能是2KB或4KB当遇到深度递归导致当前栈空间不足时Goroutine会根据需要动态地伸缩栈的大小主流实现中栈的最大值可达到1GB。因为启动的代价很小所以我们可以轻易地启动成千上万个Goroutine。 首先每个系统级线程都会有一个固定大小的栈一般默认可能是2MB这个栈主要用来保存函数递归调用时参数和局部变量。固定了栈的大小这导致了两个问题一是对于很多只需要很小的栈空间的线程来说是一个巨大的浪费二是对于少数需要巨大栈空间的线程来说又面临栈溢出的风险。针对这两个问题的解决方案是要么降低固定的栈大小提升空间的利用率;要么增大栈的深度以允许更深的函数递归调用但这两者是没法同时兼得的。相反一个Goroutine会以一个很小的栈启动可能是2KB或4KB当遇到深度递归导致当前栈空间不足时Goroutine会根据需要动态地伸缩栈的大小主流实现中栈的最大值可达到1GB。因为启动的代价很小所以我们可以轻易地启动成千上万个Goroutine。
Go的运行时还包含了其自己的调度器这个调度器使用了一些技术手段可以在n个操作系统线程上多工调度m个Goroutine。Go调度器的工作和内核的调度是相似的但是这个调度器只关注单独的Go程序中的Goroutine。Goroutine采用的是半抢占式的协作调度只有在当前Goroutine发生阻塞时才会导致调度同时发生在用户态调度器会根据具体函数只保存必要的寄存器切换的代价要比系统线程低得多。运行时有一个`runtime.GOMAXPROCS`变量用于控制当前运行正常非阻塞Goroutine的系统线程数目。 Go的运行时还包含了其自己的调度器这个调度器使用了一些技术手段可以在n个操作系统线程上多工调度m个Goroutine。Go调度器的工作和内核的调度是相似的但是这个调度器只关注单独的Go程序中的Goroutine。Goroutine采用的是半抢占式的协作调度只有在当前Goroutine发生阻塞时才会导致调度同时发生在用户态调度器会根据具体函数只保存必要的寄存器切换的代价要比系统线程低得多。运行时有一个`runtime.GOMAXPROCS`变量用于控制当前运行正常非阻塞Goroutine的系统线程数目。
在Go语言中启动一个Goroutine不仅和调用函数一样简单而且Goroutine之间调度代价也很低这些因素极大地促进了并发编程的流行和发展。 在Go语言中启动一个Goroutine不仅和调用函数一样简单而且Goroutine之间调度代价也很低这些因素极大地促进了并发编程的流行和发展。
## 原子操作 ## 原子操作
所谓的原子操作就是并发编程中“最小的且不可并行化”的操作。通常,有多个并发体对一个共享资源的操作是原子操作的话,同一时刻最多只能有一个并发体对该资源进行操作。从线程角度看,在当前线程修改共享资源期间,其它的线程是不能访问该资源的。原子操作对于多线程并发编程模型来说,不会发生有别于单线程的意外情况,共享资源的完整性可以得到保证。 所谓的原子操作就是并发编程中“最小的且不可并行化”的操作。通常,有多个并发体对一个共享资源的操作是原子操作的话,同一时刻最多只能有一个并发体对该资源进行操作。从线程角度看,在当前线程修改共享资源期间,其它的线程是不能访问该资源的。原子操作对于多线程并发编程模型来说,不会发生有别于单线程的意外情况,共享资源的完整性可以得到保证。
一般情况下原子操作都是通过“互斥”访问来保证访问的通常由特殊的CPU指令提供保护。当然如果仅仅是想模拟下粗粒度的原子操作我们可以借助于`sync.Mutex`来实现: 一般情况下原子操作都是通过“互斥”访问来保证访问的通常由特殊的CPU指令提供保护。当然如果仅仅是想模拟下粗粒度的原子操作我们可以借助于`sync.Mutex`来实现:
```go ```go
import ( import (
"sync" "sync"
) )
var total struct { var total struct {
sync.Mutex sync.Mutex
value int value int
} }
func worker(wg *sync.WaitGroup) { func worker(wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
for i := 0; i <= 100; i++ { for i := 0; i <= 100; i++ {
total.Lock() total.Lock()
total.value += i total.value += i
total.Unlock() total.Unlock()
} }
} }
func main() { func main() {
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(2) wg.Add(2)
go worker(&wg) go worker(&wg)
go worker(&wg) go worker(&wg)
wg.Wait() wg.Wait()
fmt.Println(total.value) fmt.Println(total.value)
} }
``` ```
`worker`的循环中,为了保证`total.value += i`的原子性,我们通过`sync.Mutex`加锁和解锁来保证该语句在同一时刻只被一个线程访问。对于多线程模型的程序而言,进出临界区前后进行加锁和解锁都是必须的。如果没有锁的保护,`total`的最终值将由于多线程之间的竞争而可能会不正确。 `worker`的循环中,为了保证`total.value += i`的原子性,我们通过`sync.Mutex`加锁和解锁来保证该语句在同一时刻只被一个线程访问。对于多线程模型的程序而言,进出临界区前后进行加锁和解锁都是必须的。如果没有锁的保护,`total`的最终值将由于多线程之间的竞争而可能会不正确。
用互斥锁来保护一个数值型的共享资源,麻烦且效率低下。标准库的`sync/atomic`包对原子操作提供了丰富的支持。我们可以重新实现上面的例子: 用互斥锁来保护一个数值型的共享资源,麻烦且效率低下。标准库的`sync/atomic`包对原子操作提供了丰富的支持。我们可以重新实现上面的例子:
```go ```go
import ( import (
"sync" "sync"
"sync/atomic" "sync/atomic"
) )
var total uint64 var total uint64
func worker(wg *sync.WaitGroup) { func worker(wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
var i uint64 var i uint64
for i = 0; i <= 100; i++ { for i = 0; i <= 100; i++ {
atomic.AddUint64(&total, i) atomic.AddUint64(&total, i)
} }
} }
func main() { func main() {
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(2) wg.Add(2)
go worker(&wg) go worker(&wg)
go worker(&wg) go worker(&wg)
wg.Wait() wg.Wait()
} }
``` ```
`atomic.AddUint64`函数调用保证了`total`的读取、更新和保存是一个原子操作,因此在多线程中访问也是安全的。 `atomic.AddUint64`函数调用保证了`total`的读取、更新和保存是一个原子操作,因此在多线程中访问也是安全的。
原子操作配合互斥锁可以实现非常高效的单件模式。互斥锁的代价比普通整数的原子读写高很多,在性能敏感的地方可以增加一个数字型的标志位,通过原子检测标志位状态降低互斥锁的使用次数来提高性能。 原子操作配合互斥锁可以实现非常高效的单件模式。互斥锁的代价比普通整数的原子读写高很多,在性能敏感的地方可以增加一个数字型的标志位,通过原子检测标志位状态降低互斥锁的使用次数来提高性能。
```go ```go
type singleton struct {} type singleton struct {}
var ( var (
instance *singleton instance *singleton
initialized uint32 initialized uint32
mu sync.Mutex mu sync.Mutex
) )
func Instance() *singleton { func Instance() *singleton {
if atomic.LoadUint32(&initialized) == 1 { if atomic.LoadUint32(&initialized) == 1 {
return instance return instance
} }
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
if instance == nil { if instance == nil {
defer atomic.StoreUint32(&initialized, 1) defer atomic.StoreUint32(&initialized, 1)
instance = &singleton{} instance = &singleton{}
} }
return instance return instance
} }
``` ```
我们可以将通用的代码提取出来,就成了标准库中`sync.Once`的实现: 我们可以将通用的代码提取出来,就成了标准库中`sync.Once`的实现:
```go ```go
type Once struct { type Once struct {
m Mutex m Mutex
done uint32 done uint32
} }
func (o *Once) Do(f func()) { func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 1 { if atomic.LoadUint32(&o.done) == 1 {
return return
} }
o.m.Lock() o.m.Lock()
defer o.m.Unlock() defer o.m.Unlock()
if o.done == 0 { if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1) defer atomic.StoreUint32(&o.done, 1)
f() f()
} }
} }
``` ```
基于`sync.Once`重新实现单件模式: 基于`sync.Once`重新实现单件模式:
```go ```go
var ( var (
instance *singleton instance *singleton
once sync.Once once sync.Once
) )
func Instance() *singleton { func Instance() *singleton {
once.Do(func() { once.Do(func() {
instance = &singleton{} instance = &singleton{}
}) })
return instance return instance
} }
``` ```
`sync/atomic`包对基本的数值类型及复杂对象的读写都提供了原子操作的支持。`atomic.Value`原子对象提供了`Load``Store`两个原子方法,分别用于加载和保存数据,返回值和参数都是`interface{}`类型,因此可以用于任意的自定义复杂类型。 `sync/atomic`包对基本的数值类型及复杂对象的读写都提供了原子操作的支持。`atomic.Value`原子对象提供了`Load``Store`两个原子方法,分别用于加载和保存数据,返回值和参数都是`interface{}`类型,因此可以用于任意的自定义复杂类型。
```go ```go
var config atomic.Value // 保存当前配置信息 var config atomic.Value // 保存当前配置信息
// 初始化配置信息 // 初始化配置信息
config.Store(loadConfig()) config.Store(loadConfig())
// 启动一个后台线程, 加载更新后的配置信息 // 启动一个后台线程, 加载更新后的配置信息
go func() { go func() {
for { for {
time.Sleep(time.Second) time.Sleep(time.Second)
config.Store(loadConfig()) config.Store(loadConfig())
} }
}() }()
// 用于处理请求的工作者线程始终采用最新的配置信息 // 用于处理请求的工作者线程始终采用最新的配置信息
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
go func() { go func() {
for r := range requests() { for r := range requests() {
c := config.Load() c := config.Load()
// ... // ...
} }
}() }()
} }
``` ```
这是一个简化的生产者、消费者模型:后台线程生成最新的配置信息;前台多个工作者线程获取最新的配置信息。所有线程共享配置信息资源。 这是一个简化的生产者、消费者模型:后台线程生成最新的配置信息;前台多个工作者线程获取最新的配置信息。所有线程共享配置信息资源。
## 顺序一致性内存模型 ## 顺序一致性内存模型
如果只是想简单地在线程之间进行数据同步的话,原子操作已经为编程人员提供了一些同步保障。不过这种保障有一个前提:顺序一致性的内存模型。要了解顺序一致性,我们先看看一个简单的例子: 如果只是想简单地在线程之间进行数据同步的话,原子操作已经为编程人员提供了一些同步保障。不过这种保障有一个前提:顺序一致性的内存模型。要了解顺序一致性,我们先看看一个简单的例子:
```go ```go
var a string var a string
var done bool var done bool
func setup() { func setup() {
a = "hello, world" a = "hello, world"
done = true done = true
} }
func main() { func main() {
go setup() go setup()
for !done {} for !done {}
print(a) print(a)
} }
``` ```
我们创建了`setup`线程,用于对字符串`a`的初始化工作,初始化完成之后设置`done`标志为`true``main`函数所在的主线程中,通过`for !done {}`检测`done`变为`true`时,认为字符串初始化工作完成,然后进行字符串的打印工作。 我们创建了`setup`线程,用于对字符串`a`的初始化工作,初始化完成之后设置`done`标志为`true``main`函数所在的主线程中,通过`for !done {}`检测`done`变为`true`时,认为字符串初始化工作完成,然后进行字符串的打印工作。
但是Go语言并不保证在`main`函数中观测到的对`done`的写入操作发生在对字符串`a`的写入的操作之后,因此程序很可能打印一个空字符串。更糟糕的是,因为两个线程之间没有同步事件,`setup`线程对`done`的写入操作甚至无法被`main`线程看到,`main`函数有可能陷入死循环中。 但是Go语言并不保证在`main`函数中观测到的对`done`的写入操作发生在对字符串`a`的写入的操作之后,因此程序很可能打印一个空字符串。更糟糕的是,因为两个线程之间没有同步事件,`setup`线程对`done`的写入操作甚至无法被`main`线程看到,`main`函数有可能陷入死循环中。
在Go语言中同一个Goroutine线程内部顺序一致性内存模型是得到保证的。但是不同的Goroutine之间并不满足顺序一致性内存模型需要通过明确定义的同步事件来作为同步的参考。如果两个事件不可排序那么就说这两个事件是并发的。为了最大化并行Go语言的编译器和处理器在不影响上述规定的前提下可能会对执行语句重新排序CPU也会对一些指令进行乱序执行 在Go语言中同一个Goroutine线程内部顺序一致性内存模型是得到保证的。但是不同的Goroutine之间并不满足顺序一致性内存模型需要通过明确定义的同步事件来作为同步的参考。如果两个事件不可排序那么就说这两个事件是并发的。为了最大化并行Go语言的编译器和处理器在不影响上述规定的前提下可能会对执行语句重新排序CPU也会对一些指令进行乱序执行
因此如果在一个Goroutine中顺序执行`a = 1; b = 2;`两个语句虽然在当前的Goroutine中可以认为`a = 1;`语句先于`b = 2;`语句执行但是在另一个Goroutine中`b = 2;`语句可能会先于`a = 1;`语句执行甚至在另一个Goroutine中无法看到它们的变化可能始终在寄存器中。也就是说在另一个Goroutine看来, `a = 1; b = 2;`两个语句的执行顺序是不确定的。如果一个并发程序无法确定事件的偏序关系,那么程序的运行结果往往会有不确定的结果。比如下面这个程序: 因此如果在一个Goroutine中顺序执行`a = 1; b = 2;`两个语句虽然在当前的Goroutine中可以认为`a = 1;`语句先于`b = 2;`语句执行但是在另一个Goroutine中`b = 2;`语句可能会先于`a = 1;`语句执行甚至在另一个Goroutine中无法看到它们的变化可能始终在寄存器中。也就是说在另一个Goroutine看来, `a = 1; b = 2;`两个语句的执行顺序是不确定的。如果一个并发程序无法确定事件的偏序关系,那么程序的运行结果往往会有不确定的结果。比如下面这个程序:
```go ```go
func main() { func main() {
go println("你好, 世界") go println("你好, 世界")
} }
``` ```
根据Go语言规范`main`函数退出时程序结束不会等待任何后台线程。因为Goroutine的执行和`main`函数的返回事件是并发的,谁都有可能先发生,所以什么时候打印,能否打印都是未知的。 根据Go语言规范`main`函数退出时程序结束不会等待任何后台线程。因为Goroutine的执行和`main`函数的返回事件是并发的,谁都有可能先发生,所以什么时候打印,能否打印都是未知的。
用前面的原子操作并不能解决问题,因为我们无法确定两个原子操作之间的顺序。解决问题的办法就是通过同步原语来给两个事件明确排序: 用前面的原子操作并不能解决问题,因为我们无法确定两个原子操作之间的顺序。解决问题的办法就是通过同步原语来给两个事件明确排序:
```go ```go
func main() { func main() {
done := make(chan int) done := make(chan int)
go func(){ go func(){
println("你好, 世界") println("你好, 世界")
done <- 1 done <- 1
}() }()
<-done <-done
} }
``` ```
`<-done`执行时,必然要求`done <- 1`也已经执行。根据同一个Gorouine依然满足顺序一致性规则我们可以判断当`done <- 1`执行时,`println("你好, 世界")`语句必然已经执行完成了。因此,现在的程序确保可以正常打印结果。 `<-done`执行时,必然要求`done <- 1`也已经执行。根据同一个Gorouine依然满足顺序一致性规则我们可以判断当`done <- 1`执行时,`println("你好, 世界")`语句必然已经执行完成了。因此,现在的程序确保可以正常打印结果。
当然,通过`sync.Mutex`互斥量也是可以实现同步的: 当然,通过`sync.Mutex`互斥量也是可以实现同步的:
```go ```go
func main() { func main() {
var mu sync.Mutex var mu sync.Mutex
mu.Lock() mu.Lock()
go func(){ go func(){
println("你好, 世界") println("你好, 世界")
mu.Unock() mu.Unlock()
}() }()
mu.Lock() mu.Lock()
} }
``` ```
可以确定后台线程的`mu.Unock()`必然在`println("你好, 世界")`完成后发生(同一个线程满足顺序一致性),`main`函数的第二个`mu.Lock()`必然在后台线程的`mu.Unock()`之后发生(`sync.Mutex`保证),此时后台线程的打印工作已经顺利完成了。 可以确定后台线程的`mu.Unlock()`必然在`println("你好, 世界")`完成后发生(同一个线程满足顺序一致性),`main`函数的第二个`mu.Lock()`必然在后台线程的`mu.Unlock()`之后发生(`sync.Mutex`保证),此时后台线程的打印工作已经顺利完成了。
## 初始化顺序 ## 初始化顺序
前面函数章节中我们已经简单介绍过程序的初始化顺序这是属于Go语言面向并发的内存模型的基础规范。 前面函数章节中我们已经简单介绍过程序的初始化顺序这是属于Go语言面向并发的内存模型的基础规范。
Go程序的初始化和执行总是从`main.main`函数开始的。但是如果`main`包里导入了其它的包,则会按照顺序将它们包含进`main`包里(这里的导入顺序依赖具体实现,一般可能是以文件名或包路径名的字符串顺序导入)。如果某个包被多次导入的话,在执行的时候只会导入一次。当一个包被导入时,如果它还导入了其它的包,则先将其它的包包含进来,然后创建和初始化这个包的常量和变量。然后就是调用包里的`init`函数,如果一个包有多个`init`函数的话,实现可能是以文件名的顺序调用,同一个文件内的多个`init`则是以出现的顺序依次调用(`init`不是普通函数,可以定义有多个,所以不能被其它函数调用)。最终,在`main`包的所有包常量、包变量被创建和初始化,并且`init`函数被执行后,才会进入`main.main`函数程序开始正常执行。下图是Go程序函数启动顺序的示意图 Go程序的初始化和执行总是从`main.main`函数开始的。但是如果`main`包里导入了其它的包,则会按照顺序将它们包含进`main`包里(这里的导入顺序依赖具体实现,一般可能是以文件名或包路径名的字符串顺序导入)。如果某个包被多次导入的话,在执行的时候只会导入一次。当一个包被导入时,如果它还导入了其它的包,则先将其它的包包含进来,然后创建和初始化这个包的常量和变量。然后就是调用包里的`init`函数,如果一个包有多个`init`函数的话,实现可能是以文件名的顺序调用,同一个文件内的多个`init`则是以出现的顺序依次调用(`init`不是普通函数,可以定义有多个,所以不能被其它函数调用)。最终,在`main`包的所有包常量、包变量被创建和初始化,并且`init`函数被执行后,才会进入`main.main`函数程序开始正常执行。下图是Go程序函数启动顺序的示意图
![](../images/ch1-04-init.png) ![](../images/ch1-04-init.png)
要注意的是,在`main.main`函数执行之前所有代码都运行在同一个goroutine中也是运行在程序的主系统线程中。如果某个`init`函数内部用go关键字启动了新的goroutine的话新的goroutine只有在进入`main.main`函数之后才可能被执行到。 要注意的是,在`main.main`函数执行之前所有代码都运行在同一个goroutine中也是运行在程序的主系统线程中。如果某个`init`函数内部用go关键字启动了新的goroutine的话新的goroutine只有在进入`main.main`函数之后才可能被执行到。
因为所有的`init`函数和`main`函数都是在主线程完成,它们也是满足顺序一致性模型的。 因为所有的`init`函数和`main`函数都是在主线程完成,它们也是满足顺序一致性模型的。
## Goroutine的创建 ## Goroutine的创建
`go`语句会在当前Goroutine对应函数返回前创建新的Goroutine. 例如: `go`语句会在当前Goroutine对应函数返回前创建新的Goroutine. 例如:
```go ```go
var a string var a string
func f() { func f() {
print(a) print(a)
} }
func hello() { func hello() {
a = "hello, world" a = "hello, world"
go f() go f()
} }
``` ```
执行`go f()`语句创建Goroutine和`hello`函数是在同一个Goroutine中执行, 根据语句的书写顺序可以确定Goroutine的创建发生在`hello`函数返回之前, 但是新创建Goroutine对应的`f()`的执行事件和`hello`函数返回的事件则是不可排序的,也就是并发的。调用`hello`可能会在将来的某一时刻打印`"hello, world"`,也很可能是在`hello`函数执行完成后才打印。 执行`go f()`语句创建Goroutine和`hello`函数是在同一个Goroutine中执行, 根据语句的书写顺序可以确定Goroutine的创建发生在`hello`函数返回之前, 但是新创建Goroutine对应的`f()`的执行事件和`hello`函数返回的事件则是不可排序的,也就是并发的。调用`hello`可能会在将来的某一时刻打印`"hello, world"`,也很可能是在`hello`函数执行完成后才打印。
## 基于Channel的通信 ## 基于Channel的通信
Channel通信是在Goroutine之间进行同步的主要方法。在无缓存的Channel上的每一次发送操作都有与其对应的接收操作相配对发送和接收操作通常发生在不同的Goroutine上在同一个Goroutine上执行2个操作很容易导致死锁。**无缓存的Channel上的发送操作总在对应的接收操作完成前发生.** Channel通信是在Goroutine之间进行同步的主要方法。在无缓存的Channel上的每一次发送操作都有与其对应的接收操作相配对发送和接收操作通常发生在不同的Goroutine上在同一个Goroutine上执行2个操作很容易导致死锁。**无缓存的Channel上的发送操作总在对应的接收操作完成前发生.**
```go ```go
var done = make(chan bool) var done = make(chan bool)
var msg string var msg string
func aGoroutine() { func aGoroutine() {
msg = "你好, 世界" msg = "你好, 世界"
done <- true done <- true
} }
func main() { func main() {
go aGoroutine() go aGoroutine()
<-done <-done
println(msg) println(msg)
} }
``` ```
可保证打印出“hello, world”。该程序首先对`msg`进行写入,然后在`done`管道上发送同步信号,随后从`done`接收对应的同步信号,最后执行`println`函数。 可保证打印出“hello, world”。该程序首先对`msg`进行写入,然后在`done`管道上发送同步信号,随后从`done`接收对应的同步信号,最后执行`println`函数。
若在关闭信道后继续从中接收数据,接收者就会收到该信道返回的零值。因此在这个例子中,用`close(c)`关闭管道代替`done <- false`依然能保证该程序产生相同的行为。 若在关闭信道后继续从中接收数据,接收者就会收到该信道返回的零值。因此在这个例子中,用`close(c)`关闭管道代替`done <- false`依然能保证该程序产生相同的行为。
```go ```go
var done = make(chan bool) var done = make(chan bool)
var msg string var msg string
func aGoroutine() { func aGoroutine() {
msg = "你好, 世界" msg = "你好, 世界"
close(done) close(done)
} }
func main() { func main() {
go aGoroutine() go aGoroutine()
<-done <-done
println(msg) println(msg)
} }
``` ```
**对于从无缓冲信道进行的接收,发生在对该信道进行的发送完成之前。** **对于从无缓冲信道进行的接收,发生在对该信道进行的发送完成之前。**
基于上面这个规则可知交换两个Goroutine中的接收和发送操作也是可以的但是很危险 基于上面这个规则可知交换两个Goroutine中的接收和发送操作也是可以的但是很危险
```go ```go
var done = make(chan bool) var done = make(chan bool)
var msg string var msg string
func aGoroutine() { func aGoroutine() {
msg = "hello, world" msg = "hello, world"
<-done <-done
} }
func main() { func main() {
go aGoroutine() go aGoroutine()
done <- true done <- true
println(msg) println(msg)
} }
``` ```
也可保证打印出“hello, world”。因为`main`线程中`done <- true`发送完成前,后台线程`<-done`接收已经开始,这保证`msg = "hello, world"`被执行了,所以之后`println(msg)`的msg已经被赋值过了。简而言之后台线程首先对`msg`进行写入,然后从`done`中接收信号,随后`main`线程向`done`发送对应的信号,最后执行`println`函数完成。但是,若该信道为带缓冲的(例如,`done = make(chan bool, 1)``main`线程的`done <- true`接收操作将不会被后台线程的`<-done`接收操作阻塞该程序将无法保证打印出“hello, world”。 也可保证打印出“hello, world”。因为`main`线程中`done <- true`发送完成前,后台线程`<-done`接收已经开始,这保证`msg = "hello, world"`被执行了,所以之后`println(msg)`的msg已经被赋值过了。简而言之后台线程首先对`msg`进行写入,然后从`done`中接收信号,随后`main`线程向`done`发送对应的信号,最后执行`println`函数完成。但是,若该信道为带缓冲的(例如,`done = make(chan bool, 1)``main`线程的`done <- true`接收操作将不会被后台线程的`<-done`接收操作阻塞该程序将无法保证打印出“hello, world”。
对于带缓冲的Channel**对于Channel的第`K`个接收完成操作发生在第`K+C`个发送操作完成之前,其中`C`是Channel的缓存大小。** 如果将`C`设置为0自然就对应无缓存的Channel也即使第K个接收完成在第K个发送完成之前。因为无缓存的Channel只能同步发1个也就简化为前面无缓存Channel的规则**对于从无缓冲信道进行的接收,发生在对该信道进行的发送完成之前。** 对于带缓冲的Channel**对于Channel的第`K`个接收完成操作发生在第`K+C`个发送操作完成之前,其中`C`是Channel的缓存大小。** 如果将`C`设置为0自然就对应无缓存的Channel也即使第K个接收完成在第K个发送完成之前。因为无缓存的Channel只能同步发1个也就简化为前面无缓存Channel的规则**对于从无缓冲信道进行的接收,发生在对该信道进行的发送完成之前。**
我们可以根据控制Channel的缓存大小来控制并发执行的Goroutine的最大数目, 例如: 我们可以根据控制Channel的缓存大小来控制并发执行的Goroutine的最大数目, 例如:
```go ```go
var limit = make(chan int, 3) var limit = make(chan int, 3)
func main() { func main() {
for _, w := range work { for _, w := range work {
go func() { go func() {
limit <- 1 limit <- 1
w() w()
<-limit <-limit
}() }()
} }
select{} select{}
} }
``` ```
最后一句`select{}`是一个空的管道选择语句,该语句会导致`main`线程阻塞,从而避免程序过早退出。还有`for{}``<-make(chan int)`等诸多方法可以达到类似的效果。因为`main`线程被阻塞了,如果需要程序正常退出的话可以通过调用`os.Exit(0)`实现。 最后一句`select{}`是一个空的管道选择语句,该语句会导致`main`线程阻塞,从而避免程序过早退出。还有`for{}``<-make(chan int)`等诸多方法可以达到类似的效果。因为`main`线程被阻塞了,如果需要程序正常退出的话可以通过调用`os.Exit(0)`实现。
## 不靠谱的同步 ## 不靠谱的同步
前面我们已经分析过,下面代码无法保证正常打印结果。实际的运行效果也是大概率不能正常输出结果。 前面我们已经分析过,下面代码无法保证正常打印结果。实际的运行效果也是大概率不能正常输出结果。
```go ```go
func main() { func main() {
go println("你好, 世界") go println("你好, 世界")
} }
``` ```
刚接触Go语言的话可能希望通过加入一个随机的休眠时间来保证正常的输出 刚接触Go语言的话可能希望通过加入一个随机的休眠时间来保证正常的输出
```go ```go
func main() { func main() {
go println("hello, world") go println("hello, world")
time.Sleep(time.Second) time.Sleep(time.Second)
} }
``` ```
因为主线程休眠了1秒钟因此这个程序大概率是可以正常输出结果的。因此很多人会觉得这个程序已经没有问题了。但是这个程序是不稳健的依然有失败的可能性。我们先假设程序是可以稳定输出结果的。因为Go线程的启动是非阻塞的`main`线程显式休眠了1秒钟退出导致程序结束我们可以近似地认为程序总共执行了1秒多时间。现在假设`println`函数内部实现休眠的时间大于`main`线程休眠的时间的话,就会导致矛盾:后台线程既然先于`main`线程完成打印,那么执行时间肯定是小于`main`线程执行时间的。当然这是不可能的。 因为主线程休眠了1秒钟因此这个程序大概率是可以正常输出结果的。因此很多人会觉得这个程序已经没有问题了。但是这个程序是不稳健的依然有失败的可能性。我们先假设程序是可以稳定输出结果的。因为Go线程的启动是非阻塞的`main`线程显式休眠了1秒钟退出导致程序结束我们可以近似地认为程序总共执行了1秒多时间。现在假设`println`函数内部实现休眠的时间大于`main`线程休眠的时间的话,就会导致矛盾:后台线程既然先于`main`线程完成打印,那么执行时间肯定是小于`main`线程执行时间的。当然这是不可能的。
严谨的并发程序的正确性不应该是依赖于CPU的执行速度和休眠时间等不靠谱的因素的。严谨的并发也应该是可以静态推导出结果的根据线程内顺序一致性结合Channel或`sync`同步事件的可排序性来推导,最终完成各个线程各段代码的偏序关系排序。如果两个事件无法根据此规则来排序,那么它们就是并发的,也就是执行先后顺序不可靠的。 严谨的并发程序的正确性不应该是依赖于CPU的执行速度和休眠时间等不靠谱的因素的。严谨的并发也应该是可以静态推导出结果的根据线程内顺序一致性结合Channel或`sync`同步事件的可排序性来推导,最终完成各个线程各段代码的偏序关系排序。如果两个事件无法根据此规则来排序,那么它们就是并发的,也就是执行先后顺序不可靠的。
解决同步问题的思路是相同的:使用显式的同步。 解决同步问题的思路是相同的:使用显式的同步。

View File

@ -66,7 +66,7 @@ func CopyFile(dstName, srcName string) (written int64, err error) {
} }
``` ```
上面的代码虽然能够工作但是隐藏一个bug。如果第一个`os.Open`调用失败,那么会在没有释放`src`文件资源的情况下返回。虽然我们可以通过在第二个返回语句前添加`src.Close()`调用来修复这个BUG但是当代码变得复杂时类似的问题将很难被发现和修复。我们可以通过`defer`语句来确保每个被正常打开的文件都能被正常关闭: 上面的代码虽然能够工作但是隐藏一个bug。如果第一个`os.Open`调用成功,但是第二个`os.Create`调用失败,那么会在没有释放`src`文件资源的情况下返回。虽然我们可以通过在第二个返回语句前添加`src.Close()`调用来修复这个BUG但是当代码变得复杂时类似的问题将很难被发现和修复。我们可以通过`defer`语句来确保每个被正常打开的文件都能被正常关闭:
```go ```go
func CopyFile(dstName, srcName string) (written int64, err error) { func CopyFile(dstName, srcName string) (written int64, err error) {

View File

@ -152,7 +152,7 @@ DATA ·Name+8(SB)/8,$6
因为在Go汇编语言中go.string."gopher"不是一个合法的符号我们无法手工创建这是给编译器保留的部分特权因为手工创建类似符号可能打破编译器输出代码的某些规则。因此我们新创建了一个·NameData符号表示底层的字符串数据。 因为在Go汇编语言中go.string."gopher"不是一个合法的符号我们无法手工创建这是给编译器保留的部分特权因为手工创建类似符号可能打破编译器输出代码的某些规则。因此我们新创建了一个·NameData符号表示底层的字符串数据。
然后定义·Name符号为两个16字节其中前8个字节用·NameData符号对应的地址初始化后8个字节为常量6表示字符串长度。 然后定义·Name符号内存大小为16字节其中前8个字节用·NameData符号对应的地址初始化后8个字节为常量6表示字符串长度。
通过以下代码测试输出Name变量 通过以下代码测试输出Name变量
@ -172,7 +172,7 @@ func main() {
pkgpath.NameData: missing Go //type information for global symbol: size 8 pkgpath.NameData: missing Go //type information for global symbol: size 8
``` ```
提示汇编中定义的NameData符号没有类型信息。其实Go汇编语言中定义的数据并没有所谓的类型每个符号只不过是对应一个内存而且。出现这种错误的原因是Go语言的垃圾回收器在扫描NameData变量的时候无法知晓该变量内部是否包含指针。因此真正错误的原因并不是NameData没有类型是NameData变量没有标注是否会含有指针信息。 提示汇编中定义的NameData符号没有类型信息。其实Go汇编语言中定义的数据并没有所谓的类型每个符号只不过是对应一块内存而已。出现这种错误的原因是Go语言的垃圾回收器在扫描NameData变量的时候无法知晓该变量内部是否包含指针。因此真正错误的原因并不是NameData没有类型是NameData变量没有标注是否会含有指针信息。
通过给NameData变量增加一个标志表示其中不会包含指针数据可以修复该错误 通过给NameData变量增加一个标志表示其中不会包含指针数据可以修复该错误
@ -193,9 +193,9 @@ var NameData [8]byte
var Name string var Name string
``` ```
我们将NameData声明为长度为8的字节数组。因为编译器可以通过类型分析出该变量不会包含指针因此汇编代码中可以NOPTR标志信息 我们将NameData声明为长度为8的字节数组。编译器可以通过类型分析出该变量不会包含指针因此汇编代码中可以省略NOPTR标志。
在这个实现中Name字符串底层其实引用的是NameData内存对应的“gopher”字符串数据。因此如果NameData发生变化的化Name字符串的数据也会跟着变化 在这个实现中Name字符串底层其实引用的是NameData内存对应的“gopher”字符串数据。因此如果NameData发生变化Name字符串的数据也会跟着变化。
```go ```go
func main() { func main() {
@ -218,7 +218,7 @@ DATA ·Name+8(SB)/8,$6
DATA ·Name+16(SB)/8,$"gopher" DATA ·Name+16(SB)/8,$"gopher"
``` ```
在新的结构中Name符号对应的内存从16字节变为24字节多出的8个字节用户存放底层的“gopher”字符串。·Name符号前16个字节依然对应reflect.StringHeader结构体Data部分对应`$·Name+16(SB)`表示数据的地址为Name符号往后偏移16个字节的位置Len部分依然对应6个字节的长度。 在新的结构中Name符号对应的内存从16字节变为24字节多出的8个字节存放底层的“gopher”字符串。·Name符号前16个字节依然对应reflect.StringHeader结构体Data部分对应`$·Name+16(SB)`表示数据的地址为Name符号往后偏移16个字节的位置Len部分依然对应6个字节的长度。
## 定义main函数 ## 定义main函数
@ -245,7 +245,7 @@ TEXT ·main(SB), $16-0
CALL runtime·printnl(SB) CALL runtime·printnl(SB)
RET RET
``` ```
`TEXT ·main(SB), $16-0`用于定义`main`函数,其中`$16-0`表示`main`函数的帧大小是16个字节对应string头的大小用于给`runtime·printstring`函数传递参数),`0`表示`main`函数没有参数和返回值。`main`函数内部通过调用运行时内部的`runtime·printstring(SB)`函数来打印字符串。然后调用runtime·printnl打印换行符号。 `TEXT ·main(SB), $16-0`用于定义`main`函数,其中`$16-0`表示`main`函数的帧大小是16个字节对应string头部结构体的大小,用于给`runtime·printstring`函数传递参数),`0`表示`main`函数没有参数和返回值。`main`函数内部通过调用运行时内部的`runtime·printstring(SB)`函数来打印字符串。然后调用`runtime·printnl`打印换行符号。
Go语言函数在函数调用时完全通过栈传递调用参数和返回值。先通过MOVQ指令将helloworld对应的字符串头部结构体的16个字节复制到栈指针SP对应的16字节的空间然后通过CALL指令调用对应函数。最后使用RET指令表示当前函数返回。 Go语言函数在函数调用时完全通过栈传递调用参数和返回值。先通过MOVQ指令将helloworld对应的字符串头部结构体的16个字节复制到栈指针SP对应的16字节的空间然后通过CALL指令调用对应函数。最后使用RET指令表示当前函数返回。
@ -254,7 +254,7 @@ Go语言函数在函数调用时完全通过栈传递调用参数和返回值
Go语言函数或方法符号在编译为目标文件后目标文件中的每个符号均包含对应包的绝对导入路径。因此目标文件的符号可能非常复杂比如“path/to/pkg.(*SomeType).SomeMethod”或“go.string."abc"”。目标文件的符号名中不仅仅包含普通的字母还可能包含诸多特殊字符。而Go语言的汇编器是从plan9移植过来的二把刀并不能处理这些特殊的字符导致了用Go汇编语言手工实现Go诸多特性时遇到种种限制。 Go语言函数或方法符号在编译为目标文件后目标文件中的每个符号均包含对应包的绝对导入路径。因此目标文件的符号可能非常复杂比如“path/to/pkg.(*SomeType).SomeMethod”或“go.string."abc"”。目标文件的符号名中不仅仅包含普通的字母还可能包含诸多特殊字符。而Go语言的汇编器是从plan9移植过来的二把刀并不能处理这些特殊的字符导致了用Go汇编语言手工实现Go诸多特性时遇到种种限制。
Go汇编语言同样遵循Go语言少即是多的哲学它只保留了最基本的特性定义变量和全局函数。同时为了简化Go汇编器的词法扫描程序的实现特别引入了Unicode中的中点`·`和大写的除法`/`对应的Unicode码点为`U+00B7``U+2215`。汇编器编译后,中点`·`会被替换为ASCII中的点“.”,大写除法会被替换为ASCII码中的除法“/”,比如`math/rand·Int`会被替换为`math/rand.Int`。这样可以将点和浮点数中的小数点、大写的除法和表达式中的除法符号分开,可以简化汇编程序法分析部分的实现。 Go汇编语言同样遵循Go语言少即是多的哲学它只保留了最基本的特性定义变量和全局函数。同时为了简化Go汇编器的词法扫描程序的实现特别引入了Unicode中的中点`·`和大写的除法`/`对应的Unicode码点为`U+00B7``U+2215`。汇编器编译后,中点`·`会被替换为ASCII中的点“.”,大写除法会被替换为ASCII码中的除法“/”,比如`math/rand·Int`会被替换为`math/rand.Int`。这样可以将点和浮点数中的小数点、大写的除法和表达式中的除法符号分开,可以简化汇编程序法分析部分的实现。
即使暂时抛开Go汇编语言设计取舍的问题中点`·`和除法`/`两个字符的如何输入就是一个挑战。这两个字符在 https://golang.org/doc/asm 文档中均有描述,因此直接从该页面复制是最简单可靠的方式。 即使暂时抛开Go汇编语言设计取舍的问题中点`·`和除法`/`两个字符的如何输入就是一个挑战。这两个字符在 https://golang.org/doc/asm 文档中均有描述,因此直接从该页面复制是最简单可靠的方式。

View File

@ -4,7 +4,7 @@
## 故意设计没有goid ## 故意设计没有goid
根据官方的相关资料显示Go语言刻意没有提供goid的原因是为了避免被滥用。因为大部分用户在轻松拿到goid之后在之后的编程中会不自觉地编写出强依赖goid的代码。强依赖goid将导致这些代码不好移植同时也会导致并发模型复杂化。同时Go语言中可能同时存在海量的Goroutine但是每个Goroutine合适被销毁并不好实时监控这也会导致依赖goid的资源无法很好地自动回收需要手工回收。如果你是Go汇编语言用户完全可以忽略这些借口。 根据官方的相关资料显示Go语言刻意没有提供goid的原因是为了避免被滥用。因为大部分用户在轻松拿到goid之后在之后的编程中会不自觉地编写出强依赖goid的代码。强依赖goid将导致这些代码不好移植同时也会导致并发模型复杂化。同时Go语言中可能同时存在海量的Goroutine但是每个Goroutine何时被销毁并不好实时监控这也会导致依赖goid的资源无法很好地自动回收需要手工回收。如果你是Go汇编语言用户完全可以忽略这些借口。
## 纯Go方式获取goid ## 纯Go方式获取goid
@ -191,7 +191,7 @@ TEXT ·getg(SB), NOSPLIT, $32-16
RET RET
``` ```
其中AX寄存器对应g指针BX寄存器对应g结构体的类型。然后通过runtime·convT2E函数将类型转为接口。因为我们使用的不是g结构体指针类型因此返回的接口表示的g结构体值类型。理论上我们也可以构造g指针类型的接口但是因为Go汇编语言的限制我们无法`type·*runtime·g`标识符。 其中AX寄存器对应g指针BX寄存器对应g结构体的类型。然后通过runtime·convT2E函数将类型转为接口。因为我们使用的不是g结构体指针类型因此返回的接口表示的g结构体值类型。理论上我们也可以构造g指针类型的接口但是因为Go汇编语言的限制我们无法`type·*runtime·g`标识符。
基于g返回的接口就可以容易获取goid了 基于g返回的接口就可以容易获取goid了
@ -264,7 +264,7 @@ TEXT ·getg(SB), NOSPLIT, $32-16
RET RET
``` ```
其中NO_LOCAL_POINTERS表示函数没有局部指针变量。同时对返回的接口进行零值初始化初始化完成后通过GO_RESULTS_INITIALIZED告知GC。这样可以在保证栈分裂GC能够正确处理返回值和局部变量中的指针。 其中NO_LOCAL_POINTERS表示函数没有局部指针变量。同时对返回的接口进行零值初始化初始化完成后通过GO_RESULTS_INITIALIZED告知GC。这样可以在保证栈分裂GC能够正确处理返回值和局部变量中的指针。
## goid的应用: 局部存储 ## goid的应用: 局部存储
@ -316,7 +316,7 @@ func Delete(key interface{}) {
} }
``` ```
最后我们再提供一个Clean函数用于是否Goroutine对应的map资源 最后我们再提供一个Clean函数用于释放Goroutine对应的map资源
```go ```go
func Clean() { func Clean() {
@ -350,5 +350,5 @@ func main() {
} }
``` ```
通过Goroutine局部存储不同层次函数之间可以共享存储资源。同时未来避免资源泄露,需要再Goroutine的根函数中通过defer语句调用gls.Clean()函数释放资源。 通过Goroutine局部存储不同层次函数之间可以共享存储资源。同时为了避免资源泄漏,需要在Goroutine的根函数中通过defer语句调用gls.Clean()函数释放资源。

View File

@ -1,3 +1,3 @@
# 3.8. 补充说明 # 3.8. 补充说明
得益于Go语言的设计Go汇编语言的优势也非常明显跨操作系统、不同CPU之间的用法也非常相似、支持C语言预处理器、支持模块。同时Go汇编语言也存在很多不足它不是一个独立的语言底层需要依赖Go语言甚至操作系统很多高级特性很难通过手工汇编完成。虽然Go语言官方尽量保持Go汇编语言简单但是汇编语言是一个比较大的话题大到足以写一本Go汇编语言的教程。本章的目的是让大家对Go汇编语言简单入门在看到底层汇编代码的时候不会一头雾水在某些遇到性能或禁制的场合能够通过Go汇编突破限制。这只是一个开始后续版本会继续完善。 得益于Go语言的设计Go汇编语言的优势也非常明显跨操作系统、不同CPU之间的用法也非常相似、支持C语言预处理器、支持模块。同时Go汇编语言也存在很多不足它不是一个独立的语言底层需要依赖Go语言甚至操作系统很多高级特性很难通过手工汇编完成。虽然Go语言官方尽量保持Go汇编语言简单但是汇编语言是一个比较大的话题大到足以写一本Go汇编语言的教程。本章的目的是让大家对Go汇编语言简单入门在看到底层汇编代码的时候不会一头雾水在某些遇到性能受限制的场合能够通过Go汇编突破限制。这只是一个开始后续版本会继续完善。

View File

@ -82,7 +82,7 @@ func RegisterHelloService(svc HelloServiceInterface) error {
} }
``` ```
我们将RPC服务的接口规范分为三个部分是服务的名字然后是服务要实现的详细方法列表最后是注册该类型服务的函数。为了避免名字冲突我们在RPC服务的名字中增加了包路径前缀这个是RPC服务抽象的包路径并非完全等价Go语言的包路径。RegisterHelloService注册服务时编译器会要求传入的对象满足HelloServiceInterface接口。 我们将RPC服务的接口规范分为三个部分是服务的名字然后是服务要实现的详细方法列表最后是注册该类型服务的函数。为了避免名字冲突我们在RPC服务的名字中增加了包路径前缀这个是RPC服务抽象的包路径并非完全等价Go语言的包路径。RegisterHelloService注册服务时编译器会要求传入的对象满足HelloServiceInterface接口。
在定义了RPC服务接口规范之后客户端就可以根据规范编写RPC调用的代码了 在定义了RPC服务接口规范之后客户端就可以根据规范编写RPC调用的代码了
@ -101,7 +101,7 @@ func main() {
} }
``` ```
其中唯一的变化是client.Call的第一个参数用`HelloServiceName+".Hello"`代理了"HelloService.Hello"。然后通过client.Call函数调用RPC方法依然比较繁琐同时参数的类型依然无法得到编译器提供的安全保障。 其中唯一的变化是client.Call的第一个参数用`HelloServiceName+".Hello"代替了"HelloService.Hello"。然而通过client.Call函数调用RPC方法依然比较繁琐同时参数的类型依然无法得到编译器提供的安全保障。
为了简化客户端用户调用RPC函数我们在可以在接口规范部分增加对客户端的简单包装 为了简化客户端用户调用RPC函数我们在可以在接口规范部分增加对客户端的简单包装
@ -125,7 +125,7 @@ func (p *HelloServiceClient) Hello(request string, reply *string) error {
} }
``` ```
我们在接口规范中针对客户端新增加了HelloServiceClient类型类型也必须满足HelloServiceInterface接口这样客户端用户就可以直接通过接口对应的方法调用RPC函数。同时提供了一个DialHelloService方法直接拨号HelloService服务。 我们在接口规范中针对客户端新增加了HelloServiceClient类型类型也必须满足HelloServiceInterface接口这样客户端用户就可以直接通过接口对应的方法调用RPC函数。同时提供了一个DialHelloService方法直接拨号HelloService服务。
基于新的客户端接口,我们可以简化客户端用户的代码: 基于新的客户端接口,我们可以简化客户端用户的代码:
@ -175,14 +175,14 @@ func main() {
} }
``` ```
在新的RPC服务端实现中我们用RegisterHelloService函数来注册函数这样不仅可以避免服务名称的工作同时也保证了传入的服务对象满足了RPC接口定义的定义。最后我们支持多个TCP链接然后为每个TCP链接建立RPC服务。 在新的RPC服务端实现中我们用RegisterHelloService函数来注册函数这样不仅可以避免命名服务名称的工作同时也保证了传入的服务对象满足了RPC接口的定义。最后我们支持多个TCP链接然后为每个TCP链接建立RPC服务。
## 跨语言的RPC ## 跨语言的RPC
标准库的RPC默认采用Go语言特有的gob规范编码因此从其它语言调用Go语言实现的RPC服务将比较困难。在互联网的微服务时代每个RPC以及服务的使用者都可能采用不同的编程语言因此跨语言是互联网时代RPC的一个首要条件。得益于RPC的框架设计Go语言的RPC其实也是很容易实现跨语言支持的。 标准库的RPC默认采用Go语言特有的gob规范编码因此从其它语言调用Go语言实现的RPC服务将比较困难。在互联网的微服务时代每个RPC以及服务的使用者都可能采用不同的编程语言因此跨语言是互联网时代RPC的一个首要条件。得益于RPC的框架设计Go语言的RPC其实也是很容易实现跨语言支持的。
Go语言的RPC框架有两个比较有特色的设计一个是RPC数据打包时可以通过插件实现自定义的编码和解码另一个是RPC建立在抽象的io.ReadWriteCloser接口之上的我们可以将RPC架设在不同的通讯协议之上。我们这里将尝试通过官方自带的net/rpc/jsonrpc扩展实现一个跨语言的PPC。 Go语言的RPC框架有两个比较有特色的设计一个是RPC数据打包时可以通过插件实现自定义的编码和解码另一个是RPC建立在抽象的io.ReadWriteCloser接口之上的我们可以将RPC架设在不同的通讯协议之上。这里我们将尝试通过官方自带的net/rpc/jsonrpc扩展实现一个跨语言的PPC。
首先是基于json实现RPC服务 首先是基于json实现RPC服务
@ -237,9 +237,9 @@ func main() {
{"method":"HelloService.Hello","params":["hello"],"id":0} {"method":"HelloService.Hello","params":["hello"],"id":0}
``` ```
这是一个json编码的数据其中method部分对应要调用的rpc服务和方法组合成的名字params部分的第一个元素为参数部分id是由调用端维护的一个唯一的调用编号。 这是一个json编码的数据其中method部分对应要调用的rpc服务和方法组合成的名字params部分的第一个元素为参数id是由调用端维护的一个唯一的调用编号。
请求的json数据对在内部对应两个结构体客户端是clientRequest服务端是serverRequest。clientRequest和serverRequest结构体的内容基本是一致的 请求的json数据对在内部对应两个结构体客户端是clientRequest服务端是serverRequest。clientRequest和serverRequest结构体的内容基本是一致的
```go ```go
type clientRequest struct { type clientRequest struct {
@ -255,7 +255,7 @@ type serverRequest struct {
} }
``` ```
在获取到RPC调用对应的json数据后我们可以通过直接向设了RPC服务的TCP服务器发送json数据模拟RPC方法调用 在获取到RPC调用对应的json数据后我们可以通过直接向设了RPC服务的TCP服务器发送json数据模拟RPC方法调用
``` ```
$ echo -e '{"method":"HelloService.Hello","params":["hello"],"id":1}' | nc localhost 1234 $ echo -e '{"method":"HelloService.Hello","params":["hello"],"id":1}' | nc localhost 1234
@ -285,13 +285,13 @@ type serverResponse struct {
} }
``` ```
因此无论采用何语言只要遵循同样的json结构以同样的流程就可以和Go语言编写的RPC服务进行通信。这样我们就实现了跨语言的RPC。 因此无论采用何语言只要遵循同样的json结构以同样的流程就可以和Go语言编写的RPC服务进行通信。这样我们就实现了跨语言的RPC。
## Http上的RPC ## Http上的RPC
Go语言内在的RPC框架已经支持在Http协议上提供RPC服务。但是框架的http服务同样采用了内置的gob协议并且没有提供采用其它协议的接口因此从其它语言依然无法访问的。在前面的例子中我们已经实现了在纯的TCP协议之上运行jsonrpc服务并且可以通过nc命令行工具成功实现了RPC方法调用。现在我们尝试在http协议上提供jsonrpc服务。 Go语言内在的RPC框架已经支持在Http协议上提供RPC服务。但是框架的http服务同样采用了内置的gob协议并且没有提供采用其它协议的接口因此从其它语言依然无法访问的。在前面的例子中我们已经实现了在纯的TCP协议之上运行jsonrpc服务并且可以通过nc命令行工具成功实现了RPC方法调用。现在我们尝试在http协议上提供jsonrpc服务。
心的RPC服务其实是一个类似REST规范的接口采用请求和相应处理流程: 新的RPC服务其实是一个类似REST规范的接口接收请求和采用相应处理流程:
```go ```go
func main() { func main() {
@ -315,7 +315,7 @@ func main() {
RPC的服务假设在“/jsonrpc”路径在处理函数中基于http.ResponseWriter和http.Request类型的参数构造一个io.ReadWriteCloser类型的conn通道。然后基于conn构建针对服务端的json编码解码器。最后通过rpc.ServeRequest处理一次RPC方法调用。 RPC的服务假设在“/jsonrpc”路径在处理函数中基于http.ResponseWriter和http.Request类型的参数构造一个io.ReadWriteCloser类型的conn通道。然后基于conn构建针对服务端的json编码解码器。最后通过rpc.ServeRequest处理一次RPC方法调用。
模拟一次RPC调用的过程就是向该链接发一个json字符串 模拟一次RPC调用的过程就是向该链接发一个json字符串
``` ```
$ curl localhost:1234/jsonrpc -X POST --data '{"method":"HelloService.Hello","params":["hello"],"id":0}' $ curl localhost:1234/jsonrpc -X POST --data '{"method":"HelloService.Hello","params":["hello"],"id":0}'

View File

@ -1,10 +1,10 @@
# 4.2. Protobuf # 4.2. Protobuf
Protobuf是Protocol Buffers的简称它是Google公司开发的一种数据描述语言并于2008年对外开源。Protobuf刚开源时的定位类似于XML、JSON等数据描述语言通过附带工具生成等代码提实现将结构化数据序列化的功能。但是我们更关注的是Protobuf作为接口规范的描述语言可以作为设计安全的跨语言PRC接口的基础工具。 Protobuf是Protocol Buffers的简称它是Google公司开发的一种数据描述语言并于2008年对外开源。Protobuf刚开源时的定位类似于XML、JSON等数据描述语言通过附带工具生成代码并实现将结构化数据序列化的功能。但是我们更关注的是Protobuf作为接口规范的描述语言可以作为设计安全的跨语言PRC接口的基础工具。
## Protobuf入门 ## Protobuf入门
对于没有用过Protobuf读者建议先从官网了解下基本用法。这里我们尝试如何将Protobuf和RPC结合在一起使用通过Protobuf来最终保证RPC的接口规范和全。Protobuf中最基本的数据单元是message是类似Go语言中结构体的存在。在message中可以嵌套message或其它的基础数据类型的成员。 对于没有用过Protobuf读者建议先从官网了解下基本用法。这里我们尝试如何将Protobuf和RPC结合在一起使用通过Protobuf来最终保证RPC的接口规范和全。Protobuf中最基本的数据单元是message是类似Go语言中结构体的存在。在message中可以嵌套message或其它的基础数据类型的成员。
首先创建hello.proto文件其中包装HelloService服务中用到的字符串类型 首先创建hello.proto文件其中包装HelloService服务中用到的字符串类型
@ -18,11 +18,11 @@ message String {
} }
``` ```
开头的syntax语句表示采用Protobuf第三版本的语法。第三版的Protobuf对语言进行的提炼简化所有成员均采用类似Go语言中的零值初始化不在支持自定义默认值同时消息成员也不再支持required特性。然后package指令指明当前是main包这样可以和Go的包明保持一致当然用户也可以针对不同的语言定制对应的包路径和名称。最后message关键字定义一个新的String类型在最终生成的Go语言代码中对应一个String结构体。String类型中只有一个字符串类型的value成员该成员的Protobuf编码时的成员编号为1。 开头的syntax语句表示采用Protobuf第三版本的语法。第三版的Protobuf对语言进行了提炼简化所有成员均采用类似Go语言中的零值初始化不再支持自定义默认值同时消息成员也不再支持required特性。然后package指令指明当前是main包这样可以和Go的包明保持一致当然用户也可以针对不同的语言定制对应的包路径和名称。最后message关键字定义一个新的String类型在最终生成的Go语言代码中对应一个String结构体。String类型中只有一个字符串类型的value成员该成员的Protobuf编码时的成员编号为1。
在XML或JSON成数据描述语言中,一遍通过成员的名字来绑定对应的数据。但是Protobuf编码却是通过成员的唯一编号来绑定对应的数据因此Protobuf编码后数据的体积会比较小但是也非常不便于人类查阅。我们目前并不关注Protobuf的编码技术最终生成的Go结构体可以自由采用JSON或gob等编码格式因此大家可以暂时忽略Protobuf的成员编部分。 在XML或JSON等数据描述语言中,一般通过成员的名字来绑定对应的数据。但是Protobuf编码却是通过成员的唯一编号来绑定对应的数据因此Protobuf编码后数据的体积会比较小但是也非常不便于人类查阅。我们目前并不关注Protobuf的编码技术最终生成的Go结构体可以自由采用JSON或gob等编码格式因此大家可以暂时忽略Protobuf的成员编部分。
Protobuf核心的工具集是C++语言开发的在官方的protoc编译器中并不支持Go语言。要想基于上面的hello.proto文件生成相应的Go代码需要安装相应的工具。首先是安装官方的protoc工具可以从 https://github.com/google/protobuf/releases 下载。然后是安装针对Go语言的代码生成插件可以通过`go get github.com/golang/protobuf/protoc-gen-go`命令安装。 Protobuf核心的工具集是C++语言开发的在官方的protoc编译器中并不支持Go语言。要想基于上面的hello.proto文件生成相应的Go代码需要安装相应的工具。首先是安装官方的protoc工具可以从 https://github.com/google/protobuf/releases 下载。然后是安装针对Go语言的代码生成插件可以通过`go get github.com/golang/protobuf/protoc-gen-go`命令安装。
然后通过以下命令生成相应的Go代码 然后通过以下命令生成相应的Go代码
@ -30,7 +30,7 @@ Protobuf核心的工具集是C++语言开发的在官方的protoc编译器中
$ protoc --go_out=. hello.proto $ protoc --go_out=. hello.proto
``` ```
其中`go_out`参数告知protoc编译器加载对应的protoc-gen-go工具然后通过该工具生成代码生成代码放到当前目录。最后是一系列要处理的protobuf文件的列表。 其中`go_out`参数告知protoc编译器加载对应的protoc-gen-go工具然后通过该工具生成代码生成代码放到当前目录。最后是一系列要处理的protobuf文件的列表。
这里只生成了一个hello.pb.go文件其中String结构体内容如下 这里只生成了一个hello.pb.go文件其中String结构体内容如下
@ -57,7 +57,7 @@ func (m *String) GetValue() string {
} }
``` ```
生成的结构体中有一些以`XXX_`为前缀名字的成员目前可以忽略这些成员。同时String类型还自动生成了一组方法其中ProtoMessage方法表示这是一个实现了proto.Message接口的方法。此外Protobuf还为每个成员生成了一个Get方法Get方法不仅可以处理空指针类型而且可以和Protobuf第版的方法保持一致(第二版的自定义默认值特性依赖这类方法)。 生成的结构体中有一些以`XXX_`名字前缀的成员目前可以忽略这些成员。同时String类型还自动生成了一组方法其中ProtoMessage方法表示这是一个实现了proto.Message接口的方法。此外Protobuf还为每个成员生成了一个Get方法Get方法不仅可以处理空指针类型而且可以和Protobuf第版的方法保持一致(第二版的自定义默认值特性依赖这类方法)。
基于新的String类型我们可以重新实现HelloService 基于新的String类型我们可以重新实现HelloService
@ -74,7 +74,7 @@ func (p *HelloService) Hello(request *String, reply *String) error {
至此我们初步实现了Protobuf和RPC组合工作。在启动RPC服务时我们依然可以选择默认的gob或手工指定json编码甚至可以重新基于protobuf编码实现一个插件。虽然做了这么多工作但是似乎并没有看到什么收益 至此我们初步实现了Protobuf和RPC组合工作。在启动RPC服务时我们依然可以选择默认的gob或手工指定json编码甚至可以重新基于protobuf编码实现一个插件。虽然做了这么多工作但是似乎并没有看到什么收益
回顾第一章中更安全的PRC接口部分的内容当时我们花费了极大的力气去给RPC服务增加安全的保障。最终得到的更安全的PRC接口的代码本书就非常繁琐比利于手工维护同时全部安全相关的代码只适用于Go语言环境既然使用了Protobuf定义的输入和输出参数那么RPC服务接口是否也可以通过Protobuf定义呢其实用Protobuf定义语言无关的PRC服务接口才是它真正的价值所在 回顾第一章中更安全的PRC接口部分的内容当时我们花费了极大的力气去给RPC服务增加安全的保障。最终得到的更安全的PRC接口的代码本书就非常繁琐的使用手工维护同时全部安全相关的代码只适用于Go语言环境既然使用了Protobuf定义的输入和输出参数那么RPC服务接口是否也可以通过Protobuf定义呢其实用Protobuf定义语言无关的PRC服务接口才是它真正的价值所在
下面更新hello.proto文件通过Protobuf来定义HelloService服务 下面更新hello.proto文件通过Protobuf来定义HelloService服务
@ -92,7 +92,7 @@ service HelloService {
$ protoc --go_out=plugins=grpc:. hello.proto $ protoc --go_out=plugins=grpc:. hello.proto
``` ```
在生成的代码中多了一些类似HelloServiceServer、HelloServiceClient的新类型。这些类是为grpc服务的并不符合我们的RPC要求。 在生成的代码中多了一些类似HelloServiceServer、HelloServiceClient的新类型。这些类是为grpc服务的并不符合我们的RPC要求。
grpc插件为我们提供了改进思路下面我们将探索如何为我们的RPC生成安全的代码。 grpc插件为我们提供了改进思路下面我们将探索如何为我们的RPC生成安全的代码。
@ -233,7 +233,7 @@ func main() {
$ protoc --go-netrpc_out=plugins=netrpc:. hello.proto $ protoc --go-netrpc_out=plugins=netrpc:. hello.proto
``` ```
其中`--go-netrpc_out`参数高中protoc编译器加载名为protoc-gen-go-netrpc的插件插件中的`plugins=netrpc`指示启用内部名为netrpc的netrpcPlugin插件。 其中`--go-netrpc_out`参数告知protoc编译器加载名为protoc-gen-go-netrpc的插件插件中的`plugins=netrpc`指示启用内部名为netrpc的netrpcPlugin插件。
在新生成的hello.pb.go文件中将包含增加的注释代码。至此手工定制的Protobuf代码生成插件终于可以工作了。 在新生成的hello.pb.go文件中将包含增加的注释代码。至此手工定制的Protobuf代码生成插件终于可以工作了。

View File

@ -1,24 +1,151 @@
# 4.3. 玩转RPC # 4.3. 玩转RPC
TODO 在不同的场景中RPC有着不同的需求因此开源的社区就诞生了各种RPC框架。本节我们将尝试Go内置RPC框架在一些比较特殊场景的用法。
<!-- ## 反向RPC
认证/反向/gls能够拿到req吗基于req的gls 通常的RPC是基于C/S结构RPC的服务端对应网络的服务器RPC的客户端也对应网络客户端。但是对于一些特殊场景比如在公司内网提供一个RPC服务但是在外网无法链接到内网的服务器。这种时候我们可以参考类似反向代理的技术首先从内网主动链接到外网的TCP服务器然后基于TCP链接向外网提供RPC服务。
-- 以下是启动反向RPC服务的代码
pb 和 json 是类似的, ```go
func main() {
rpc.Register(new(HelloService))
唯一的差异的 protoc 工具 for {
conn, _ := net.Dial("tcp", "localhost:1234")
if conn == nil {
time.Sleep(time.Second)
continue
}
有了代码生成,一切就发生变化了 rpc.ServeConn(conn)
conn.Close()
}
}
```
pb 的重要性不再时底层的编码,二是 api 的通用语言!!! 反向RPC的内网服务将不再主导提供TCP监听服务而是首先主动链接到对方的TCP服务器。然后基于每个建立的TCP链接向对方提供RPC服务。
但是 pb 个 rest 是无法一一等价的 而RPC客户端则需要在一个公共的地址提供一个TCP服务用于接受RPC服务器的链接请求
生成简单的代码,增加接口约束 ```go
func main() {
listener, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("ListenTCP error:", err)
}
甚至最终回归到 gob 或 json 编码 clientChan := make(chan *rpc.Client)
-->
go func() {
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal("Accept error:", err)
}
clientChan <- rpc.NewClient(conn)
}
}()
doClientWork(clientChan)
}
```
当每个链接建立后基于网络链接构造RPC客户端对象并发送到clientChan管道。
客户端执行RPC调用的操作在doClientWork函数完成
```go
func doClientWork(clientChan <-chan *rpc.Client) {
client := <-clientChan
defer client.Close()
var reply string
err = client.Call("HelloService.Hello", "hello", &reply)
if err != nil {
log.Fatal(err)
}
fmt.Println(reply)
}
```
首先从管道去取一个RPC客户端对象并且通过defer语句指定在函数退出前关闭客户端。然后是执行正常的RPC调用。
## 上下文信息
首先是上下文信息基于上下文我们可以针对不同客户端提供定制化的RPC服务。我们可以通过为每个信道提供独立的RPC服务来实现对上下文特性的支持。
首先改造HelloService里面增加了对应链接的conn成员
```go
type HelloService struct {
conn net.Conn
}
```
然后为每个信道启动独立的RPC服务
```go
func main() {
listener, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("ListenTCP error:", err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal("Accept error:", err)
}
go func() {
defer conn.Close()
p := rpc.NewServer()
p.Register(&HelloService{conn: conn})
p.ServeConn(conn)
} ()
}
}
```
Hello方法中就可以根据conn成员识别不同信道的RPC调用
```go
func (p *HelloService) Hello(request string, reply *string) error {
*reply = "hello:" + request + ", from" + p.conn.RemoteAddr().String()
return nil
}
```
基于上下文信息我们可以方便地为RPC服务增加简单的登陆状态的验证
```go
type HelloService struct {
conn net.Conn
isLogin bool
}
func (p *HelloService) Login(request string, reply *string) error {
if request != "user:password" {
return fmt.Errorf("auth failed")
}
log.Println("login ok")
p.isLogin = true
return nil
}
func (p *HelloService) Hello(request string, reply *string) error {
if !p.isLogin {
return fmt.Errorf("please login")
}
*reply = "hello:" + request + ", from" + p.conn.RemoteAddr().String()
return nil
}
```
这样可以要求在客户端链接RPC服务时首先要执行登陆操作登陆成功后才能正常执行其他的服务。

View File

@ -1,8 +1,223 @@
# 4.4. GRPC入门 # 4.4. GRPC入门
GRPC是Google公司基于Protobuf开发的跨语言的开源RPC框架。GRPC基于HTTP/2协议设计可以基于一个HTTP/2链接提供多个服务对于移动设备更加友好。本节将讲述GRPC的简单用法。
## GRPC入门
如果从Protobuf的角度看GRPC只不过是一个针对service接口生成代码的生成器。我们在本章的第二节中手工实现了一个简单的Protobuf代码生成器插件只不过当时生成的代码是适配标准库的RPC框架的。
创建hello.proto文件定义HelloService接口
```proto
syntax = "proto3";
package main;
message String {
string value = 1;
}
service HelloService {
rpc Hello (String) returns (String);
}
```
使用protoc-gen-go内置的grpc插件生成GRPC代码
```
$ protoc --go_out=plugins=grpc:. hello.proto
```
GRPC插件会为服务端和客户端生成不同的接口
```go
type HelloServiceServer interface {
Hello(context.Context, *String) (*String, error)
}
type HelloServiceClient interface {
Hello(ctx context.Context, in *String, opts ...grpc.CallOption) (*String, error)
}
```
GRPC通过context.Context参数为每个方法调用提供了上下文支持。客户端在调用方法的时候可以通过可选的grpc.CallOption类型的参数提供额外的上下文信息。
基于服务端的HelloServiceServer接口可以重新实现HelloService服务
```go
type HelloServiceImpl struct{}
func (p *HelloServiceImpl) Hello(ctx context.Context, args *String) (*String, error) {
reply := &String{Value: "hello:" + args.GetValue()}
return reply, nil
}
```
GRPC服务的启动流程和标准库的RPC服务启动流程类似
```go
func main() {
grpcServer := grpc.NewServer()
RegisterHelloServiceServer(grpcServer, &HelloServiceImpl{})
lis, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal(err)
}
grpcServer.Serve(lis)
}
```
首先是通过`grpc.NewServer()`构造一个GRPC服务对象然后通过GRPC插件生成的RegisterHelloServiceServer函数注册我们实现的HelloServiceImpl服务。然后通过`grpcServer.Serve(lis)`在一个监听端口上提供GRPC服务。
然后就可以通过客户端链接GRPC服务了
```go
func main() {
conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
if err != nil {
log.Fatal(err)
}
defer conn.Close()
client := NewHelloServiceClient(conn)
reply, err := client.Hello(context.Background(), &String{Value: "hello"})
if err != nil {
log.Fatal(err)
}
fmt.Println(reply.GetValue())
}
```
其中grpc.Dial负责和GRPC服务建立链接然后NewHelloServiceClient函数基于已经建立的链接构造HelloServiceClient对象。返回的client其实是一个HelloServiceClient接口对象通过接口定义的方法就可以调用服务端对应的GRPC服务提供的方法。
GRPC和标准库的RPC框架还有一个区别GRPC生成的接口并不支持异步调用。
## GRPC流
RPC是远程函数调用因此每次调用的函数参数和返回值不能太大否则将严重影响每次调用的性能。因此传统的RPC方法调用对于上传和下载较大数据量场景并不适合。同时传统RPC模式也不适用于对时间不确定的订阅和发布模式。为此GRPC框架分别提供了服务器端和客户端的流特性。
服务端或客户端的单向流是双向流的特例我们在HelloService增加一个支持双向流的Channel方法
```proto
service HelloService {
rpc Hello (String) returns (String);
rpc Channel (stream String) returns (stream String);
}
```
关键字stream指定启用流特性参数部分是接收客户端参数的流返回值是返回给客户端的流。
重新生成代码可以看到接口中新增加的Channel方法的定义
```go
type HelloServiceServer interface {
Hello(context.Context, *String) (*String, error)
Channel(HelloService_ChannelServer) error
}
type HelloServiceClient interface {
Hello(ctx context.Context, in *String, opts ...grpc.CallOption) (*String, error)
Channel(ctx context.Context, opts ...grpc.CallOption) (HelloService_ChannelClient, error)
}
```
在服务端的Channel方法参数是一个新的HelloService_ChannelServer类型的参数可以用于和客户端双向通信。客户端的Channel方法返回一个HelloService_ChannelClient类型的返回值可以用于和服务端进行双向通信。
HelloService_ChannelServer和HelloService_ChannelClient均为接口类型
```go
type HelloService_ChannelServer interface {
Send(*String) error
Recv() (*String, error)
grpc.ServerStream
}
type HelloService_ChannelClient interface {
Send(*String) error
Recv() (*String, error)
grpc.ClientStream
}
```
可以发现服务端和客户端的流辅助接口均定义了Send和Recv方法用于流数据的双向通信。
现在我们可以实现流服务:
```go
func (p *HelloServiceImpl) Channel(stream HelloService_ChannelServer) error {
for {
args, err := stream.Recv()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
reply := &String{Value: "hello:" + args.GetValue()}
err = stream.Send(reply)
if err != nil {
return err
}
}
}
```
服务端在循环中接收客户端发来的数据如果遇到io.EOF表示客户端流被关闭如果函数退出表示服务端流关闭。然后生成返回的数据通过流发送给客户端。需要注意的是发送和接收的操作并不需要一一对应用户可以根据真实场景进行组织代码。
客户端需要先调用Channel方法获取返回的流对象
```go
stream, err := client.Channel(context.Background())
if err != nil {
log.Fatal(err)
}
```
在客户端我们将发送和接收操作放到两个独立的Goroutine。首先是向服务端发送数据
```go
go func() {
for {
if err := stream.Send(&String{Value: "hi"}); err != nil {
log.Fatal(err)
}
time.Sleep(time.Second)
}
}()
```
然后在循环中接收服务端返回的数据:
```go
for {
reply, err := stream.Recv()
if err != nil {
if err == io.EOF {
break
}
log.Fatal(err)
}
fmt.Println(reply.GetValue())
}
```
这样就完成了完整的流接收和发送支持。
<!--
Publish
Watch
TODO TODO
<!-- ## 认证
TODO
入门/流/认证 入门/流/认证

4
ch4-rpc/ch4-05-faq.md Normal file
View File

@ -0,0 +1,4 @@
# 4.5. 补充说明
本章重点讲述了Go标准库的RPC和基于Protobuf衍生的GRPC框架同时也简单展示了如何自己定制一个RPC框架。之所以聚焦在这几个有限的主题是因为这几个技术都是Go语言团队官方在进行维护和Go语言契合也最为默契。不过RPC依然是一个庞大的主题足以单独成书。目前开源世界也有很多富有特色的RPC框架还有针对分布式系统进行深度定制的RPC系统用户可以根据自己实际需求选择合适的工具。

View File

@ -1,8 +0,0 @@
# 4.8. 补充说明
TODO
<!--
参考资料
-->

View File

@ -127,7 +127,7 @@ for _, endpoint := range endpointList {
```shell ```shell
ls /platform/order-system/create-order-service-http ls /platform/order-system/create-order-service-http
[] ['10.1.23.1:1023', '10.11.23.1:1023']
``` ```
当与 zk 断开连接时,注册在该节点下的临时节点也会消失,即实现了服务节点故障时的被动摘除。 当与 zk 断开连接时,注册在该节点下的临时节点也会消失,即实现了服务节点故障时的被动摘除。
@ -136,16 +136,51 @@ ls /platform/order-system/create-order-service-http
## 基于 zk 的完整服务发现流程 ## 基于 zk 的完整服务发现流程
节点故障断开 zk 连接时zk 会负责将该消息通知所有监听方。
用代码来实现一下上面的几个逻辑。 用代码来实现一下上面的几个逻辑。
### 临时节点注册 ### 临时节点注册
```go ```go
package main
import (
"fmt"
"time"
"github.com/samuel/go-zookeeper/zk"
)
func main() {
c, _, err := zk.Connect([]string{"127.0.0.1"}, time.Second)
if err != nil {
panic(err)
}
res, err := c.Create("/platform/order-system/create-order-service-http/10.1.13.3:1043", []byte("1"),
zk.FlagEphemeral, zk.WorldACL(zk.PermAll))
if err != nil {
panic(err)
}
println(res)
time.Sleep(time.Second * 50)
}
``` ```
### 服务节点获取 在 sleep 的时候我们在 cli 中查看写入的临时节点数据:
```shell
ls /platform/order-system/create-order-service-http
['10.1.13.3:1043']
```
在程序结束之后,很快这条数据也消失了:
```shell
ls /platform/order-system/create-order-service-http
[]
```
### watch 数据变化
### 消息通知 ### 消息通知

View File

@ -94,3 +94,28 @@ Go 自身的 timer 就是用时间堆来实现的,不过并没有使用二叉
### 时间轮 ### 时间轮
![timewheel](../images/ch6-timewheel.png) ![timewheel](../images/ch6-timewheel.png)
用时间轮来实现 timer 时,我们需要定义每一个格子的“刻度”,可以将时间轮想像成一个时钟,中心有秒针顺时针转动。每次转动到一个刻度时,我们就需要去查看该刻度挂载的 tasklist 是否有已经到期的任务。
从结构上来讲,时间轮和哈希表很相似,如果我们把哈希算法定义为:触发时间%时间轮元素大小。那么这就是一个简单的哈希表。在哈希冲突时,采用链表挂载哈希冲突的定时器。
除了这种单层时间轮,业界也有一些时间轮采用多层实现,这里就不再赘述了。
## 任务分发
有了基本的 timer 实现方案,如果我们开发的是单机系统,那么就可以撸起袖子开干了,不过本章我们讨论的是分布式,距离“分布式”还稍微有一些距离。
我们还需要把这些“定时”或是“延时”(本质也是定时)任务分发出去。下面是一种思路:
![task-dist](../images/ch6-task-sched.png)
每一个实例每隔一小时,会去数据库里把下一个小时需要处理的定时任务捞出来,捞取的时候只要取那些 task_id % shard_count = shard_id 的那些 task 即可。
当这些定时任务被触发之后需要通知用户侧,有两种思路:
1. 将任务被触发的信息封装为一条 event 消息,发往消息队列,由用户侧对消息队列进行监听。
2. 对用户预先配置的回调函数进行调用。
两种方案各有优缺点,如果采用 1那么如果消息队列出故障会导致整个系统不可用当然现在的消息队列一般也会有自身的高可用方案大多数时候我们不用担心这个问题。其次一般业务流程中间走消息队列的话会导致延时增加定时任务若必须在触发后的几十毫秒到几百毫秒内完成那么采用消息队列就会有一定的风险。如果采用 2会加重定时任务系统的负担。我们知道单机的 timer 执行时最害怕的就是回调函数执行时间长,这样会阻塞后续的任务执行。在分布式场景下,这种忧虑依然是适用的。一个不负责任的业务回调可能就会直接拖垮整个定时任务系统。所以我们还要考虑在回调的基础上增加经过测试的超时时间设置,并且对由用户填入的超时时间做慎重的审核。
## rebalance 和幂等考量

View File

@ -1,12 +0,0 @@
package main
type HelloService struct{}
func (p *HelloService) Hello(request String, reply *String) error {
reply.Value = "hello:" + request.GetValue()
return nil
}
func main() {
}

View File

@ -1,5 +0,0 @@
package main
func main() {
// todo
}

View File

@ -1,5 +0,0 @@
package main
func main() {
// todo
}

BIN
images/ch6-task-sched.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -4,7 +4,7 @@
![](cover.png) ![](cover.png)
- 作者:柴树杉 (chai2010, chaishushan@gmail.com) - 作者:柴树杉 (chai2010, chaishushan@gmail.com), 曹春晖 (cch123, https://github.com/cch123)
- 网址https://github.com/chai2010/advanced-go-programming-book - 网址https://github.com/chai2010/advanced-go-programming-book
## 在线阅读 ## 在线阅读

Some files were not shown because too many files have changed in this diff Show More