1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-05-28 23:42: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)
- 作者:柴树杉 (chai2010, chaishushan@gmail.com)
- 作者:柴树杉 (chai2010, chaishushan@gmail.com), 曹春晖 (cch123, https://github.com/cch123)
- 网址https://github.com/chai2010/advanced-go-programming-book
## 在线阅读

View File

@ -34,12 +34,9 @@
* [第四章 RPC和Protobuf](ch4-rpc/readme.md)
* [4.1. RPC入门](ch4-rpc/ch4-01-rpc-intro.md)
* [4.2. Protobuf](ch4-rpc/ch4-02-pb-intro.md)
* [4.3. 玩转RPC(TODO)](ch4-rpc/ch4-03-netrpc-hack.md)
* [4.4. GRPC入门(TODO)](ch4-rpc/ch4-04-grpc.md)
* [4.5. GRPC进阶(TODO)](ch4-rpc/ch4-05-grpc-hack.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)
* [4.3. 玩转RPC](ch4-rpc/ch4-03-netrpc-hack.md)
* [4.4. GRPC入门](ch4-rpc/ch4-04-grpc.md)
* [4.5. 补充说明](ch4-rpc/ch4-05-faq.md)
* [第五章 Go和Web](ch5-web/readme.md)
* [5.1. Web开发简介](ch5-web/ch5-01-introduction.md)
* [5.2. Router请求路由](ch5-web/ch5-02-router.md)

View File

@ -1,4 +1,4 @@
# 附录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)开发工作。
- [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:]
```
和数组一样,内置的`len``cap`函数返回相同的结果,都对应字符串的长度。也可以通过`reflect.StringHeader`结构访问字符串的长度(这里只是为了演示字符串的结构,并不是推荐的做法):
字符串和数组类似,内置的`len`函数返回字符串的长度。也可以通过`reflect.StringHeader`结构访问字符串的长度(这里只是为了演示字符串的结构,并不是推荐的做法):
```go
fmt.Println("len(s):", (*reflect.StringHeader)(unsafe.Pointer(&s)).Len) // 12

View File

@ -244,14 +244,14 @@ func main() {
mu.Lock()
go func(){
println("你好, 世界")
mu.Unock()
mu.Unlock()
}()
mu.Lock()
}
```
可以确定后台线程的`mu.Unock()`必然在`println("你好, 世界")`完成后发生(同一个线程满足顺序一致性),`main`函数的第二个`mu.Lock()`必然在后台线程的`mu.Unock()`之后发生(`sync.Mutex`保证),此时后台线程的打印工作已经顺利完成了。
可以确定后台线程的`mu.Unlock()`必然在`println("你好, 世界")`完成后发生(同一个线程满足顺序一致性),`main`函数的第二个`mu.Lock()`必然在后台线程的`mu.Unlock()`之后发生(`sync.Mutex`保证),此时后台线程的打印工作已经顺利完成了。
## 初始化顺序

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
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符号表示底层的字符串数据。
然后定义·Name符号为两个16字节其中前8个字节用·NameData符号对应的地址初始化后8个字节为常量6表示字符串长度。
然后定义·Name符号内存大小为16字节其中前8个字节用·NameData符号对应的地址初始化后8个字节为常量6表示字符串长度。
通过以下代码测试输出Name变量
@ -172,7 +172,7 @@ func main() {
pkgpath.NameData: missing Go //type information for global symbol: size 8
```
提示汇编中定义的NameData符号没有类型信息。其实Go汇编语言中定义的数据并没有所谓的类型每个符号只不过是对应一个内存而且。出现这种错误的原因是Go语言的垃圾回收器在扫描NameData变量的时候无法知晓该变量内部是否包含指针。因此真正错误的原因并不是NameData没有类型是NameData变量没有标注是否会含有指针信息。
提示汇编中定义的NameData符号没有类型信息。其实Go汇编语言中定义的数据并没有所谓的类型每个符号只不过是对应一块内存而已。出现这种错误的原因是Go语言的垃圾回收器在扫描NameData变量的时候无法知晓该变量内部是否包含指针。因此真正错误的原因并不是NameData没有类型是NameData变量没有标注是否会含有指针信息。
通过给NameData变量增加一个标志表示其中不会包含指针数据可以修复该错误
@ -193,9 +193,9 @@ var NameData [8]byte
var Name string
```
我们将NameData声明为长度为8的字节数组。因为编译器可以通过类型分析出该变量不会包含指针因此汇编代码中可以NOPTR标志信息
我们将NameData声明为长度为8的字节数组。编译器可以通过类型分析出该变量不会包含指针因此汇编代码中可以省略NOPTR标志。
在这个实现中Name字符串底层其实引用的是NameData内存对应的“gopher”字符串数据。因此如果NameData发生变化的化Name字符串的数据也会跟着变化
在这个实现中Name字符串底层其实引用的是NameData内存对应的“gopher”字符串数据。因此如果NameData发生变化Name字符串的数据也会跟着变化。
```go
func main() {
@ -218,7 +218,7 @@ DATA ·Name+8(SB)/8,$6
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函数
@ -245,7 +245,7 @@ TEXT ·main(SB), $16-0
CALL runtime·printnl(SB)
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指令表示当前函数返回。
@ -254,7 +254,7 @@ 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 文档中均有描述,因此直接从该页面复制是最简单可靠的方式。

View File

@ -4,7 +4,7 @@
## 故意设计没有goid
根据官方的相关资料显示Go语言刻意没有提供goid的原因是为了避免被滥用。因为大部分用户在轻松拿到goid之后在之后的编程中会不自觉地编写出强依赖goid的代码。强依赖goid将导致这些代码不好移植同时也会导致并发模型复杂化。同时Go语言中可能同时存在海量的Goroutine但是每个Goroutine合适被销毁并不好实时监控这也会导致依赖goid的资源无法很好地自动回收需要手工回收。如果你是Go汇编语言用户完全可以忽略这些借口。
根据官方的相关资料显示Go语言刻意没有提供goid的原因是为了避免被滥用。因为大部分用户在轻松拿到goid之后在之后的编程中会不自觉地编写出强依赖goid的代码。强依赖goid将导致这些代码不好移植同时也会导致并发模型复杂化。同时Go语言中可能同时存在海量的Goroutine但是每个Goroutine何时被销毁并不好实时监控这也会导致依赖goid的资源无法很好地自动回收需要手工回收。如果你是Go汇编语言用户完全可以忽略这些借口。
## 纯Go方式获取goid
@ -191,7 +191,7 @@ TEXT ·getg(SB), NOSPLIT, $32-16
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了
@ -264,7 +264,7 @@ TEXT ·getg(SB), NOSPLIT, $32-16
RET
```
其中NO_LOCAL_POINTERS表示函数没有局部指针变量。同时对返回的接口进行零值初始化初始化完成后通过GO_RESULTS_INITIALIZED告知GC。这样可以在保证栈分裂GC能够正确处理返回值和局部变量中的指针。
其中NO_LOCAL_POINTERS表示函数没有局部指针变量。同时对返回的接口进行零值初始化初始化完成后通过GO_RESULTS_INITIALIZED告知GC。这样可以在保证栈分裂GC能够正确处理返回值和局部变量中的指针。
## goid的应用: 局部存储
@ -316,7 +316,7 @@ func Delete(key interface{}) {
}
```
最后我们再提供一个Clean函数用于是否Goroutine对应的map资源
最后我们再提供一个Clean函数用于释放Goroutine对应的map资源
```go
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. 补充说明
得益于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调用的代码了
@ -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函数我们在可以在接口规范部分增加对客户端的简单包装
@ -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默认采用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服务
@ -237,9 +237,9 @@ func main() {
{"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
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
@ -285,13 +285,13 @@ type serverResponse struct {
}
```
因此无论采用何语言只要遵循同样的json结构以同样的流程就可以和Go语言编写的RPC服务进行通信。这样我们就实现了跨语言的RPC。
因此无论采用何语言只要遵循同样的json结构以同样的流程就可以和Go语言编写的RPC服务进行通信。这样我们就实现了跨语言的RPC。
## Http上的RPC
Go语言内在的RPC框架已经支持在Http协议上提供RPC服务。但是框架的http服务同样采用了内置的gob协议并且没有提供采用其它协议的接口因此从其它语言依然无法访问的。在前面的例子中我们已经实现了在纯的TCP协议之上运行jsonrpc服务并且可以通过nc命令行工具成功实现了RPC方法调用。现在我们尝试在http协议上提供jsonrpc服务。
心的RPC服务其实是一个类似REST规范的接口采用请求和相应处理流程:
新的RPC服务其实是一个类似REST规范的接口接收请求和采用相应处理流程:
```go
func main() {
@ -315,7 +315,7 @@ func main() {
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}'

View File

@ -1,10 +1,10 @@
# 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和RPC结合在一起使用通过Protobuf来最终保证RPC的接口规范和全。Protobuf中最基本的数据单元是message是类似Go语言中结构体的存在。在message中可以嵌套message或其它的基础数据类型的成员。
对于没有用过Protobuf读者建议先从官网了解下基本用法。这里我们尝试如何将Protobuf和RPC结合在一起使用通过Protobuf来最终保证RPC的接口规范和全。Protobuf中最基本的数据单元是message是类似Go语言中结构体的存在。在message中可以嵌套message或其它的基础数据类型的成员。
首先创建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代码
@ -30,7 +30,7 @@ Protobuf核心的工具集是C++语言开发的在官方的protoc编译器中
$ protoc --go_out=. hello.proto
```
其中`go_out`参数告知protoc编译器加载对应的protoc-gen-go工具然后通过该工具生成代码生成代码放到当前目录。最后是一系列要处理的protobuf文件的列表。
其中`go_out`参数告知protoc编译器加载对应的protoc-gen-go工具然后通过该工具生成代码生成代码放到当前目录。最后是一系列要处理的protobuf文件的列表。
这里只生成了一个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
@ -74,7 +74,7 @@ func (p *HelloService) Hello(request *String, reply *String) error {
至此我们初步实现了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服务
@ -92,7 +92,7 @@ service HelloService {
$ protoc --go_out=plugins=grpc:. hello.proto
```
在生成的代码中多了一些类似HelloServiceServer、HelloServiceClient的新类型。这些类是为grpc服务的并不符合我们的RPC要求。
在生成的代码中多了一些类似HelloServiceServer、HelloServiceClient的新类型。这些类是为grpc服务的并不符合我们的RPC要求。
grpc插件为我们提供了改进思路下面我们将探索如何为我们的RPC生成安全的代码。
@ -233,7 +233,7 @@ func main() {
$ 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代码生成插件终于可以工作了。

View File

@ -1,24 +1,151 @@
# 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入门
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
入门/流/认证

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
ls /platform/order-system/create-order-service-http
[]
['10.1.23.1:1023', '10.11.23.1:1023']
```
当与 zk 断开连接时,注册在该节点下的临时节点也会消失,即实现了服务节点故障时的被动摘除。
@ -136,16 +136,51 @@ ls /platform/order-system/create-order-service-http
## 基于 zk 的完整服务发现流程
节点故障断开 zk 连接时zk 会负责将该消息通知所有监听方。
用代码来实现一下上面的几个逻辑。
### 临时节点注册
```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)
用时间轮来实现 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)
- 作者:柴树杉 (chai2010, chaishushan@gmail.com)
- 作者:柴树杉 (chai2010, chaishushan@gmail.com), 曹春晖 (cch123, https://github.com/cch123)
- 网址https://github.com/chai2010/advanced-go-programming-book
## 在线阅读

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