《Go语言高级编程》学习笔记

开个新坑,能写多少算多少吧

基本算是写完了。

部分代码实现会同步到Github上。

第一章 基础

切片技巧-排序

注意到:浮点数在IEEE754标准中,如果其有序,那么其对应的整数就是有序的;所以可以用强制类型转换切片来实现高速排序。

(注意Nan和Inf会导致一些问题)

1
2
3
((*[1 << 20]int)(unsafe.Pointer(&a[0])))[:len(a):cap(a)]

(*reflect.SliceHeader)(unsafe.Pointer(&a))

Go 接口限制隐式转换

特有方法

私有方法

可以被嵌入匿名成员来伪造私有方法,从而绕过限制。

Goroutine和系统级线程

Go协程高效原因:启动栈更小(2KB-4KB,线程默认达到了2MB),拥有特殊的调度器(半抢占式,发生阻塞才调度;用户态只保存必要的寄存器)

原子操作

sync.Mutex互斥锁

sync/atomic原子操作包

sync.Once单例模式,使用Do方法

并发模型

生产者/消费者模型

流水线生产,用阻塞管道来实现消费。

发布/订阅模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package main

import (
"fmt"
"strings"
"sync"
"time"
)

type (
Subscriber chan interface{}
topicFunc func(v interface{}) bool
)

type Publisher struct {
m sync.RWMutex
buffer int
timeout time.Duration
subs map[Subscriber]topicFunc
}

func NewPublisher(timeout time.Duration, buffer int) *Publisher {
return &Publisher{
buffer: buffer,
timeout: timeout,
subs: make(map[Subscriber]topicFunc),
}
}

func (p *Publisher) Subscribe() Subscriber {
return p.SubscribeTopic(nil)
}

func (p *Publisher) SubscribeTopic(topic topicFunc) Subscriber {
ch := make(Subscriber, p.buffer)
p.m.Lock()
p.subs[ch] = topic
p.m.Unlock()
return ch
}

func (p *Publisher) Evict(sub Subscriber) {
p.m.Lock()
defer p.m.Unlock()
delete(p.subs, sub)
close(sub)
}

func (p *Publisher) sendTopic(message interface{}, sub Subscriber, topic topicFunc, wg *sync.WaitGroup) {
defer wg.Done()
if topic != nil && !topic(message) {
return
}

select {
case sub <- message:
case <-time.After(p.timeout):
}
}

func (p *Publisher) Close() {
p.m.Lock()
defer p.m.Unlock()
for sub, _ := range p.subs {
delete(p.subs, sub)
close(sub)
}
}

func (p *Publisher) Publish(message interface{}) {
p.m.RLock()
defer p.m.RUnlock()
var wg sync.WaitGroup
for sub, topic := range p.subs {
wg.Add(1)
go p.sendTopic(message, sub, topic, &wg)
}
wg.Wait()
}

func main() {
p := NewPublisher(100*time.Millisecond, 100)

link1 := p.Subscribe()
link2 := p.SubscribeTopic(func(message interface{}) bool {
if s, ok := message.(string); ok {
return strings.Contains(s, "golang")
}
return false
})

p.Publish("gogo")
p.Publish("Oh my new dream")
p.Publish("golang is a good language")

go func() {
for msg := range link1 {
fmt.Printf("Link1 %s\n", msg)
}
}()

go func() {
for msg := range link2 {
fmt.Printf("Link2 %s\n", msg)
}
}()

time.Sleep(3 * time.Second)

print("Hello,world")
}


事件驱动类型的最简易的消息中间件,可以实现发布/订阅模式。

并发素数筛

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

import "fmt"

func Generate() chan int {
ch := make(chan int)
go func() {
for i := 2; ; i++ {
ch <- i
}
}()
return ch
}

func PrimeFilter(in <-chan int, prime int) chan int { //这里的in是只读管道
out := make(chan int)
go func() {
for {
if i := <-in; i%prime != 0 {
out <- i
}
}
}()
return out
}

func main() {
q := Generate()
for i := 1; i <= 100; i++ {
prime := <-q
fmt.Printf("%d\n", prime)
q = PrimeFilter(q, prime)
}
}

每个数字一定被PrimeFilter启动的协程筛过。本质上是个套娃的过程,每次开的新的管道代替原来的管道,中间加上了过滤器。

goroutine控制并发

利用vfs/gatefs来实现控制并发数。本质上还是依靠管道阻塞。

通道广播与资源回收

利用通道广播关闭来通知goroutine关闭,通过sync.WaitGroup来等待回收资源。利用select中的default来执行正常操作,通过case来判断退出。

context上下文

通过context上下文控制,我们可以简化操作,一定程度上简化程序,比如代替channel来进行通知结束回收goroutine

异常

recover调用

注意,recover必须直接被defer调用。包装的话,会导致一层正常的执行流程,覆盖掉panic。而不在defer中,则不会被panic找到。因为panic会直接结束函数。

第二章 CGO编程

CGO基础

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

//#include<stdio.h>
//void SayHello(_GoString_ s);
import "C"
import "fmt"

func main(){
C.SayHello("Hello world")
}

//export SayHello
func SayHello(s string){
fmt.Printf("Test: %s\n",s)
}

CGO语句

1
2
3
4
5
6
7
8
9
10
//#cgo 语句可以设置参数,也可以通过条件设置参数。
//#cgo windows CFLAGS: -DX86=1
//#cgo !windows LDFLAGS: -lm

可以设置编译阶段和连接阶段的相关参数。可以通过${SRCDIR}表示当前包目录的绝对路径。

可以通过tag设置编译参数,其中空格代表或,逗号代表与

// +build linux,386 darwin,!cgo

union和enum

操作联合变量在CGO中一般有三种方法:

第一种是在C语言中定义辅助函数

第二种是通过encoding/binary手工解码成员,需要考虑大小端

第三种是通过unsafe包强制转换

枚举类型枚举类型通过C.enum_xxx来进行访问

Golang直接操作C内存空间

对于操作内存空间我们可以使用reflect包配合unsafe.Pointer来获得实际的地址。

1
2
var arrHdr = (*reflect.SliceHeader)(unsafe.Pointer(&arr))
arrHdr.Data = uintptr(unsafe.Pointer(&C.arr[0]))

Golang指针转换

利用unsafe.Pointer作为中间桥接变量来进行转换。

Golang处理C中的异常

可以利用errno.h标准库。在Golang中,常见以下的写法:

1
2
3
if xxx,ok:= func();ok{
...
}

这里的ok,我们可以在CGO中通过errno.h库解决。

1
2
3
4
5
6
7
8
9
#include<errno.h>

static int div(int a,int b){
if(b==0){
errno=EINVAL;
return 0;
}
return a/b;
}
1
2
3
if v,err := C.div(2,0);err!=nil{
print(err)
}

这里如果是void类型的返回值,我们一样可以用这种方法获得错误码。

在Golang中,一个CGO中的void型相当于[0]byte,也就是funcx._Ctype_void

CGO内存模型

注意:Golang的内存分布不是稳定的,在某个goroutine进行扩展的时候内存会被移动到新的地址,从而直接使用CGO进行访问的情况是不安全的。

到118页,先跳到后面读

第四章 RPC和Protobuf

RPC入门

Go RPC规则

只能有两个可序列化的参数,第二个参数是指针类型,返回一个error类型,是公开的方法。

RPC接口规范一般分成了三个部分:服务的名字,服务要实现的详细的方法列表(接口形式),注册该类型的服务的函数。

常用函数

1
rpc.Dial() //客户端拨号服务端的rpc,建立连接用

跨语言RPC

Go语言RPC实现中采取了以下的两个设计:一个是RPC数据打包可以通过插件实现自定义编码解码;另一个是RPC建立在io.ReadWriteCloser上,可以重写通信协议。

代码实现如下:

Server端

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

import (
"fmt"
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)

type HelloService struct {
}

func (h *HelloService) Hello(request string,reply *string) error{
*reply = "hello new world! req = " + request
fmt.Printf("Server get a request.\n")
return nil
}

func main(){
rpc.RegisterName("HelloService",new(HelloService))
listen,err := net.Listen("tcp",":12345")
if err != nil{
log.Fatalf("Listen tcp err: %v\n",err)
}
conn,err := listen.Accept()
if err != nil{
log.Fatalf("Accept err: %v",err)
}
rpc.ServeCodec(jsonrpc.NewServerCodec(conn))

}

Client端

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

import (
"fmt"
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)

func main(){
conn,err := net.Dial("tcp","localhost:12345")
if err != nil{
log.Fatalf("Dial tcp err: %v\n",err)
}

client:= rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))

var reply string
err = client.Call("HelloService.Hello","initialize",&reply)
if err != nil{
log.Fatalf("Client err: %v\n",err)
}
fmt.Println(reply)
}

第五章 Go和Web

请求校验

使用请求检验器可以有效的缓解大量的if带来的问题。

流量限制

漏桶:流出速率固定

令牌桶:匀速添加令牌,有令牌就可以拿走从而可以请求

令牌桶在桶中没有令牌的时候会退化成漏桶。

可以用到的库:

1
github.com/juju/ratelimit

最基本的令牌桶实现可以使用channel,而更高级的实现可以使用类似惰性求值的方式来进行预先计算,同时加锁保证安全性,从而提高性能。

Web项目分层

前端工程化->前后端分离,MVC->MC(V层转移到前端工程)

现代化后端开发:CLD

C-Controller:控制层,服务入口,路由、参数校验、转发

L-Logic/Service:逻辑层,服务逻辑-业务逻辑入口

D-DAO/Repository:数据层,封装下层存储的函数

业务系统迁移

异步化:旧系统独立运行部署;拆解就系统,使用消息中间件维护消息同步,最后积累数据-平滑迁移。

业务流程封装:基于行为的封装,最简单的封装已经有利于接口替换

接口抽象:流程稳定后引入接口,可以使用接口插件化;

Golang的接口符合正交性。

灰度发布和A/B测试

分批次部署发布:1-2-4-8……多次部署,查看程序错误日志,出现错误回滚

业务规则灰度发布:根据用户特征生成散列,对特定的散列结果发布灰度测试功能。需要保证一套业务流程使用的是一套API。在此处使用时,常见的哈希算法是MurmurHash;这种哈希算法性能比较好,且对于规律性强的数据表现更加随机。

第六章 分布式系统

分布式ID生成器

Snowflake算法:41位时间戳(毫秒),5位数据中心ID,5位机器实例ID,12位自增循环ID

Sonyflake算法:39位时间戳(10毫秒),8位自增ID,16位机器实例ID;可以自定义MachineID,默认是本机IP的低16位;检查MachineID的函数也可以自定义。

分布式锁

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

import "sync"



func counter_without_lock(){
var wg sync.WaitGroup
var counter int
counter = 0
for i:=0;i<1000;i++{
wg.Add(1)
go func(){
defer wg.Done()
counter++
}()
}
wg.Wait()
println(counter)
}

func counter_with_lock(){
var wg sync.WaitGroup
var mx sync.Mutex
var counter int
counter = 0
for i:=0;i<1000;i++{
wg.Add(1)
go func(){
defer wg.Done()
mx.Lock()
counter++
mx.Unlock()
}()
}
wg.Wait()
println(counter)
}

func main(){
counter_without_lock()
counter_with_lock()
}

image.png

尝试锁:抢锁失败时放弃流程。(用大小为1的channel模拟,需要用到select default;也可以用标准库中的CAS实现)注意活锁问题,可能会有大量资源浪费在抢锁的状态上。

Redis NX锁:SET NX(原SETNX已被废弃),注意原子操作问题,需要设置超时时间,注意删除锁的时候要判断是否是当前进程加的锁;

Redis Redlock:Redis官方的分布式锁实现,需要多个实例且不能有主从;需要N/2+1个节点加锁成功才会判定成功,否则失败。加锁具有超时时间。

Zookeeper分布式锁:利用通知策略;适合分布式任务调度(粗粒度加锁),不适合高频率持续时间短的加锁操作。

etcd分布式锁:流程:检查有值(没有下一步,否则第三步)-写入值(成功返回结束,写失败下一步)-监视陷入阻塞-发生事件(删除或过期)-回到第一步抢锁

延时任务系统

定时器的一些实现

时间堆:定时检查堆顶元素,判断当前时间是否超过,超过就弹出然后处理事件;Golang的定时器实现是四叉堆

时间轮:转动,查询刻度中是否有事件没做,是就做,否则持续转动;

数据再平衡

使用基于Elasticsearch的策略,主从副本-只有主副本被执行。可以用协调节点进行额外的分配工作。

分布式搜索引擎

业务需求:订单查询-高维度数据查询需求,可以接受一定的写入延迟;关键字查询,过多的条目数

Elasticsearch

倒排列表:非数值字段bi-gram分词(T(i)和T(i+1)组成一个词);本质是对多个排好序的字段来求交集。时间复杂度O(N*M),N为最短的列表长度,M为列表个数。

查询DSL:Bool Must逻辑表达与,Bool should逻辑表达或;把SQL转换成ES语句,可以用广度优先算法对AST遍历然后二元表达式转换,拼装即可;可参考github上面的elasticsql。

异构数据同步

时间戳同步

Binlog同步

负载均衡

洗牌算法负载均衡:每次洗牌后选择第一个值当成选择的服务节点;写在服务端,可以每次启动的种子一样;

Zookeeper负载均衡:需要每次Client的第一个请求实现随机化;写在客户端,需要启动种子不同。

分布式配置管理

etcd拉取配置;配置更新订阅。

例子后面再写。

分布式爬虫

单机爬虫:gocolly

分布式爬虫:nats,基于Go的高性能消息队列。可以结合nats和colly来实现分布式爬虫。

例子后面再写。

写在最后

算是匆忙的略读了这本书,收获还是挺大的。但是因为是略读,所以有些细节的体会就不够深刻,这个后面再通过实践来弥补吧。暂时先这样;后面有机会,开始学习Spark和Presto。


《Go语言高级编程》学习笔记
http://hexo.init-new-world.com/advanced-go-programming-study-notes
Author
John Doe
Posted on
March 27, 2021
Licensed under