【笔记】Go网课笔记

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
    1
    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")
    }
  • 简单的阻塞
    1
    select {}
  • 简单的利用管道缓冲(以下表示最多同时运行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也记录了底层是由什么类型实现的和实现的方法
    • 所以当接口有类型信息时,空接口就不会是nil了

16.nil不一定是空指针,可能是别的类型的0值。在底层文件builtin里面nil的类型时可变的

  • 空接口不一定是nil,因为如果这个空接口有类型信息的话它就不会是nil
  • 空结构体的指针是zerobase,跟nil没关系

17.以下语句可以获得对齐系数

1
unsafe.Alignof()

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文件里的锁拷贝等问题,例如拷贝了某个有锁的结构体
    1
    go 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的策略
      • 串行GC:停下其他协程,然后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

  • 安装
    1
    docker pull micro/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

1
go get go-micro.dev/v4

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
    1
    docker pull micro/micro
  • ~~~
    sudo docker run –rm -v $(pwd):$(pwd) -w $(pwd) micro/micro new user
    ~~收益