第一章 基础

切片技巧-排序

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

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

((*[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方法

并发模型

生产者/消费者模型

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

发布/订阅模型

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 %sn", msg)
        }
    }()

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

    time.Sleep(3 * time.Second)

    print("Hello,world")
}

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

并发素数筛

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("%dn", 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基础

示例代码

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: %sn",s)
}

CGO语句

//#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来获得实际的地址。

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

Golang指针转换

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

Golang处理C中的异常

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

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

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

#include<errno.h>

static int div(int a,int b){
    if(b==0){
        errno=EINVAL;
        return 0;
    }
    return a/b;
}
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接口规范一般分成了三个部分:服务的名字,服务要实现的详细的方法列表(接口形式),注册该类型的服务的函数。

常用函数

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

跨语言RPC

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

代码实现如下:

Server端

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: %vn",err)
    }
    conn,err := listen.Accept()
    if err != nil{
        log.Fatalf("Accept err: %v",err)
    }
    rpc.ServeCodec(jsonrpc.NewServerCodec(conn))

}

Client端

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: %vn",err)
    }

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

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

第五章 Go和Web

请求校验

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

流量限制

漏桶:流出速率固定

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

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

可以用到的库:

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的函数也可以自定义。

分布式锁

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。

It is my final heart.
最后更新于 2022-07-24