1 2 心得: 1.错误处理很关键,前期可以只打印日志,后面还需要写入数据库或者发邮件等等
Go重写Redis中间件 Runtime介绍 1.Go语言的优势
不像Java还有一个JVM层,和C++一样直接编译成二进制让机器执行
自带运行环境,无需处理GC问题
一次编码,多套环境使用
天生支持高性能并发
2.Runtime的概念
是go的一个官方库,也是go程序的运行环境,用来支撑go代码的正常运行,相当于Java的JVM
编译成二进制时,会自动把Runtime作为程序的一部分打包进去
Runtime实际上也就是一堆google开发者写的代码,和用户程序同时运行的,也可以直接通过函数来调用
3.Runtime的功能
内存管理:自动为变量分配内存
垃圾回收:回收堆上的空间
超强的并发能力:协程的实现
有一定屏蔽系统调用的能力
go、new、make、<-等其实都是去调用Runtime的函数
4.Go的编译
编译指令是go build
.a文件是compile编译过程中出现的机器码的中间文件,后续需要link成exe文件
编译过程
词法、句法、语义分析
生成平台无关的中间码SSA(类似汇编语言)1 2 3 4 5 # 查看SSA可以依次执行下列命令 $env:GOSSAFUNC="main" export GOSSAFUNC=main go build # 加-n表示不实际编译,只打印出编译的过程
生成二进制的机器码.a文件
链接生成.exe文件
5.go程序运行
1.运行rt0文件。go程序的入口底层上是runtime库的rt0_xx.s汇编文件,而不是main.go
rt意思是runtime,0表示入口,rt0有多个文件不同的操作系统、芯片用不同的rt0文件
2.起g0协程。g0是调度协程的协程,即母协程,是第一个协程
3.启动主协程,并放入调度器等待调。用来来运行main.go
执行runtime包的init()方法
启动GC垃圾收集器
执行用户包依赖的init()方法
执行用户的main()方法
6.细节
argc存参数的数量,argv存参数的值,args泛指参数
ide中双击shift只会查找有意义的变量和方法,即它会忽略汇编文件里的成员,要找的话要再选file选项去找
7.在struct中只声明类型没指定名字的变量,称为匿名字段,go会自动到里面去调用函数(因为没有成员名,算一种语法糖)
8.Go的接口其实就是抽象出来的一组方法,哪个struct里实现了里面的所有方法,就说这个struct实现了该接口
9.一些包管理的方法
在go.mod文件里用如下语句可以把对应的包换成本地存在的库1 replace github.com/Jeffail/tunny => xx/xx
执行以下命令可以把依赖的包都缓存到本地,以免go mod的时候去网上拉1 2 3 go mod vendor //也可以 go build -mod vendor
自动向远程仓库推送生成go.mod文件1 go mod init github.com/xx/xx
Go的数据结构底层 10.go的数据结构
指针大小和int保持一致,32位系统4字节,64位系统8字节
独立的空结构体的大小是0,但是也是有一个地址的,所有的空结构体共有这个地址,该地址叫zerobase
该设计用于节省内存,但是如果一个父结构体包含了空结构体,那么这个空结构体就不能算独立了,它的地址就要跟着它的兄弟结构体了,还有内存补齐等问题
set可以用map来声明,将val声明为空结构体1 2 3 set1 := map[string]struct{}{} set1["aa"] = struct{}{} //正常写法应该是map[string]int{},struct{}表示空结构体类型
11.string在底层是由一个unsafe.point指针(本质是byte数组)和一个int组成的结构体,分别指向字符串值地址和存字符串byte长度
注意len(str)的结果是字符串底层字节的长度!!例如“慕a”的len()就是4
rune就是utf8字符的意思,底层是int32
utf-8的文件在runtime/utf8.go里面
12.切片的底层slice由一个unsafe.point指针和两个int类型的len、cap组成,参考C++的vector
声明定义时[]写了具体数字的就是数组,没写的就是切片
只增加len就只由编译器负责,如果需要扩容增加cap就要去调runtime.growslice()
默认扩容是长度小于1024扩大到2倍,大于1024扩大到0.25倍,除非期望容量比要扩容的还大那就采用期望容量
切片的扩容是并发不安全的,扩容要记得加锁,因为扩容会把老数组丢掉
13.map的底层其实是HashMap实现的
go的map是由链表法实现的,hmap底层又由bucket指针指向一个由bmap结构体组成的数组,每一个bmap放8个哈希值相同的数据
bmap
有tophash存hash值的前8位
有一个overflow指针指向可用的bmap,若超过8个的话就把多的数据根据这个指针传到另一个bmap去
有key和value,最多只能存8个k-v对,注意这里存的不是值,也是key和value的地址
将key值和hash0输入到hash函数里面会得到一个二进制的hash值,然后取后B位作为桶数,例如100010 B=3,则取010,即该key应该放在bucket2里
然后再根据前8位的值作为tophash得到这个key具体在bucket2的哪个位置,tophash的设置就是为了快速定位的(因为是二进制表示节省开销,而用key去匹配可能是string等开销大的类型)
在runtime包里面
开放寻址法和链表法其实差不多,区别只在于开放寻址法的槽存值,而链表法的槽存指针(也叫桶法)
字面量赋值,少于25个之间编译成一个个赋值,多余25个会自动建key数组和val数组然后改写成for赋值
扩容是因为如果哈希值太多,溢出桶指向溢出桶指向溢出桶,这样性能就会大跌
装载因子超过6.5会触发扩容,即每个桶里平均装了6.5个以上数据
溢出桶数量大于普通桶数量也会触发扩容(这里的扩容有可能不增加桶数,只做整理,因为可能之前很多然后删了之后现在数据很少,会很稀疏地存在)
step1:建新的双倍普通桶,然后bucket指向新桶,oldbucktet指向旧桶,并且把该hmap标记为扩容
step2:渐进式转移数据,即每一次数据要写到旧桶时才把这个旧桶的数据转移到新桶里(因为根据B的增加只增加一个高位,所以根据高位是0还是1只分成两部分放入新桶)
step3:当所有旧桶里都迁移完了之后就释放旧桶占据的内存空间(所以会有并发问题)
并发里用map要用sync.Map,里面自动会给读、写加锁
里面有两个map,一个read map,一个dirty map,他们的key都是一样的并且都指向同一组val地址
正常的读、修改都走read这个map;发生追加的时候走dirty这个map,mutex锁会去锁dirty,同一时间只允许一个协程去操作dirty
每次去read读读不到并且read的mended为true则要去dirty里面找,每发生一次misses+1,当misses==len(dirty)时触发把dirty上升为read,然后之前那个read删掉,新建一个老dirty的副本作为新dirty——当时并不新建,发生追加时才顺便新建
删除则是把执行val的指针置为空,这样过段时间go就会自动回收空间
如果是提升之前dirty里追加的就被删了,那么提升后新dirty就不会再建立这个key
Go的并发 14.常用并发
简单的并发1 2 3 go func(){ fmt.print("123") }
简单的阻塞
简单的利用管道缓冲(以下表示最多同时运行3000个协程)1 2 3 4 5 6 7 8 9 10 11 12 func main(){ c := make(chan struct{}, 3000) for...{ c <- struct{}{} go do(c) } } func do(ch chan struct{}){ time.sleep(time.Hour) <-ch }
简单的加锁:在结构体里新增一个mutex类型成员1 2 3 4 5 6 7 8 9 10 type person struct{ mu sync.mutex name string } func(p *person) promote{ p.mu.Lock() ... p.mu.Unlock() }
另一种加锁方式:用原子操作,这种方法不实用,在并发大的时候已卡住,因为触发不了协程切换1 2 3 4 5 6 7 8 9 10 type person struct{ mu int32 //0表示未锁,1表示已上锁 name string } func(p *person) promote{ atomic.CompareAndSwapInt32(&p.mu, 0, 1); ... atomic.CompareAndSwapInt32(&p.mu, 0, 1); }
15.interface{}的底层,一个实例化的接口有以下特性
接口的数据用runtime.iface表示
iface记录了数据的地址
iface也记录了底层是由什么类型实现的和实现的方法
16.nil不一定是空指针,可能是别的类型的0值。在底层文件builtin里面nil的类型时可变的
空接口不一定是nil,因为如果这个空接口有类型信息的话它就不会是nil
空结构体的指针是zerobase,跟nil没关系
17.以下语句可以获得对齐系数
18.可以调整结构体的成员声明顺序,来节省空间,因为会有字节对齐
结构体的对齐系数是其最大成员对齐系数,和C++一样
空结构体在末尾时要补齐字节,比如前面字节已经对齐了,然后多了一个空结构体,那么也要补齐字长
19.go用协程而不是线程的原因
协程资源开销、切换开销更小,协程本质是一段包含了运行状态的程序
协程并发其实就是用同一个线程先执行协程a的一步计算,把状态保存到协程a,再去切换到协程b计算再保存,又切回协程a继续计算。这样的好处就是cpu一直在一个线程上跑,减少了cpu在线程之间切换带来的开销
协程底层是一个g结构体(g0是母协程),g结构体里包含的gobuf成员的sp堆栈指针记录的是运行到哪个方法了,pc程序计数器记录的是运行到哪一行了
描述线程的底层是m结构体,里面记录了母协程g0、当前协程curg和操作系统的信息mOS
go执行协程会把这个协程要运行的方法依次压入栈中,并且初始化这个栈的时候先压入一个goexit()用于执行完成退出协程
20.go协程在线程中执行的循环过程(一个线程是一个循环)
首先,g0的stack上执行schedule()、execute()、gogo()等
然后,去对应协程的g的stack上执行业务方法
最后,在g的stack上运行到goexit()的时候又返回g0开始下一个执行协程的循环
21.GMP调度模型:用于协程的调度
go以前是用一个有全局锁的协程队列实现多线程并发
每个线程循环运行完后,就去队列里拿一个协程进行下一个循环
G是协程,M是线程,P是送料体,每一个P服务一个M,P里面有:
指向本地协程队列头尾的指针,当前协程的指针,下一个协程指针
线程信息
某个P的本地队列用完之后,就会去全局锁队列里面获取一批新的协程作为本地队列
如果全局的队列也用完,会出现任务窃取:从其他本地队列里偷
新建协程时,会随机寻找一个P放入本地队列,若本地队列满再放入全局队列,这是因为go认为新协程更需要先完成
21.协程的并发
如果顺序执行的话,会产生饥饿问题:大协程卡住了线程,后面的协程永远得不到执行
触发切换时,正在线程中循环的协程会被保存现场然后放回队列去
四种触发方式:
gopark(),主动调用或者在方法内执行sleep等操作后go自动调用
系统调用后去执行exitsyscall(),系统调用指的是,比如linux会去定期检查网络连接等
抢占式调度,当被标记为抢占的协程触发了morestack(),就会触发切换
当某个协程运行超过10ms时,系统会把它标记为抢占
垃圾回收器定期让线程去调用doSigPreempt(),注册信号函数
每执行61次协程循环,就去全局队列里去拿一个协程进入本地队列
22.协程的方法a里再去调方法b时,编译器会自动在b()前面去先调用runtime.morestack()
morestack()本质意为去检查协程方法栈里面是否有空间再放入一个b()
23.协程过多
带来问题:
解决办法:
优化业务逻辑
利用channel缓存区:channel大小就是允许同时运行的协程数量
协程池tunny:预创建一定数量协程,然后把任务送入协程池队列,协程池再不断取出可用协程来执行任务
不太推荐,因为go的协程调度已经有池的思想了,再自己套一个池的逻辑会冗余、出问题难以查错
增加系统资源
24.atomic包的原子锁是一种硬件层面的加锁,因为它是用底层汇编语言来实现的,会进行对某块内存加锁
只能用于简单变量的加减操作,不能对结构体、map这种复杂类型使用
25.sema锁是信号量锁,是mutex这些互斥锁、读写锁的底层实现
底层用平衡二叉树来存协程
sema锁为几就表示有几个协程可以同时获取这个sema锁
为0的时候,就会把要获取这个sema锁的协程放进平衡二叉树树里面去休眠
有一种用法是,把sema设为0然后当成一个休眠队列把要休眠的放进去,要用时再对sema赋>0的值
26.mutex的state的最后一位0表示未被锁,1表示被锁
正常模式:默认模式
当一个协程去获取mutex的锁时,若获取不到,则会自旋一段时间后再去获取,还获取不到就放到sema的休眠队列里去
会有饥饿问题,即协程a等锁等了很久,终于锁释放了然后轮到唤醒它去拿锁,但是那一瞬间有新来的协程一起在竞争这个锁,那这样协程a可能永远也得不到执行
饥饿模式(先来后到):当某个协程等锁等了1ms就会进入
新来的协程不自旋,直接进sema休眠
休眠队列里的协程被唤醒时,直接获得这把锁,不用再去和刚来的新队列竞争
当sema队列被清空时,退出饥饿模式,返回正常模式
mutex有3个sema,一个mutex的sema,一个readSema,一个writeSema
27.iota是以此类推的意思,下面一次多加1,是go的语法糖
另一个常用语法糖,在方法后.var可以快速新建变量来接受方法返回值
28.只读锁mutex.RLock():锁上的时候不能再去写,但是还可以去读,即第一个读协程锁上,最后一个读协程释放
加读锁
给readCount+1
如果readCount > 0,表示加锁成功
如果readCount < 0,表示有写锁,要被放入readerSema去等待
解读锁
给readCount-1
如果readCount > 0,表示解锁成功
如果readCount < 0,表示有写协程在writerSema里等待,如果此时自己是readerWait里的最后一个读协程,那么还要去唤醒这个写协程
29.读写锁mutex.Lock()
加读写锁
先加mutex的锁,如果已经有锁了就会阻塞等待
将readCount前面加-号变为负值,阻塞读锁的获取
如果有读协程还未释放,那么就进入writerSema
解写锁
解开mutex的锁
将readCount变为正值,允许读锁的获取
释放在rederSema中等待的读协程
29.加锁的底层
加互斥锁就是去竞争mutex的state的最后一位,希望它由0变成1
读锁是共享锁,写锁是互斥锁
30.开go协程的时候一定要记得:主程序退出会把所有协程都终止!
31.sync.WaitGroup对象:用于实现一组协程要等待另一组协程
Add(int)方法进行设置等待数
Done()方法会结束一个等待
Wait()方法只有在等待数为0时才不阻塞
32.让一段代码全局只执行一次的方法
调用sync包的once,底层也是用mutex互斥锁来实现的1 2 3 4 5 o := sync.Once{} go o.Do(xx.xx()) go o.Do(xx.xx()) ...... //这样无论起多少个协程,xx.xx()全局只被执行一次
33.并发问题的检查工具
锁千万不要去拷贝,因为会连锁状态、sema休眠队列等也一起拷贝的,会有死锁等很多问题
拷贝结构体的时候要注意里面有没有锁mutex的情况
vet检查:用于检测xx.go文件里的锁拷贝等问题,例如拷贝了某个有锁的结构体
race检查:用于检查出文件里的数据竞争问题,例如并发地加某个变量值而不上锁1 2 go build -race xx.go //然后运行xx.go
go-deadlock库:用于检测死锁,实际是检测锁解开的时间来实现的
用go-deadlock包的mutex来取代sync包
channel管道 34.管道的阻塞
以下代码会在第二行阻塞,因为channel没有缓冲区,塞不进去
能运行的情况是有另一个协程阻塞着等着从a里面拿数据1 2 3 a := make(channel int) a <- 1 <- a
使用管道比共享内存好的原因
避免协程竞争和数据冲突问题
减少开销,可以省去循环一遍遍查询,管道是一种信号机制,一旦有值就会通过
更高级的抽象,代码逻辑清晰,更容易模块解耦
原则上:不要通过共享内存的方式来通信,而是要用通信的方式来共享内存
35.channel底层
channel的底层结构体是hchan
指针实现ring buffer,缓存是用环状结构来实现的
环状可以降低垃圾回收GC开销,固定环的长度新增的数据直接放在下一个数据块即可,如果不用环状的话,新增要开辟空间,删除要回收空间
接受队列Receive Queue,用于存放准备读这个channel里数据的协程(休眠等待)————此时ring buffer一定是空的
从这个队列被唤醒时,数据已经被拷贝放到了这个协程里面
发送队列Send Queue,用于存放准备把数据放到这个channel里的协程(休眠等待)————此时ring buffer一定是满的或者没有缓存区
mutex成员,任何对该channel的修改都要去获取mutex
一般人说的“channel无锁”,指的是ring buffer里有位置,数据只有放进去的一瞬间要加锁,很短暂,所以说无锁
36.channel实践
实践中用select搭配channel更方便
因为select可以用case来判断哪个channel里有数据或哪个channel可以塞数据,然后执行不同逻辑
time库的timer对象里的成员C是一个channel,在这个timer对象计时结束后会自动往里面塞一个数据
用netpoll管理socket(go的网络编程是基于epoll的) 37.socket
TCP连接应该要有三次握手四次挥手的,socket是操作系统对这些操作的封装,我们只需操作socket即可实现TCP,不用去具体写什么时候握手什么时候挥手等
socket的id叫文件描述符FD
38.Go封装的epoll
把linux/windows/mac的多路复用统一抽象成Network Poller
底层核心方法netpoll()用来查询发生了什么事件
里面实际是调用epoll_wait()来具体查询的
会返回一个关心这些事件的协程列表pollDesc(链表结构)
pollcache是带锁的链表头,不含具体信息,它后面跟的pollDesc才有数据
pollDesc是链表成员-也是runtime包对socket的详细描述
实际逻辑
首先,runtime里的gcStart()会调用netpoll(),因为gc是周期性的所以可以理解为周期性地调用netpoll()查询,发现某Socket可读写的时候,给对应的rg或wg置为pdReady(1),在g0协程上运行的
然后,当协程运行的时候会去判断rg或wg是否就绪,如果就绪就往下走;如果不就绪就会把这个协程的地址写到rg或wg上,并把协程休眠,等rg或wg就绪的时候就会用这个地址去唤醒协程
39.net包是go原生的网络包,它实现了TCP、UDP、HTTP等网络操作,底层靠NetworkPoller实现
net.Listen()会得到一个TCPListener对象(listen状态的socket),底层如下
新建一个Socket,并执行bind和listen
新建一个FD,FD是net包对socket的描述,相当于runtime包的pollDesc
TCPListener.Accept()得到TCPConn对象(establish状态的socket)
直接调用accept,新生成一个socket来连接
如果失败了就休眠等待
TCPConn.Read()和TCPConn.Write()就可以对socket进行读写了
40.把byte数组转成字符串的方法
1 2 # b是[]byte类型 string(b[:])
41.怎样实现一个协程负责一个socket
死循环里每监听到一个新的连接,就新起一个协程去处理这个连接
即主协程监听listen,然后起新协程去处理连接
这种代码风格也叫goroutine-per-connection编程
Go的GC垃圾回收 42.Go的栈也是从堆上申请的,即不像C++等栈由程序释放,Go的栈依旧由GC释放
堆内存在操作系统的虚拟内存上
函数参数传递顺序和C++一样,从右往左传,因为栈是后进先出
Go天然有一个runtime.main栈帧(一个协程有一个自己的协程栈,多个栈帧组成了一个协程栈)
运行主函数时有一个main.main栈帧紧跟着runtime的栈帧
Go的main()调用一个函数,会在新起一个栈帧跟着main.main的后面(C++的递归太多导致栈溢出就是这个原理)
43.Go解决协程栈溢出的方法
逃逸分析:不是所有的变量都能放在协程栈上,用来减少栈空间不足
指针逃逸:某方法返回了一个指向它局部变量的指针,按理来说这个局部变量应该要被回收,但是指针被传出去了,外面的其他方法可能会用到,所以就把这个局部变量放到堆上(C++的策略是直接回收)
例如a()调用了b(),按理说b()的变量要全删,但是因为b()返回了一个指针给a()使用,那么这个指针指向的变量就不能删,会逃逸到堆上
空接口逃逸(反射导致):某方法用局部变量调用了另一个参数类型是空接口的方法,那么这个局部变量就会逃逸了,因为空接口的方法往往都会用到反射,而反射要求变量需要在堆上
大变量逃逸:太大的变量会导致协程栈空间不足,一般64位的机器超过64kb的变量都直接放堆上
栈扩容:Go之前采用分段栈,原理类似链表;现在采用连续栈,原理类似vector,找一个2倍的空间然后迁移过去
43.Go的堆内存结构,类似tcmalloc-也是C++17推荐用的内存结构
Go申请和释放虚拟内存的单位是64M(一个heapArena),许多个64M组成了Go的用的所有内存(底层用mheap描述)
在heapArena里面给对象分配内存策略
分级分配,给heapArena里的空间分割成不同大小的箱子,每个箱子只能放一个对象,然后把每个对象放到能放下它的最小的箱子
每一级箱子叫做一个mspan,即如果分成2级,就有两个mspan
go最多有68个mspan(0级-67级),是根据对象的内存需求再去分割的,最小8bit,最大32kb
不用线性和链表分配,是因为易出现内存碎片
用mcentral来做mspan的索引,而每个mspan里面有两个mcentral,分别记录需要GC扫描的内存和不需要GC扫描的内存,里面有互斥锁
每个线程P有一个本地的内存mcache做缓存用,里面每个级别的mspan分别有2个(scan和noscan),每次线程要申请空间时不用去中央要,先从自己本地去要,这种结构类似协程调度的GMP模型
即整体结构,heapArena里用mcentral来指向mspan,采用分级分配,线程本地的mspan缓存叫做mcache
44.堆内存的分配
微对象tiny(0,16b),不包含指针。把多个微对象合并成一个16 bite分配到2级mspan,优先使用本地的mcache
小对象small[16b,32kb]。分配到对应等级的mspan,优先使用本地的mcache
大对象large(32kb, 无穷大)。分配到量身定制的0级mspan,即0级mspan没有固定大小,它是根据大对象来改的,只能从中央要
45.GC垃圾回收
“标记-清除”:把需要的内存标记,然后清除,易内存碎片,go采用,因为go用了分级分配,不会有内存碎片问题
标记的对象(GC Root)
被栈上的指针引用
被全局变量指针引用
被寄存器中的指针引用(即os正在操作处理时用的指针)
每次标记的时候会对每个Root Set对象进行bfs,因为如果某个对象是Root Set的话,那么它里面引用的对象也不能清除
GC的策略
“标记-整理”:把需要的内存标记,然后把还在用的内存往前移,开销大,老java用
“标记-复制”:把需要的内存标记,然后只把标记的部分复制到另一个空间,新java用
46.三色标记法-go采用的并发的垃圾回收,并发指的是程序一边运行业务一边进行GC(说是并发,但是其实关键操作的瞬间还是会暂停程序,只不过时间及其短)
含义
黑色:有用,并且已经分析扫描过了
灰色:有用,还未分析扫描
白色:无用或者还没分析到,最后需要清除的对象
GC过程
首先,所有的节点都置为白色
接着,把所有Root Set节点置为灰色
然后,把Root Set引用的所有节点置为灰色
如此循环往复,当灰色队列里面为空时表示扫描完成,开始清除
解决GC对新引用节点标记错误的问题:解决思路都是,保险起见把有改变的节点都置灰
删除屏障(也叫快照标记):把释放的指针指向的对象置灰色
插入屏障:把新增的指针指向的对象置灰色
go采用的是混合屏障,即删除屏障+插入屏障
触发GC方式
sysmon定时检查超过2分钟没GC,自动触发
分配的堆大小达到阈值,自动触发
如果检测GC机制没有启动,启动GC并触发一次
代码里用runtime.GC方法强制触发(不推荐)
47.写代码过程中优化GC的方式
减少堆上垃圾
减少逃逸:因为逃逸会使栈上的对象跑到堆上,不能靠栈来回收,而变成堆上的垃圾等GC来回收,例如fmt这种常会用到反射的包
常用空结构体:因为所有空结构体指向同一个地址,不占用堆空间
GC分析工具1 2 $env:GODEBUG="gctrace=1" gun run main.go
Go的cgo、defer、recover、反射 48.在Go里面调用C写的代码
在package下面,import上面用注释的形式写C的代码1 2 3 /* int sum(int a){return a;} */
使用,先导入包C,然后用C.sum()来使用1 2 import "C" fmt.Println(C.sum(1))
注意执行c程序的时候,不一定快,因为有以下开销
调度器就会暂停不再调度,直到执行完这个c程序
从go的协程栈切换到系统栈上方便c执行
49.defer
碰到defer的时候会把地址记录,然后函数返回的时候调deferreturn()去执行这个地址
一般记录在栈上,如果defer里面有recover就放到堆上
另一种是defer的语句在编译的时候就确定了,那么编译器会直接把defer的内容加在函数末尾,也叫开放编码
50.print和fmt.print区别
print和println输出到标准错误流中并打印,官方不建议写程序时候用它,后期不保证是否会继续保留
fmt.Print,fmt,Println属于官方包fmt中自带的打印方法,属于标准输出流,一般使用它来进行屏幕输出.
51.recover用于从panic中恢复
panic一旦触发会直接终止当前协程,并向调用堆栈的上层函数传播,直到被最外层的recover捕获或者程序结束
如果在协程里panic的话,会触发当前协程的defer,但不会触发主协程main的defer
在当前协程的defer里面写recover(),可以让当前协程因为panic退出时不带崩其他协程1 2 3 4 defer func(){ recover() }() panic("")
52.反射reflect,底层还是把变量的值和类型转换成一个eface结构体表示
获取变量的值和类型1 2 ty := reflect.TypeOf(s) val := reflect.ValueOf(s)
把变量的值还原成变量1 2 str := val.Interface().(string) //意为先转成空结构体,再转成字符串
处理函数,用于框架和用户方法解耦,由框架来调用,用户来实现1 2 3 4 5 6 7 8 9 10 11 func CallAdd(f func(a int, b int) int){ val := reflect.ValueOf(f) if val.Kind() != reflect.Func { return; } argv := make([]reflect.Value, 2) argv[0] = reflect.Value(1) //表示把第一个参数a存到argv[0]里 argv[1] = reflect.Value(2) val.Call(argv) //这一步就会用argv作为参数去调用上面传进来的f()方法 }
Go微服务和容器化 1.微服务是一种架构模式,里面的一个服务仅仅用于一个特定功能,可以单独拿出来用而不依赖其他架构模块
2.Docker搭建微服务
拉取微服务镜像1 sudo docker pull micro/micro
设置工作目录1 2 # user是模块名称,后面会换成git地址 sudo docker run --rm -v $(pwd):$(pwd) -w $(pwd) \ micro/micro new user
3.ProtoBuf,简称pb,后缀名是.proto
概念:本质上是一种序列化数据的协议,常用于存储数据和需要远程数据通信的情景,可以提高数据传输效率和解决数据格式不规范问题
服务端与服务端之间只需要关注接口方法名(service)和参数(message)即可通信,而不需关注繁琐的链路协议和字段解析
数据类型:int32/64、float、double、string、bool、bytes
一个pb实例1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 syntax = "proto3"; package go.micro.service.product; service Product{ rpc AddProduct(ProductInfo) returns (ResponseProduct){} } message ProductInfo{ int64 id = 1; string product_name = 2; } message ResponseProduct{ int64 product_id = 1; }
4.命令行工具micro
5.go-micro的主要组件
注册Registry:提供服务发现机制
选择器Selector:实现负载均衡
传输Transport:服务之间通信
6.一个微服务架构
服务注册
client端通过Slector去服务注册中心发现服务
server端通过Registry去服务注册中心注册服务
客户端请求
client通过Broker发布消息到消息队列里面
server通过Broker从消息队列里面订阅消息
客户端和服务端的信息传输直接用Tranport
7.根据pb文件生成go代码
下载proto-compile,并且注册到环境变量里
安装go的支持插件1 2 go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
运行,根据.proto文件生成.go文件1 protoc --go_out=./test --go-grpc_out=./test test\test.proto
9.安装go-micro
10.一个简单微服务
结构1 2 3 4 5 6 7 rpc_demo - test //该文件夹里go文件是根据.proto文件自动生成的 - test.pb.go - test.proto - test_grpc.pb.go - server.go - client.go
客户端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 func main(){ address := "127.0.0.1:5004" conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock()) if err != nil { fmt.Println(err) return } defer conn.Close() client := test.NewTestClient(conn) message := "hello world" ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() res, err := client.SayHello(ctx, &test.SayRequest{Message: message}) if err != nil { fmt.Println(err) return } fmt.Println(res.Answer) return }
服务端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 type Server struct{ test.UnimplementedTestServer } func (s *Server) SayHello(ctx context.Context, req *test.SayRequest) (*test.ResponseSay, error){ fmt.Println(req.GetMessage()) return &test.ResponseSay{Answer: "你好世界"}, nil } func main(){ listen, err := net.Listen("tcp", "127.0.0.1:5004") if err != nil { fmt.Println(err) return } s := grpc.NewServer() test.RegisterTestServer(s, &Server{}) err = s.Serve(listen) if err != nil { fmt.Println(err) return } return }
11.用docker在git仓库上建模板
拉micro
~~~ sudo docker run –rm -v $(pwd):$(pwd) -w $(pwd) micro/micro new user ~~收益