mirror of
https://github.com/chai2010/advanced-go-programming-book.git
synced 2025-05-29 08:12:21 +00:00
commit
080131aff4
5
.github/ISSUE_TEMPLATE.md
vendored
5
.github/ISSUE_TEMPLATE.md
vendored
@ -1,4 +1 @@
|
|||||||
## Expected Behavior
|
提示:哪一章节的问题,建议如何修改
|
||||||
|
|
||||||
## Actual Behavior
|
|
||||||
|
|
||||||
|
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,14 +1 @@
|
|||||||
## Description
|
提示:解决了什么问题,也可以讲下理由。
|
||||||
|
|
||||||
A few sentences describing the overall goals of the pull request's commits.
|
|
||||||
|
|
||||||
## Designs
|
|
||||||
|
|
||||||
If it's a feature, please write your code designs here.
|
|
||||||
|
|
||||||
## Notes to reviewers
|
|
||||||
|
|
||||||
Review the following things, including but not limit to:
|
|
||||||
|
|
||||||
1. Design
|
|
||||||
2. Code (styles, magic...)
|
|
||||||
|
11
SUMMARY.md
11
SUMMARY.md
@ -8,7 +8,7 @@
|
|||||||
* [1.5. 面向并发的内存模型](ch1-basic/ch1-05-mem.md)
|
* [1.5. 面向并发的内存模型](ch1-basic/ch1-05-mem.md)
|
||||||
* [1.6. 常见的并发模式](ch1-basic/ch1-06-goroutine.md)
|
* [1.6. 常见的并发模式](ch1-basic/ch1-06-goroutine.md)
|
||||||
* [1.7. 错误和异常](ch1-basic/ch1-07-error-and-panic.md)
|
* [1.7. 错误和异常](ch1-basic/ch1-07-error-and-panic.md)
|
||||||
* [1.8. 配置开发环境](ch1-basic/ch1-08-ide.md)
|
* [1.8. 补充说明](ch1-basic/ch1-08-ext.md)
|
||||||
* [第二章 CGO编程](ch2-cgo/readme.md)
|
* [第二章 CGO编程](ch2-cgo/readme.md)
|
||||||
* [2.1. 快速入门](ch2-cgo/ch2-01-hello-cgo.md)
|
* [2.1. 快速入门](ch2-cgo/ch2-01-hello-cgo.md)
|
||||||
* [2.2. CGO基础](ch2-cgo/ch2-02-basic.md)
|
* [2.2. CGO基础](ch2-cgo/ch2-02-basic.md)
|
||||||
@ -19,9 +19,8 @@
|
|||||||
* [2.7. CGO内存模型](ch2-cgo/ch2-07-memory.md)
|
* [2.7. CGO内存模型](ch2-cgo/ch2-07-memory.md)
|
||||||
* [2.8. C++类包装](ch2-cgo/ch2-08-class.md)
|
* [2.8. C++类包装](ch2-cgo/ch2-08-class.md)
|
||||||
* [2.9. 静态库和动态库](ch2-cgo/ch2-09-static-shared-lib.md)
|
* [2.9. 静态库和动态库](ch2-cgo/ch2-09-static-shared-lib.md)
|
||||||
* [2.10. Go实现Python模块](ch2-cgo/ch2-10-py-module.md)
|
* [2.10. 编译和链接参数](ch2-cgo/ch2-10-link.md)
|
||||||
* [2.11. 编译和链接参数](ch2-cgo/ch2-11-link.md)
|
* [2.11. 补充说明](ch2-cgo/ch2-11-ext.md)
|
||||||
* [2.12. 补充说明](ch2-cgo/ch2-12-faq.md)
|
|
||||||
* [第三章 汇编语言](ch3-asm/readme.md)
|
* [第三章 汇编语言](ch3-asm/readme.md)
|
||||||
* [3.1. 快速入门](ch3-asm/ch3-01-basic.md)
|
* [3.1. 快速入门](ch3-asm/ch3-01-basic.md)
|
||||||
* [3.2. 计算机结构](ch3-asm/ch3-02-arch.md)
|
* [3.2. 计算机结构](ch3-asm/ch3-02-arch.md)
|
||||||
@ -31,7 +30,7 @@
|
|||||||
* [3.6. 再论函数](ch3-asm/ch3-06-func-again.md)
|
* [3.6. 再论函数](ch3-asm/ch3-06-func-again.md)
|
||||||
* [3.7. 例子:Goroutine ID](ch3-asm/ch3-07-goroutine-id.md)
|
* [3.7. 例子:Goroutine ID](ch3-asm/ch3-07-goroutine-id.md)
|
||||||
* [3.8. Delve调试器](ch3-asm/ch3-08-debug.md)
|
* [3.8. Delve调试器](ch3-asm/ch3-08-debug.md)
|
||||||
* [3.9. 补充说明](ch3-asm/ch3-09-faq.md)
|
* [3.9. 补充说明](ch3-asm/ch3-09-ext.md)
|
||||||
* [第四章 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)
|
||||||
@ -40,7 +39,7 @@
|
|||||||
* [4.5. GRPC进阶](ch4-rpc/ch4-05-grpc-hack.md)
|
* [4.5. GRPC进阶](ch4-rpc/ch4-05-grpc-hack.md)
|
||||||
* [4.6. GRPC和Protobuf扩展](ch4-rpc/ch4-06-grpc-ext.md)
|
* [4.6. GRPC和Protobuf扩展](ch4-rpc/ch4-06-grpc-ext.md)
|
||||||
* [4.7. pbgo: 基于Protobuf的框架](ch4-rpc/ch4-07-pbgo.md)
|
* [4.7. pbgo: 基于Protobuf的框架](ch4-rpc/ch4-07-pbgo.md)
|
||||||
* [4.8. 补充说明](ch4-rpc/ch4-08-faq.md)
|
* [4.8. 补充说明](ch4-rpc/ch4-08-ext.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)
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
# 附录B:参考资料
|
|
||||||
|
|
||||||
## 参考网站
|
|
||||||
|
|
||||||
- Go语言官网: https://golang.org
|
|
||||||
- SWIG官网: http://swig.org
|
|
||||||
- GopherJS官网: http://www.gopherjs.org
|
|
||||||
- GRPC官网: http://www.grpc.io
|
|
||||||
- rsc博客: http://research.swtch.com
|
|
||||||
|
|
||||||
## 参考书目
|
|
||||||
|
|
||||||
- 《Go语言圣经》: https://gopl.io
|
|
||||||
- 《Go语言圣经(中文版)》: https://github.com/golang-china/gopl-zh
|
|
||||||
- 《Go语言·云动力》: http://www.ituring.com.cn/book/1040
|
|
||||||
- 《Go语言编程》: http://www.ituring.com.cn/book/967
|
|
||||||
- 《Go语言程序设计》: http://www.ptpress.com.cn/Book.aspx?id=35714
|
|
||||||
- 《C程序设计语言》: http://product.china-pub.com/14975
|
|
||||||
- 《汇编语言:基于X86处理器》: http://product.china-pub.com/4934543
|
|
||||||
- 《现代x86汇编语言程序设计》: http://product.china-pub.com/5006762
|
|
||||||
- 《深入理解程序设计:使用Linux汇编语言》: http://product.china-pub.com/3768972
|
|
||||||
- 《代码的未来》: http://product.china-pub.com/3767536
|
|
||||||
|
|
105
appendix/draft/ch1-02.md
Normal file
105
appendix/draft/ch1-02.md
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
|
||||||
|
## CGO版本
|
||||||
|
|
||||||
|
Go语言开源初期就支持通过CGO和C语言保持交互。CGO通过导入一个虚拟的`"C"`包来访问C语言中的函数。下面是CGO版本的“Hello World”程序:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
// #include <stdio.h>
|
||||||
|
// #include <stdlib.h>
|
||||||
|
import "C"
|
||||||
|
import "unsafe"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
msg := C.CString("Hello, World!\n")
|
||||||
|
defer C.free(unsafe.Pointer(msg))
|
||||||
|
|
||||||
|
C.fputs(msg, C.stdout)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
先通过`C.CString`函数将Go语言字符串转为C语言字符串,然后调用C语言的`C.fputs`函数向标准输出窗口打印转换后的C字符串。`defer`延迟语句保证程序返回前通过`C.free`释放分配的C字符串。需要注意的是, CGO不支持C语言中的可变参数函数(因为Go语言每次函数调用的栈帧大小是固定的,而且Go语言中可变参数语法只是切片的一个语法糖而已),因此在Go语言中是无法通过CGO访问C语言的`printf`等可变参数函数的。同时,CGO只能访问C语言的函数、变量和简单的宏定义常量,CGO并不支持访问C++语言的符号(C++和C语言符号的名字修饰规则不同,CGO采用C语言的名字修饰规则)。
|
||||||
|
|
||||||
|
其实CGO不仅仅用于在Go语言中调用C语言函数,还可以用于导出Go语言函数给C语言函数调用。在用Go语言编写生成C语言的静、动态库时,也可以用CGO导出对应的接口函数。正是CGO的存在,才保证了Go语言和C语言资源的双向互通,同时保证了Go语言可以继承C语言已有的庞大的软件资产。
|
||||||
|
|
||||||
|
|
||||||
|
## SWIG版本
|
||||||
|
|
||||||
|
Go语言开源初期除了支持通过CGO访问C语言资源外,还支持通过SWIG访问C/C++接口。SWIG是从2010年10月04日发布的SWIG-2.0.1版本开始正式支持Go语言的。可以将SWIG看作一个高级的CGO代码自动生成器,同时通过生成C语言桥接代码增加了对C++类的支持。下面是SWIG版本的"Hello World"程序:
|
||||||
|
|
||||||
|
首先是创建一个`hello.cc`文件,里面有`SayHello`函数用于打印(这里的`SayHello`函数采用C++的名字修饰规则):
|
||||||
|
|
||||||
|
```c++
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
void SayHello() {
|
||||||
|
std::cout << "Hello, World!" << std::endl;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
然后创建一个`hello.swigcxx`文件,以SWIG语法导出上面的C++函数`SayHello`:
|
||||||
|
|
||||||
|
```swig
|
||||||
|
%module main
|
||||||
|
|
||||||
|
%inline %{
|
||||||
|
extern void SayHello();
|
||||||
|
%}
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在Go语言中直接访问`SayHello`函数(首字母自动转为大写字母):
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
hello "."
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
hello.SayHello()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
需要将上述3个文件放到同一个目录中,并且`hello.swigcxx`和Go文件对应同一个包。系统除了需要安装Go语言环境外,还需要安装对应版本的SWIG工具。最后运行`go build`就可以构建了。
|
||||||
|
|
||||||
|
*注: 在Windows系统下, 路径最长为260个字符. 这个程序生成的中间cgo文件可能导致某些文件的绝对路径长度超出Windows系统限制, 可能导致程序构建失败. 这是由于`go build`调用swig和cgo等命令生成中间文件时生成的不合适的超长文件名导致(作者提交ISSUE3358,Go1.8已经修复)。*
|
||||||
|
|
||||||
|
|
||||||
|
## Go汇编语言版本
|
||||||
|
|
||||||
|
Go语言底层使用了自己独有的跨操作系统汇编语言,该汇编语言是从Plan9系统的汇编语言演化而来。Go汇编语言并不能独立使用,它是属于Go语言的一个组成部分,必须以Go语言包的方式被组织。下面是Go汇编语言版本的“Hello World”程序:
|
||||||
|
|
||||||
|
先创建一个`main.go`文件,以Go语言的语法声明包和声明汇编语言对应的函数签名,函数签名不能有函数体:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
func main()
|
||||||
|
```
|
||||||
|
|
||||||
|
然后创建`main_amd64.s`文件,对应Go汇编语言实现AMD64架构的`main`函数:
|
||||||
|
|
||||||
|
```asm
|
||||||
|
#include "textflag.h"
|
||||||
|
#include "funcdata.h"
|
||||||
|
|
||||||
|
// "Hello World!\n"
|
||||||
|
DATA text<>+0(SB)/8,$"Hello Wo"
|
||||||
|
DATA text<>+8(SB)/8,$"rld!\n"
|
||||||
|
GLOBL text<>(SB),NOPTR,$16
|
||||||
|
|
||||||
|
// func main()
|
||||||
|
TEXT ·main(SB), $16-0
|
||||||
|
NO_LOCAL_POINTERS
|
||||||
|
MOVQ $text<>+0(SB), AX
|
||||||
|
MOVQ AX, (SP)
|
||||||
|
MOVQ $16, 8(SP)
|
||||||
|
CALL runtime·printstring(SB)
|
||||||
|
RET
|
||||||
|
```
|
||||||
|
|
||||||
|
代码中`#include "textflag.h"`语句包含运行时库定义的头文件, 里面含有`NOPTR`/`NO_LOCAL_POINTERS`等基本的宏的定义。`DATA`汇编指令用于定义数据,每个数据的宽度必须是1/2/4/8,然后`GLOBL`汇编命令在当前文件内导出`text`变量符号。`TEXT ·main(SB), $16-0`用于定义`main`函数,其中`$16-0`表示`main`函数的帧大小是16个字节(对应string头的大小,用于给`runtime·printstring`函数传递参数),`0`表示`main`函数没有参数和返回值。`main`函数内部通过调用运行时内部的`runtime·printstring(SB)`函数来打印字符串。
|
||||||
|
|
||||||
|
Go汇编语言虽然针对每种CPU架构(主要有386/AMD64/ARM/ARM64等)有对应的指令和寄存器,但是汇编语言的基本语法和函数调用规范是一致的,不同操作系统之间用法是一致的。在Go语言标准库中,`runtime`运行时库、`math`数学库和`crypto`密码相关的函数很多是采用汇编语言实现的。其中`runtime`运行时库中采用部分汇编语言并不完全是为了性能,而是运行时的某些特性功能(比如goroutine上下文的切换等)无法用纯Go实现,因此需要汇编代码实现某些辅助功能。对于普通用户而言,Go汇编语言的最大价值在于性能的优化,对于性能比较关键的地方,可以尝试用Go汇编语言实现终极优化。
|
258
appendix/draft/ch1-06.md
Normal file
258
appendix/draft/ch1-06.md
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
## 控制并发数
|
||||||
|
|
||||||
|
增加的方法如下:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (g gate) Len() int { return len(g) }
|
||||||
|
func (g gate) Cap() int { return cap(g) }
|
||||||
|
|
||||||
|
func (g gate) Idle() bool { return len(g) == 0 }
|
||||||
|
func (g gate) Busy() bool { return len(g) == cap(g) }
|
||||||
|
|
||||||
|
func (g gate) Fraction() float64 {
|
||||||
|
return float64(len(g)) / float64(cap(g))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
然后我们可以在相对空闲的时候处理一些后台低优先级的任务,在并发相对繁忙或超出一定比例的时候提供预警:
|
||||||
|
|
||||||
|
|
||||||
|
```go
|
||||||
|
func New(fs vfs.FileSystem, gate chan bool) *gatefs {
|
||||||
|
p := &gatefs{fs, gate}
|
||||||
|
|
||||||
|
// 后台监控线程
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
switch {
|
||||||
|
case p.gate.Idle():
|
||||||
|
// 处理后台任务
|
||||||
|
case p.gate.Fraction() >= 0.7:
|
||||||
|
// 并发预警
|
||||||
|
default:
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这样我们通过后台线程就可以根据程序的状态动态调整自己的工作模式。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 消费海量的请求
|
||||||
|
|
||||||
|
在前面的生产者、消费者并发模型中,只有当生产者和消费的速度近似相等时才会达到最佳的效果,同时通过引入带缓存的管道可以消除因临时效率波动产生的影响。但是当生产者和消费者的速度严重不匹配时,我们是无法通过带缓存的管道来提高性能的(缓存的管道只能延缓问题发生的时间,无法消除速度差异带来的问题)。当消费者无法及时消费生产者的输出时,时间积累会导致问题越来越严重。
|
||||||
|
|
||||||
|
对于生产者、消费者并发模型,我们当然可以通过降低生产者的产能来避免资源的浪费。但在很多场景中,生产者才是核心对象,它们生产出各种问题或任务单据,这时候产出的问题是必须要解决的、任务单据也是必须要完成的。在现实生活中,制造各种生活垃圾的海量人类其实就是垃圾生产者,而清理生活垃圾的少量的清洁工就是垃圾消费者。在网络服务中,提交POST数据的海量用户则变成了生产者,Web后台服务则对应POST数据的消费者。海量生产者的问题也就变成了:如何构造一个能够处理海量请求的Web服务(假设每分钟百万级请求)。
|
||||||
|
|
||||||
|
在Web服务中,用户提交的每个POST请求可以看作是一个Job任务,而服务器是通过后台的Worker工作者来消费这些Job任务。当面向海量的Job处理时,我们一般可以通过构造一个Worker工作者池来提高Job的处理效率;通过一个带缓存的Job管道来接收新的任务请求,避免任务请求功能无法响应;Job请求接收管道和Worker工作者池通过分发系统来衔接。
|
||||||
|
|
||||||
|
|
||||||
|
我们可以用管道来模拟工作者池:当需要处理一个任务时,先从工作者池取一个工作者,处理完任务之后将工作者返回给工作者池。`WorkerPool`对应工作者池,`Worker`对应工作者。
|
||||||
|
|
||||||
|
```go
|
||||||
|
type WorkerPool struct {
|
||||||
|
workers []*Worker
|
||||||
|
pool chan *Worker
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造工作者池
|
||||||
|
func NewWorkerPool(maxWorkers int) *WorkerPool {
|
||||||
|
p := &WorkerPool{
|
||||||
|
workers: make([]*Worker, maxWorkers)
|
||||||
|
pool: make(chan *Worker, maxWorkers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化工作者
|
||||||
|
for i, _ := range p.workers {
|
||||||
|
worker := NewWorker(0)
|
||||||
|
p.workers[i] = worker
|
||||||
|
p.pool <- worker
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动工作者
|
||||||
|
func (p *WorkerPool) Start() {
|
||||||
|
for _, worker := range p.workers {
|
||||||
|
worker.Start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止工作者
|
||||||
|
func (p *WorkerPool) Stop() {
|
||||||
|
for _, worker := range p.workers {
|
||||||
|
worker.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取工作者(阻塞)
|
||||||
|
func (p *WorkerPool) Get() *Worker {
|
||||||
|
return <-p.pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回工作者
|
||||||
|
func (p *WorkerPool) Put(w *Worker) {
|
||||||
|
p.pool <- w
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
工作者池通过一个带缓存的管道来提高工作者的管理。当所有工作者都在处理任务时,工作者的获取会阻塞自动有工作者可用为止。
|
||||||
|
|
||||||
|
|
||||||
|
`Worker`对应工作者实现,具体任务由后台一个固定的Goroutine完成,和外界通过专有的管道通信(工作者的私有管道也可以选择带有一定的缓存)具体实现如下:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Worker struct {
|
||||||
|
job chan interface{}
|
||||||
|
quit chan bool
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造工作者
|
||||||
|
func NewWorker(maxJobs int) *Worker {
|
||||||
|
return &Worker{
|
||||||
|
job: make(chan interface{}, maxJobs),
|
||||||
|
quit: make(chan bool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动任务
|
||||||
|
func (w *Worker) Start() {
|
||||||
|
p.wg.Add(1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer p.wg.Done()
|
||||||
|
|
||||||
|
for {
|
||||||
|
// 接收任务
|
||||||
|
// 此时工作中已经从工作者池中取出
|
||||||
|
select {
|
||||||
|
case job := <-p.job:
|
||||||
|
// 处理任务
|
||||||
|
|
||||||
|
case <-w.quit:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭任务
|
||||||
|
func (p *Worker) Stop() {
|
||||||
|
p.quit <- true
|
||||||
|
p.wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交任务
|
||||||
|
func (p *Worker) AddJob(job interface{}) {
|
||||||
|
p.job <- job
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
任务的分发系统在`Service`对象中完成:
|
||||||
|
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Service struct {
|
||||||
|
workers *WorkerPool
|
||||||
|
jobs chan interface{}
|
||||||
|
maxJobs int
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(maxWorkers, maxJobs int) *Service {
|
||||||
|
return &Service {
|
||||||
|
workers: NewWorkerPool(maxWorkers),
|
||||||
|
jobs: make(chan interface{}, maxJobs),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Service) Start() {
|
||||||
|
p.jobs = make(chan interface{}, maxJobs)
|
||||||
|
|
||||||
|
p.wg.Add(1)
|
||||||
|
p.workers.Start()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer p.wg.Done()
|
||||||
|
|
||||||
|
for job := range p.jobs:
|
||||||
|
go func(job Job) {
|
||||||
|
// 从工作者池取一个工作者
|
||||||
|
worker := p.workers.Get()
|
||||||
|
|
||||||
|
// 完成任务后返回给工作者池
|
||||||
|
defer p.workers.Put(worker)
|
||||||
|
|
||||||
|
// 提交任务处理(异步)
|
||||||
|
worker.AddJob(job)
|
||||||
|
}(job)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
func (p *Service) Stop() {
|
||||||
|
p.workers.Stop()
|
||||||
|
close(p.jobs)
|
||||||
|
p.wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交任务
|
||||||
|
// 任务管道带较大的缓存, 延缓阻塞的时间
|
||||||
|
func (p *Service) AddJob(job interface{}) {
|
||||||
|
p.jobs <- job
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
主程序可以是一个web服务器:
|
||||||
|
|
||||||
|
|
||||||
|
```go
|
||||||
|
var (
|
||||||
|
MaxWorker = os.Getenv("MAX_WORKERS")
|
||||||
|
MaxQueue = os.Getenv("MAX_QUEUE")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
service := NewService(MaxWorker, MaxQueue)
|
||||||
|
|
||||||
|
service.Start()
|
||||||
|
defer service.Stop()
|
||||||
|
|
||||||
|
// 处理海量的任务
|
||||||
|
http.HandleFunc("/jobs", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Job以JSON格式提交
|
||||||
|
var jobs []Job
|
||||||
|
err := json.NewDecoder(io.LimitReader(r.Body, MaxLength)).Decode(&jobs)
|
||||||
|
if err != nil {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理任务
|
||||||
|
for _, job := range jobs {
|
||||||
|
service.AddJob(job)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 启动web服务
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
基于Go语言特有的管道和Goroutine特性,我们以非常简单的方式设计了一个针对海量请求的处理系统结构。在实际的系统中,用户可以根据任务的具体类型和特性,将管道定义为具体类型以避免接口等动态特性导致的开销。
|
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
Go语言最初由Google公司的Robert Griesemer、Ken Thompson和Rob Pike三个大牛于2007年开始设计发明,设计新语言的最初的洪荒之力来自于对超级复杂的C++11特性的吹捧报告的鄙视,最终的目标是设计网络和多核时代的C语言。到2008年中期,语言的大部分特性设计已经完成,并开始着手实现编译器和运行时,大约在这一年Russ Cox作为主力开发者加入。到了2010年,Go语言已经逐步趋于稳定,并在9月正式发布Go语言并开源了代码。
|
Go语言最初由Google公司的Robert Griesemer、Ken Thompson和Rob Pike三个大牛于2007年开始设计发明,设计新语言的最初的洪荒之力来自于对超级复杂的C++11特性的吹捧报告的鄙视,最终的目标是设计网络和多核时代的C语言。到2008年中期,语言的大部分特性设计已经完成,并开始着手实现编译器和运行时,大约在这一年Russ Cox作为主力开发者加入。到了2010年,Go语言已经逐步趋于稳定,并在9月正式发布Go语言并开源了代码。
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Go语言很多时候被描述为“类C语言”,或者是“21世纪的C语言”。从各种角度看,Go语言确实是从C语言继承了相似的表达式语法、控制流结构、基础数据类型、调用参数传值、指针等诸多编程思想,还有彻底继承和发扬了C语言简单直接的暴力编程哲学等。下面是《Go语言圣经》中给出的Go语言的基因图谱,我们可以从中看到有哪些编程语言对Go语言产生了影响。
|
Go语言很多时候被描述为“类C语言”,或者是“21世纪的C语言”。从各种角度看,Go语言确实是从C语言继承了相似的表达式语法、控制流结构、基础数据类型、调用参数传值、指针等诸多编程思想,还有彻底继承和发扬了C语言简单直接的暴力编程哲学等。下面是《Go语言圣经》中给出的Go语言的基因图谱,我们可以从中看到有哪些编程语言对Go语言产生了影响。
|
||||||
|
|
||||||

|

|
||||||
|
@ -285,109 +285,6 @@ func main() {
|
|||||||
|
|
||||||
Go语言终于移除了语句末尾的分号。这是Go语言在2009年11月10号正式开源之后第一个比较重要的语法改进。从1978年C语言教程第一版引入的分号分割的规则到现在,Go语言的作者们花了整整32年终于移除了语句末尾的分号。在这32年的演化的过程中必然充满了各种八卦故事,我想这一定是Go语言设计者深思熟虑的结果(现在Swift等新的语言也是默认忽略分号的,可见分号确实并不是那么的重要)。
|
Go语言终于移除了语句末尾的分号。这是Go语言在2009年11月10号正式开源之后第一个比较重要的语法改进。从1978年C语言教程第一版引入的分号分割的规则到现在,Go语言的作者们花了整整32年终于移除了语句末尾的分号。在这32年的演化的过程中必然充满了各种八卦故事,我想这一定是Go语言设计者深思熟虑的结果(现在Swift等新的语言也是默认忽略分号的,可见分号确实并不是那么的重要)。
|
||||||
|
|
||||||
## CGO版本
|
|
||||||
|
|
||||||
Go语言开源初期就支持通过CGO和C语言保持交互。CGO通过导入一个虚拟的`"C"`包来访问C语言中的函数。下面是CGO版本的“Hello World”程序:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
// #include <stdio.h>
|
|
||||||
// #include <stdlib.h>
|
|
||||||
import "C"
|
|
||||||
import "unsafe"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
msg := C.CString("Hello, World!\n")
|
|
||||||
defer C.free(unsafe.Pointer(msg))
|
|
||||||
|
|
||||||
C.fputs(msg, C.stdout)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
先通过`C.CString`函数将Go语言字符串转为C语言字符串,然后调用C语言的`C.fputs`函数向标准输出窗口打印转换后的C字符串。`defer`延迟语句保证程序返回前通过`C.free`释放分配的C字符串。需要注意的是, CGO不支持C语言中的可变参数函数(因为Go语言每次函数调用的栈帧大小是固定的,而且Go语言中可变参数语法只是切片的一个语法糖而已),因此在Go语言中是无法通过CGO访问C语言的`printf`等可变参数函数的。同时,CGO只能访问C语言的函数、变量和简单的宏定义常量,CGO并不支持访问C++语言的符号(C++和C语言符号的名字修饰规则不同,CGO采用C语言的名字修饰规则)。
|
|
||||||
|
|
||||||
其实CGO不仅仅用于在Go语言中调用C语言函数,还可以用于导出Go语言函数给C语言函数调用。在用Go语言编写生成C语言的静、动态库时,也可以用CGO导出对应的接口函数。正是CGO的存在,才保证了Go语言和C语言资源的双向互通,同时保证了Go语言可以继承C语言已有的庞大的软件资产。
|
|
||||||
|
|
||||||
## SWIG版本
|
|
||||||
|
|
||||||
Go语言开源初期除了支持通过CGO访问C语言资源外,还支持通过SWIG访问C/C++接口。SWIG是从2010年10月04日发布的SWIG-2.0.1版本开始正式支持Go语言的。可以将SWIG看作一个高级的CGO代码自动生成器,同时通过生成C语言桥接代码增加了对C++类的支持。下面是SWIG版本的"Hello World"程序:
|
|
||||||
|
|
||||||
首先是创建一个`hello.cc`文件,里面有`SayHello`函数用于打印(这里的`SayHello`函数采用C++的名字修饰规则):
|
|
||||||
|
|
||||||
```c++
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
void SayHello() {
|
|
||||||
std::cout << "Hello, World!" << std::endl;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
然后创建一个`hello.swigcxx`文件,以SWIG语法导出上面的C++函数`SayHello`:
|
|
||||||
|
|
||||||
```swig
|
|
||||||
%module main
|
|
||||||
|
|
||||||
%inline %{
|
|
||||||
extern void SayHello();
|
|
||||||
%}
|
|
||||||
```
|
|
||||||
|
|
||||||
然后在Go语言中直接访问`SayHello`函数(首字母自动转为大写字母):
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
hello "."
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
hello.SayHello()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
需要将上述3个文件放到同一个目录中,并且`hello.swigcxx`和Go文件对应同一个包。系统除了需要安装Go语言环境外,还需要安装对应版本的SWIG工具。最后运行`go build`就可以构建了。
|
|
||||||
|
|
||||||
*注: 在Windows系统下, 路径最长为260个字符. 这个程序生成的中间cgo文件可能导致某些文件的绝对路径长度超出Windows系统限制, 可能导致程序构建失败. 这是由于`go build`调用swig和cgo等命令生成中间文件时生成的不合适的超长文件名导致(作者提交ISSUE3358,Go1.8已经修复)。*
|
|
||||||
|
|
||||||
## Go汇编语言版本
|
|
||||||
|
|
||||||
Go语言底层使用了自己独有的跨操作系统汇编语言,该汇编语言是从Plan9系统的汇编语言演化而来。Go汇编语言并不能独立使用,它是属于Go语言的一个组成部分,必须以Go语言包的方式被组织。下面是Go汇编语言版本的“Hello World”程序:
|
|
||||||
|
|
||||||
先创建一个`main.go`文件,以Go语言的语法声明包和声明汇编语言对应的函数签名,函数签名不能有函数体:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
func main()
|
|
||||||
```
|
|
||||||
|
|
||||||
然后创建`main_amd64.s`文件,对应Go汇编语言实现AMD64架构的`main`函数:
|
|
||||||
|
|
||||||
```asm
|
|
||||||
#include "textflag.h"
|
|
||||||
#include "funcdata.h"
|
|
||||||
|
|
||||||
// "Hello World!\n"
|
|
||||||
DATA text<>+0(SB)/8,$"Hello Wo"
|
|
||||||
DATA text<>+8(SB)/8,$"rld!\n"
|
|
||||||
GLOBL text<>(SB),NOPTR,$16
|
|
||||||
|
|
||||||
// func main()
|
|
||||||
TEXT ·main(SB), $16-0
|
|
||||||
NO_LOCAL_POINTERS
|
|
||||||
MOVQ $text<>+0(SB), AX
|
|
||||||
MOVQ AX, (SP)
|
|
||||||
MOVQ $16, 8(SP)
|
|
||||||
CALL runtime·printstring(SB)
|
|
||||||
RET
|
|
||||||
```
|
|
||||||
|
|
||||||
代码中`#include "textflag.h"`语句包含运行时库定义的头文件, 里面含有`NOPTR`/`NO_LOCAL_POINTERS`等基本的宏的定义。`DATA`汇编指令用于定义数据,每个数据的宽度必须是1/2/4/8,然后`GLOBL`汇编命令在当前文件内导出`text`变量符号。`TEXT ·main(SB), $16-0`用于定义`main`函数,其中`$16-0`表示`main`函数的帧大小是16个字节(对应string头的大小,用于给`runtime·printstring`函数传递参数),`0`表示`main`函数没有参数和返回值。`main`函数内部通过调用运行时内部的`runtime·printstring(SB)`函数来打印字符串。
|
|
||||||
|
|
||||||
Go汇编语言虽然针对每种CPU架构(主要有386/AMD64/ARM/ARM64等)有对应的指令和寄存器,但是汇编语言的基本语法和函数调用规范是一致的,不同操作系统之间用法是一致的。在Go语言标准库中,`runtime`运行时库、`math`数学库和`crypto`密码相关的函数很多是采用汇编语言实现的。其中`runtime`运行时库中采用部分汇编语言并不完全是为了性能,而是运行时的某些特性功能(比如goroutine上下文的切换等)无法用纯Go实现,因此需要汇编代码实现某些辅助功能。对于普通用户而言,Go汇编语言的最大价值在于性能的优化,对于性能比较关键的地方,可以尝试用Go汇编语言实现终极优化。
|
|
||||||
|
|
||||||
|
|
||||||
## 你好, 世界! - V2.0
|
## 你好, 世界! - V2.0
|
||||||
|
|
||||||
|
@ -318,34 +318,6 @@ func main() {
|
|||||||
|
|
||||||
在发布订阅模型中,每条消息都会传送给多个订阅者。发布者通常不会知道、也不关心哪一个订阅者正在接收主题消息。订阅者和发布者可以在运行时动态添加是一种松散的耦合关心,这使得系统的复杂性可以随时间的推移而增长。在现实生活中,不同城市的象天气预报之类的应用就可以应用这个并发模式。
|
在发布订阅模型中,每条消息都会传送给多个订阅者。发布者通常不会知道、也不关心哪一个订阅者正在接收主题消息。订阅者和发布者可以在运行时动态添加是一种松散的耦合关心,这使得系统的复杂性可以随时间的推移而增长。在现实生活中,不同城市的象天气预报之类的应用就可以应用这个并发模式。
|
||||||
|
|
||||||
## 赢者为王
|
|
||||||
|
|
||||||
采用并发编程的动机有很多:并发编程可以简化问题,比如一类问题对应一个处理线程会更简单;并发编程还可以提升性能,在一个多核CPU上开2个线程一般会比开1个线程快一些。其实对于提升性能而言,程序并不是简单地运行速度快就表示用户体验好的;很多时候程序能快速响应用户请求才是最重要的,当没有用户请求需要处理的时候才合适处理一些低优先级的后台任务。
|
|
||||||
|
|
||||||
假设我们想快速地检索“golang”相关的主题,我们可能会同时打开Bing、Google或百度等多个检索引擎。当某个检索最先返回结果后,就可以关闭其它检索页面了。因为受限于网络环境和检索引擎算法的影响,某些检索引擎可能很快返回检索结果,某些检索引擎也可能遇到等到他们公司倒闭也没有完成检索的情况。我们可以采用类似的策略来编写这个程序:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func main() {
|
|
||||||
ch := make(chan string, 32)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
ch <- searchByBing("golang")
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
ch <- searchByGoogle("golang")
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
ch <- searchByBaidu("golang")
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(<-ch)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
首先,我们创建了一个带缓存的管道,管道的缓存数目要足够大,保证不会因为缓存的容量引起不必要的阻塞。然后我们开启了多个后台线程,分别向不同的检索引擎提交检索请求。当任意一个检索引擎最先有结果之后,都会马上将结果发到管道中(因为管道带了足够的缓存,这个过程不会阻塞)。但是最终我们只从管道取第一个结果,也就是最先返回的结果。
|
|
||||||
|
|
||||||
通过适当开启一些冗余的线程,尝试用不同途径去解决同样的问题,最终以赢者为王的方式提升了程序的相应性能。
|
|
||||||
|
|
||||||
## 控制并发数
|
## 控制并发数
|
||||||
|
|
||||||
很多用户在适应了Go语言强大的并发特性之后,都倾向于编写最大并发的程序,因为这样似乎可以提供最大的性能。在现实中我们行色匆匆,但有时却需要我们放慢脚步享受生活,并发的程序也是一样:有时候我们需要适当地控制并发的程度,因为这样不仅仅可给其它的应用/任务让出/预留一定的CPU资源,也可以适当降低功耗缓解电池的压力。
|
很多用户在适应了Go语言强大的并发特性之后,都倾向于编写最大并发的程序,因为这样似乎可以提供最大的性能。在现实中我们行色匆匆,但有时却需要我们放慢脚步享受生活,并发的程序也是一样:有时候我们需要适当地控制并发的程度,因为这样不仅仅可给其它的应用/任务让出/预留一定的CPU资源,也可以适当降低功耗缓解电池的压力。
|
||||||
@ -381,6 +353,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
不过`gatefs`对此做一个抽象类型`gate`,增加了`enter`和`leave`方法分别对应并发代码的进入和离开。当超出并发数目限制的时候,`enter`方法会阻塞直到并发数降下来为止。
|
不过`gatefs`对此做一个抽象类型`gate`,增加了`enter`和`leave`方法分别对应并发代码的进入和离开。当超出并发数目限制的时候,`enter`方法会阻塞直到并发数降下来为止。
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@ -392,6 +365,7 @@ func (g gate) leave() { <-g }
|
|||||||
|
|
||||||
`gatefs`包装的新的虚拟文件系统就是将需要控制并发的方法增加了`enter`和`leave`调用而已:
|
`gatefs`包装的新的虚拟文件系统就是将需要控制并发的方法增加了`enter`和`leave`调用而已:
|
||||||
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type gatefs struct {
|
type gatefs struct {
|
||||||
fs vfs.FileSystem
|
fs vfs.FileSystem
|
||||||
@ -405,45 +379,37 @@ func (fs gatefs) Lstat(p string) (os.FileInfo, error) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
我们不仅可以控制最大的并发数目,而且可以通过带缓存Channel的使用量和最大容量比例来判断程序运行的并发率。当管道为空的时候可以认为是空闲状态,当管道满了时任务是繁忙状态,这对于后台一些低级任务的运行是有参考价值的。增加的方法如下:
|
我们不仅可以控制最大的并发数目,而且可以通过带缓存Channel的使用量和最大容量比例来判断程序运行的并发率。当管道为空的时候可以认为是空闲状态,当管道满了时任务是繁忙状态,这对于后台一些低级任务的运行是有参考价值的。
|
||||||
|
|
||||||
|
|
||||||
|
## 赢者为王
|
||||||
|
|
||||||
|
采用并发编程的动机有很多:并发编程可以简化问题,比如一类问题对应一个处理线程会更简单;并发编程还可以提升性能,在一个多核CPU上开2个线程一般会比开1个线程快一些。其实对于提升性能而言,程序并不是简单地运行速度快就表示用户体验好的;很多时候程序能快速响应用户请求才是最重要的,当没有用户请求需要处理的时候才合适处理一些低优先级的后台任务。
|
||||||
|
|
||||||
|
假设我们想快速地检索“golang”相关的主题,我们可能会同时打开Bing、Google或百度等多个检索引擎。当某个检索最先返回结果后,就可以关闭其它检索页面了。因为受限于网络环境和检索引擎算法的影响,某些检索引擎可能很快返回检索结果,某些检索引擎也可能遇到等到他们公司倒闭也没有完成检索的情况。我们可以采用类似的策略来编写这个程序:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (g gate) Len() int { return len(g) }
|
func main() {
|
||||||
func (g gate) Cap() int { return cap(g) }
|
ch := make(chan string, 32)
|
||||||
|
|
||||||
func (g gate) Idle() bool { return len(g) == 0 }
|
|
||||||
func (g gate) Busy() bool { return len(g) == cap(g) }
|
|
||||||
|
|
||||||
func (g gate) Fraction() float64 {
|
|
||||||
return float64(len(g)) / float64(cap(g))
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
然后我们可以在相对空闲的时候处理一些后台低优先级的任务,在并发相对繁忙或超出一定比例的时候提供预警:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func New(fs vfs.FileSystem, gate chan bool) *gatefs {
|
|
||||||
p := &gatefs{fs, gate}
|
|
||||||
|
|
||||||
// 后台监控线程
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
ch <- searchByBing("golang")
|
||||||
switch {
|
}
|
||||||
case p.gate.Idle():
|
go func() {
|
||||||
// 处理后台任务
|
ch <- searchByGoogle("golang")
|
||||||
case p.gate.Fraction() >= 0.7:
|
}
|
||||||
// 并发预警
|
go func() {
|
||||||
default:
|
ch <- searchByBaidu("golang")
|
||||||
time.Sleep(time.Second)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return p
|
fmt.Println(<-ch)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
这样我们通过后台线程就可以根据程序的状态动态调整自己的工作模式。
|
首先,我们创建了一个带缓存的管道,管道的缓存数目要足够大,保证不会因为缓存的容量引起不必要的阻塞。然后我们开启了多个后台线程,分别向不同的检索引擎提交检索请求。当任意一个检索引擎最先有结果之后,都会马上将结果发到管道中(因为管道带了足够的缓存,这个过程不会阻塞)。但是最终我们只从管道取第一个结果,也就是最先返回的结果。
|
||||||
|
|
||||||
|
通过适当开启一些冗余的线程,尝试用不同途径去解决同样的问题,最终以赢者为王的方式提升了程序的相应性能。
|
||||||
|
|
||||||
|
|
||||||
## 素数筛
|
## 素数筛
|
||||||
|
|
||||||
@ -645,215 +611,8 @@ func main() {
|
|||||||
|
|
||||||
现在每个工作者并发体的创建、运行、暂停和退出都是在`main`函数的安全控制之下了。
|
现在每个工作者并发体的创建、运行、暂停和退出都是在`main`函数的安全控制之下了。
|
||||||
|
|
||||||
## 消费海量的请求
|
|
||||||
|
|
||||||
在前面的生产者、消费者并发模型中,只有当生产者和消费的速度近似相等时才会达到最佳的效果,同时通过引入带缓存的管道可以消除因临时效率波动产生的影响。但是当生产者和消费者的速度严重不匹配时,我们是无法通过带缓存的管道来提高性能的(缓存的管道只能延缓问题发生的时间,无法消除速度差异带来的问题)。当消费者无法及时消费生产者的输出时,时间积累会导致问题越来越严重。
|
## context包
|
||||||
|
|
||||||
对于生产者、消费者并发模型,我们当然可以通过降低生产者的产能来避免资源的浪费。但在很多场景中,生产者才是核心对象,它们生产出各种问题或任务单据,这时候产出的问题是必须要解决的、任务单据也是必须要完成的。在现实生活中,制造各种生活垃圾的海量人类其实就是垃圾生产者,而清理生活垃圾的少量的清洁工就是垃圾消费者。在网络服务中,提交POST数据的海量用户则变成了生产者,Web后台服务则对应POST数据的消费者。海量生产者的问题也就变成了:如何构造一个能够处理海量请求的Web服务(假设每分钟百万级请求)。
|
|
||||||
|
|
||||||
在Web服务中,用户提交的每个POST请求可以看作是一个Job任务,而服务器是通过后台的Worker工作者来消费这些Job任务。当面向海量的Job处理时,我们一般可以通过构造一个Worker工作者池来提高Job的处理效率;通过一个带缓存的Job管道来接收新的任务请求,避免任务请求功能无法响应;Job请求接收管道和Worker工作者池通过分发系统来衔接。
|
|
||||||
|
|
||||||
我们可以用管道来模拟工作者池:当需要处理一个任务时,先从工作者池取一个工作者,处理完任务之后将工作者返回给工作者池。`WorkerPool`对应工作者池,`Worker`对应工作者。
|
|
||||||
|
|
||||||
```go
|
|
||||||
type WorkerPool struct {
|
|
||||||
workers []*Worker
|
|
||||||
pool chan *Worker
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构造工作者池
|
|
||||||
func NewWorkerPool(maxWorkers int) *WorkerPool {
|
|
||||||
p := &WorkerPool{
|
|
||||||
workers: make([]*Worker, maxWorkers)
|
|
||||||
pool: make(chan *Worker, maxWorkers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化工作者
|
|
||||||
for i, _ := range p.workers {
|
|
||||||
worker := NewWorker(0)
|
|
||||||
p.workers[i] = worker
|
|
||||||
p.pool <- worker
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动工作者
|
|
||||||
func (p *WorkerPool) Start() {
|
|
||||||
for _, worker := range p.workers {
|
|
||||||
worker.Start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 停止工作者
|
|
||||||
func (p *WorkerPool) Stop() {
|
|
||||||
for _, worker := range p.workers {
|
|
||||||
worker.Stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取工作者(阻塞)
|
|
||||||
func (p *WorkerPool) Get() *Worker {
|
|
||||||
return <-p.pool
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回工作者
|
|
||||||
func (p *WorkerPool) Put(w *Worker) {
|
|
||||||
p.pool <- w
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
工作者池通过一个带缓存的管道来提高工作者的管理。当所有工作者都在处理任务时,工作者的获取会阻塞自动有工作者可用为止。
|
|
||||||
|
|
||||||
`Worker`对应工作者实现,具体任务由后台一个固定的Goroutine完成,和外界通过专有的管道通信(工作者的私有管道也可以选择带有一定的缓存)具体实现如下:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Worker struct {
|
|
||||||
job chan interface{}
|
|
||||||
quit chan bool
|
|
||||||
wg sync.WaitGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构造工作者
|
|
||||||
func NewWorker(maxJobs int) *Worker {
|
|
||||||
return &Worker{
|
|
||||||
job: make(chan interface{}, maxJobs),
|
|
||||||
quit: make(chan bool),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动任务
|
|
||||||
func (w *Worker) Start() {
|
|
||||||
p.wg.Add(1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer p.wg.Done()
|
|
||||||
|
|
||||||
for {
|
|
||||||
// 接收任务
|
|
||||||
// 此时工作中已经从工作者池中取出
|
|
||||||
select {
|
|
||||||
case job := <-p.job:
|
|
||||||
// 处理任务
|
|
||||||
|
|
||||||
case <-w.quit:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭任务
|
|
||||||
func (p *Worker) Stop() {
|
|
||||||
p.quit <- true
|
|
||||||
p.wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交任务
|
|
||||||
func (p *Worker) AddJob(job interface{}) {
|
|
||||||
p.job <- job
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
任务的分发系统在`Service`对象中完成:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Service struct {
|
|
||||||
workers *WorkerPool
|
|
||||||
jobs chan interface{}
|
|
||||||
maxJobs int
|
|
||||||
wg sync.WaitGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewService(maxWorkers, maxJobs int) *Service {
|
|
||||||
return &Service {
|
|
||||||
workers: NewWorkerPool(maxWorkers),
|
|
||||||
jobs: make(chan interface{}, maxJobs),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Service) Start() {
|
|
||||||
p.jobs = make(chan interface{}, maxJobs)
|
|
||||||
|
|
||||||
p.wg.Add(1)
|
|
||||||
p.workers.Start()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer p.wg.Done()
|
|
||||||
|
|
||||||
for job := range p.jobs:
|
|
||||||
go func(job Job) {
|
|
||||||
// 从工作者池取一个工作者
|
|
||||||
worker := p.workers.Get()
|
|
||||||
|
|
||||||
// 完成任务后返回给工作者池
|
|
||||||
defer p.workers.Put(worker)
|
|
||||||
|
|
||||||
// 提交任务处理(异步)
|
|
||||||
worker.AddJob(job)
|
|
||||||
}(job)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
func (p *Service) Stop() {
|
|
||||||
p.workers.Stop()
|
|
||||||
close(p.jobs)
|
|
||||||
p.wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交任务
|
|
||||||
// 任务管道带较大的缓存, 延缓阻塞的时间
|
|
||||||
func (p *Service) AddJob(job interface{}) {
|
|
||||||
p.jobs <- job
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
主程序可以是一个web服务器:
|
|
||||||
|
|
||||||
```go
|
|
||||||
var (
|
|
||||||
MaxWorker = os.Getenv("MAX_WORKERS")
|
|
||||||
MaxQueue = os.Getenv("MAX_QUEUE")
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
service := NewService(MaxWorker, MaxQueue)
|
|
||||||
|
|
||||||
service.Start()
|
|
||||||
defer service.Stop()
|
|
||||||
|
|
||||||
// 处理海量的任务
|
|
||||||
http.HandleFunc("/jobs", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != "POST" {
|
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Job以JSON格式提交
|
|
||||||
var jobs []Job
|
|
||||||
err := json.NewDecoder(io.LimitReader(r.Body, MaxLength)).Decode(&jobs)
|
|
||||||
if err != nil {
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理任务
|
|
||||||
for _, job := range jobs {
|
|
||||||
service.AddJob(job)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OK
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 启动web服务
|
|
||||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
基于Go语言特有的管道和Goroutine特性,我们以非常简单的方式设计了一个针对海量请求的处理系统结构。在实际的系统中,用户可以根据任务的具体类型和特性,将管道定义为具体类型以避免接口等动态特性导致的开销。
|
|
||||||
|
|
||||||
## 更多
|
|
||||||
|
|
||||||
在Go1.7发布时,标准库增加了一个`context`包,用来简化对于处理单个请求的多个Goroutine之间与请求域的数据、超时和退出等操作,官方有博文对此做了专门介绍。我们可以用`context`包来重新实现前面的线程安全退出或超时的控制:
|
在Go1.7发布时,标准库增加了一个`context`包,用来简化对于处理单个请求的多个Goroutine之间与请求域的数据、超时和退出等操作,官方有博文对此做了专门介绍。我们可以用`context`包来重新实现前面的线程安全退出或超时的控制:
|
||||||
|
|
||||||
@ -934,7 +693,7 @@ func main() {
|
|||||||
fmt.Printf("%v: %v\n", i+1, prime)
|
fmt.Printf("%v: %v\n", i+1, prime)
|
||||||
ch = PrimeFilter(ctx, ch, prime) // 基于新素数构造的过滤器
|
ch = PrimeFilter(ctx, ch, prime) // 基于新素数构造的过滤器
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
## 1.8. 扩展阅读
|
## 1.8. 补充说明
|
||||||
|
|
||||||
|
本书定位是Go语言进阶图书,因此读者需要有一定的Go语言基础。如果对Go语言不太了解,作者推荐通过以下资料开始学习Go语言。首先是安装Go语言环境,然后通过`go tool tour`命令打开“A Tour of Go”教程学习。在学习“A Tour of Go”教程的同时,可以阅读Go语言官方团队出版的[《The Go Programming Language》](http://www.gopl.io/)教程。[《The Go Programming Language》](http://www.gopl.io/)在国内Go语言社区被称为Go语言圣经,它将带你系统地学习Go语言。在学习的同时可以尝试用Go语言解决一些小问题,如果遇到要差异API的适合可以通过godoc命令打开自带的文档查询。Go语言本身不仅仅包含了所有的文档,也包含了所有标准库的实现代码,这是第一手的最权威的Go语言资料。我们此时你应该已经可以熟练使用Go语言了。
|
||||||
|
|
||||||
TODO
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# 2.11. 编译和链接参数
|
# 2.10. 编译和链接参数
|
||||||
|
|
||||||
编译和链接参数是每一个C/C++程序员需要经常面对的问题。构建每一个C/C++应用均需要经过编译和链接两个步骤,CGO也是如此。
|
编译和链接参数是每一个C/C++程序员需要经常面对的问题。构建每一个C/C++应用均需要经过编译和链接两个步骤,CGO也是如此。
|
||||||
本节我们将简要讨论CGO中经常用到的编译和链接参数的用法。
|
本节我们将简要讨论CGO中经常用到的编译和链接参数的用法。
|
@ -1,6 +1,6 @@
|
|||||||
# 2.12. 补充说明
|
## 2.11. 补充说明
|
||||||
|
|
||||||
|
CGO是C语言和Go语言混合编程的技术,因此要想熟练地使用CGO需要了解这两门语言。C语言推荐两本书:第一本是C语言之父编写的《C程序设计语言》;第二本是讲述C语言模块化编程的《C语言接口与实现:创建可重用软件的技术》。Go语言推荐官方出版的《The Go Programming Language》和Go语言自带的全部文档和全部代码。
|
||||||
|
|
||||||
为何要话费巨大的精力学习CGO是一个问题。任何技术和语言都有它自身的优点和不足,Go语言不是银弹,它无法解决全部问题。而通过CGO可以继承C/C++将近半个世纪的软件遗产,通过CGO可以用Go给其它系统写C接口的共享库,通过CGO技术可以让Go语言编写的代码可以很好地融入现有的软件生态——而现在的软件正式建立在C/C++语言之上的。因此说CGO是一个保底的后备技术,它是Go的一个重量级的替补技术,值得任何一个严肃的Go语言开发人员学习。
|
为何要话费巨大的精力学习CGO是一个问题。任何技术和语言都有它自身的优点和不足,Go语言不是银弹,它无法解决全部问题。而通过CGO可以继承C/C++将近半个世纪的软件遗产,通过CGO可以用Go给其它系统写C接口的共享库,通过CGO技术可以让Go语言编写的代码可以很好地融入现有的软件生态——而现在的软件正式建立在C/C++语言之上的。因此说CGO是一个保底的后备技术,它是Go的一个重量级的替补技术,值得任何一个严肃的Go语言开发人员学习。
|
||||||
|
|
||||||
本章讨论了CGO的一些常见用法,并给出相关的例子。关于CGO有几点补充:如果有纯Go的解决方法就不要使用CGO;CGO中涉及的C和C++构建问题非常繁琐;CGO有一定的限制无法实现解决全部的问题;不要试图越过CGO的一些限制。而且CGO只是一种官方提供并推荐的Go语言和C/C++交互的方法。如果是使用的gccgo的版本,可以通过gccgo的方式实现Go和C/C++的交互。同时SWIG也是一种选择,并对C++诸多特性提供了支持。
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
|||||||
## 2.12. 扩展阅读
|
|
||||||
|
|
||||||
TODO
|
|
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
首先是调用函数前,转变的输入参数和返回值空间。然后CALL指令将首先触发返回地址入栈操作。在进入到被调用函数内之后,汇编器自动插入了BP寄存器相关的指令,因此BP寄存器和返回地址是紧挨着的。再下面就是当前函数的局部变量的空间,包含再次调用其它函数需要准备的调用参数空间。被调用的函数执行RET返回指令时,先从栈恢复BP和SP寄存器,接着取出的返回地址跳转到对应的指令执行。
|
首先是调用函数前准备的输入参数和返回值空间。然后CALL指令将首先触发返回地址入栈操作。在进入到被调用函数内之后,汇编器自动插入了BP寄存器相关的指令,因此BP寄存器和返回地址是紧挨着的。再下面就是当前函数的局部变量的空间,包含再次调用其它函数需要准备的调用参数空间。被调用的函数执行RET返回指令时,先从栈恢复BP和SP寄存器,接着取出的返回地址跳转到对应的指令执行。
|
||||||
|
|
||||||
|
|
||||||
## 高级汇编语言
|
## 高级汇编语言
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
## 3.9. 扩展阅读
|
## 3.9. 补充说明
|
||||||
|
|
||||||
|
如果是纯粹学习汇编语言,则可以从《深入理解程序设计:使用Linux汇编语言》开始,该书讲述了如何以C语言的思维变现汇编程序。如果是学习X86汇编,则可以从《汇编语言:基于x86处理器》一开始,然后再结合《现代x86汇编语言程序设计》学习AVX等高级汇编指令的使用。
|
||||||
|
|
||||||
|
Go汇编语言的官方文档非常匮乏。其中“A Quick Guide to Go's Assembler”是唯一的一篇系统讲述Go汇编语言的官方文章,该文章中又引入了另外两篇Plan9的文档:A Manual for the Plan 9 assembler 和 Plan 9 C Compilers。Plan9的两篇文档分别讲述了汇编语言以及和汇编有关联的C语言编译器的细节。看过这几篇文档之后会对Go汇编语言有了一些模糊的概念,剩下的就是在实战中通过代码学习了。
|
||||||
|
|
||||||
|
Go语言的编译器和汇编器都带了一个`-S`参数,可以查看生成的最终目标代码。通过对比目标代码和原始的Go语言或Go汇编语言代码的差异可以加深对底层实现的理解。同时Go语言连接器的实现代码也包含了很多相关的信息。Go汇编语言是依托Go语言的语言,因此理解Go语言的工作原理是也是必要的。比较重要的部分是Go语言runtime和reflect包的实现原理。如果读者了解CGO技术,那么对Go汇编语言的学习也是一个巨大的帮助。最后是要了解syscall包是如何实现系统调用的。
|
||||||
|
|
||||||
|
得益于Go语言的设计,Go汇编语言的优势也非常明显:跨操作系统、不同CPU之间的用法也非常相似、支持C语言预处理器、支持模块。同时Go汇编语言也存在很多不足:它不是一个独立的语言,底层需要依赖Go语言甚至操作系统;很多高级特性很难通过手工汇编完成。虽然Go语言官方尽量保持Go汇编语言简单,但是汇编语言是一个比较大的话题,大到足以写一本Go汇编语言的教程。本章的目的是让大家对Go汇编语言简单入门,在看到底层汇编代码的时候不会一头雾水,在某些遇到性能受限制的场合能够通过Go汇编突破限制。
|
||||||
|
|
||||||
TODO
|
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
# 3.9. 补充说明
|
|
||||||
|
|
||||||
得益于Go语言的设计,Go汇编语言的优势也非常明显:跨操作系统、不同CPU之间的用法也非常相似、支持C语言预处理器、支持模块。同时Go汇编语言也存在很多不足:它不是一个独立的语言,底层需要依赖Go语言甚至操作系统;很多高级特性很难通过手工汇编完成。虽然Go语言官方尽量保持Go汇编语言简单,但是汇编语言是一个比较大的话题,大到足以写一本Go汇编语言的教程。本章的目的是让大家对Go汇编语言简单入门,在看到底层汇编代码的时候不会一头雾水,在某些遇到性能受限制的场合能够通过Go汇编突破限制。这只是一个开始,后续版本会继续完善。
|
|
||||||
|
|
||||||
<!--
|
|
||||||
未解的问题:
|
|
||||||
defer/go 工作原理
|
|
||||||
闭包掉工作原理
|
|
||||||
接口结构和原理
|
|
||||||
通过接口调用方法
|
|
||||||
AVX512指令集
|
|
||||||
JIT动态生成代码
|
|
||||||
直接调用C函数(假设栈足够)
|
|
||||||
-->
|
|
@ -1,3 +1,5 @@
|
|||||||
## 4.8. 扩展阅读
|
## 4.8. 补充说明
|
||||||
|
|
||||||
|
目前专门讲述RPC的图书比较少。目前Protobuf和GRPC的官网都提供了详细的参考资料和例子。本章重点讲述了Go标准库的RPC和基于Protobuf衍生的GRPC框架,同时也简单展示了如何自己定制一个RPC框架。之所以聚焦在这几个有限的主题,是因为这几个技术都是Go语言团队官方在进行维护,和Go语言契合也最为默契。不过RPC依然是一个庞大的主题,足以单独成书。目前开源世界也有很多富有特色的RPC框架,还有针对分布式系统进行深度定制的RPC系统,用户可以根据自己实际需求选择合适的工具。
|
||||||
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
# 4.8. 补充说明
|
|
||||||
|
|
||||||
本章重点讲述了Go标准库的RPC和基于Protobuf衍生的GRPC框架,同时也简单展示了如何自己定制一个RPC框架。之所以聚焦在这几个有限的主题,是因为这几个技术都是Go语言团队官方在进行维护,和Go语言契合也最为默契。不过RPC依然是一个庞大的主题,足以单独成书。目前开源世界也有很多富有特色的RPC框架,还有针对分布式系统进行深度定制的RPC系统,用户可以根据自己实际需求选择合适的工具。
|
|
||||||
|
|
||||||
<!-- Nginx 补充说明 -->
|
|
Loading…
x
Reference in New Issue
Block a user