第一章 基础
切片技巧-排序
注意到:浮点数在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()
}
尝试锁:抢锁失败时放弃流程。(用大小为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。
Comments NOTHING