• golang学习笔记


    语法


    • 内层词法域可以屏蔽外层词法域中的变量
    • 花括号为显式词法域
    • for,switch,if的条件部分会创建隐式词法域

    rune=int32

    byte=uint8

    运算符优先级

    *      /      %      <<       >>     &       &^
    +      -      |      ^
    ==     !=     <      <=       >      >=
    &&
    ||
    

    格式化

    符号 功能
    %[1]o 使用第一个操作数 fmt.Printf("%d %[1]o %#[1]o ", o)
    %#o 用%o、%x或%X输出时生成0、0x或0X前缀
    用Go语言语法打印值
    fmt.Printf("%d %[1]x %#[1]x %#[1]X ", x)
    %q 打印带单引号字符,无损打印
    %c 打印字符
    % x 在每个十六进制数字前加入一个空格
    %t 打印布尔类型
    %T 打印数据类型
    • 浮点数中,NaN、正无穷大和负无穷大都不是唯一的

    字符串处理

    bytes

    • 针对[]byte类型提供

    strings

    • 字符串查询,替换,比较,截断,拆分,合并

    strconv

    • 布尔型,整型,浮点型和对应字符串的相互转换

    整数转字符串Itoa(Int to ASCII)

    FormatInt(int, base)

    FormatUint(int, base)

    Atoi(s)

    ParseInt(s, base, bit)

    unicode

    • 字符分类,数字,大小写判断,转换

    常量

    • 表达式值计算在编译期,而非运行期

    代码风格

    • 正常执行的语句不要代码缩进
    • 娜扎铁律:注释,日志,测试
    • 使用copy()复制切片和map
    • 返回切片或map,创建新变量进行遍历复制并返回
    • 使用defer管理文件和锁等资源(除非函数执行时间为纳秒级)

    map


    • map并发读写,panic无法recover

    slice扩容


    容量 扩容大小 扩容比例
    2 1 1
    4 2 1
    8 4 1
    16 8 1
    32 16 1
    64 32 1
    128 64 1
    256 128 1
    512 256 1
    1024 512 1
    1280 256 0.25
    1696 416 0.325
    2304 608 0.35849056
    3072 768 0.33333334
    4096 1024 0.33333334
    5120 1024 0.25
    7168 2048 0.4
    9216 2048 0.2857143
    12288 3072 0.33333334
    15360 3072 0.25
    19456 4096 0.26666668
    24576 5120 0.2631579
    30720 6144 0.25
    38912 8192 0.26666668
    49152 10240 0.2631579
    61440 12288 0.25
    76800 15360 0.25
    96256 19456 0.25333333
    120832 24576 0.25531915
    151552 30720 0.2542373
    189440 37888 0.25
    237568 48128 0.25405404
    296960 59392 0.25
    371712 74752 0.25172412
    464896 93184 0.2506887
    581632 116736 0.25110132
    727040 145408 0.25
    909312 182272 0.25070423
    1136640 227328 0.25
    1421312 284672 0.25045046
    1776640 355328 0.25
    2221056 444416 0.2501441
    2777088 556032 0.2503458
    3471360 694272 0.25
    4339712 868352 0.2501475
    5425152 1085440 0.250118
    6781952 1356800 0.25009438
    8477696 1695744 0.25003776
    10597376 2119680 0.2500302

    JSON

    type struct xx{
         Color  bool `json:"color,omitempty"`
    }
    

    iota


    • iota按行递增

    封装

    1. 调用方只需关注少量值
    2. 隐藏实现细节,使开发者在不破坏api的情况下自由开发(bytes.Buffer预分配空间)
    3. 阻止调用者对对象内布置的修改

    CSP

    顺序通信进程(communicating sequential processes)

    channel


    不要通过共享内存进行通信,而应通过通信进行内存共享

    使用

    • go有锁数据结构
    • 有capacity,异步非阻塞
    • 无capacity,同步通信
    • 再次关闭已关闭channel会panic
    • 使用for elem := range ch,在channel关闭时自动退出循环
    • 向已关闭的channel中发送数据会panic
    • 关闭挂在goroutine阻塞的channel会panic
    • close一个channel会唤醒所有等待该channel的其它所有G,并使其进入Grunnable状态,这些Goroutine发现该channel已closed,会panic
    • 可以从已关闭channel中接受数据
    • 使用x, ok := <-ch中ok的值判断channel是否关闭,以及x为数据或零值
    • 关闭nil channel(使用var a chan int创建的channel)会panic
    • nil channel发送数据会永远阻塞

    happens before: 接受者接收数据发生在唤醒发送者goroutine之前

    关闭原则

    • 一个sender,多个receiver,由sender关闭
    • 多个sender,借助额外channel做信号广播
    • 不要关闭已关闭通道,或向已关闭通道发送值

    检查通道关闭

    package main
    
    import "fmt"
    
    type T int
    
    func IsClosed(ch <-chan T) bool {
    	select {
    	case <-ch:
    		return true
    	default:
    	}
    
    	return false
    }
    
    func main() {
    	c := make(chan T)
    	fmt.Println(IsClosed(c)) // false
    	close(c)
    	fmt.Println(IsClosed(c)) // true
    }
    

    发送

    +++++++++++++++
    |     buf     | -----> | x | x | x | x | x |
    |    sendx    |
    |    recvx    |
    |    sendq    | ----->   +++++++++
    |    recvq    |          |   g   | ---> G1
    |    closed   |          | elem  | ---> 6
    |    lock     |          |  ...  |
    +++++++++++++++          +++++++++
    
    • buf满时,用sudog包裹g和要发送的数据,入队sendq,并将当前gorutine进行gopark(m解除当前的g, m重新进入调度循环, g没有进入调度队列)

    • 出现新接收方时,sendq出队,从buf拷贝队头,从sender拷贝到队尾,goready(放入调度队列,等待被调度)

    • 读取空channel时,用sudog包裹g和要发送的数据,入队recvq,gopark

    关闭channel

    1. 加锁
    2. closed=1
    3. ready所有sendq和recvq
    4. 解锁

    读取已关闭channel

    1. sendq和recvq为nil
    2. buf可能不为空
    3. 为空则清零reader读取位置
    4. 不为空则继续读buf

    select


    type hselect struct {
        // 总 case 数
        tcase uint16 // total count of scase[]
        // 当前已初始化填充的 case
        ncase uint16 // currently filled scase[]
        // 轮询顺序
        pollorder *uint16 // case poll order
        // case 的 channel 的加锁顺序
        lockorder *uint16 // channel lock order
        // case 数组,为了节省一个指针的 8 个字节搞成这样的结构
        // 实际上要访问后面的值,还是需要进行指针移动
        // 指针移动使用 runtime 内部的 add 函数
        scase [1]scase // one per case (in order of appearance)
    }
    
    • 如果 select 中实际只操作一个 channel,写成 for range 形式:
    for d := range ch {
        // do some happy things with d
    }
    
    • 如果 select 中需要监听多个 channel,并且这些 channel 可能被关闭,写双返回值的 channel 取值表达式:
    for {
        select {
        case d, ok := <-ch1:
            if !ok {
                break outer
            }
        case d, ok := <-ch2:
            if !ok {
                break outer
            }
        }
    }
    

    select不支持throughfall,随机选择case使用堆实现

    panic


    type _panic struct {
    	argp      unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
    	arg       interface{}    // argument to panic
    	link      *_panic        // 指向上一个panic
    	recovered bool           // recover标志
    	aborted   bool           // the panic was aborted
    }
    
    • map高并发读写时,程序直接崩溃,会panic,但无法用recover捕获
    • panic先执行当前goroutine上已有的derfer任务(仅限当前goroutine),然后检查recovered标记
      • 如果为true,退出panic流程,程序恢复正常;
      • 如果为false,则退出程序
    • panic退出时会打印调用栈,最终调用exit(-2)退出整个进程

    recover


    • 只在defer上下文中生效
    • 不在defer上下文中调用,直接返回nil

    recover流程

    • 检查current gorountine是否在panic流程中,如果不在直接返回nil
    • 执行完recove后,调用recovery
    • 在recovery中将调用recover的defer函数设置到当前goroutine中的sched中,并将ret设置为1

    接口

    interface结构

    // iface描述接口包含方法
    type iface struct {
    	tab *itab // 接口类型
    	data unsafe.Pointer // 具体值
    }
    
    type itab struct {
    	inter *interfacetype // 接口类型
    	_type *_type // 实体类型,内存对齐方式
    	fun [1]uintptr // 数组,保存实际方法地址(第一个方法)
    }
    
    type interfacetype struct {
        typ     _type // 描述 Go 语言中各种数据类型的结构体
        pkgpath name // 接口包名
        mhdr    []imethod // 接口定义函数列表
    }
    

    // eface不包含任何方法的空接口
    type eface struct {
        _type *_type
        data  unsafe.Pointer
    }
    
    • _type
    type _type struct {
        // 类型大小
        size       uintptr
        ptrdata    uintptr
        // 类型的 hash 值
        hash       uint32
        // 类型的 flag,和反射相关
        tflag      tflag
        // 内存对齐相关
        align      uint8
        fieldalign uint8
        // 类型的编号,有bool, slice, struct 等等等等
        kind       uint8
        alg        *typeAlg
        // gc 相关
        gcdata    *byte
        str       nameOff
        ptrToThis typeOff
    }
    
    type arraytype struct {
        typ   _type
        elem  *_type
        slice *_type
        len   uintptr
    }
    
    type chantype struct {
        typ  _type
        elem *_type
        dir  uintptr
    }
    
    type slicetype struct {
        typ  _type
        elem *_type
    }
    
    type structtype struct {
        typ     _type
        pkgPath name
        fields  []structfield
    }
    

    判断myWriter是否实现io.Writer接口

    var _ io.Writer = (*myWriter)(nil)
    var _ io.Writer = myWriter{}
    
    • interface保存一对数据

      变量实际的值
      变量的静态类型

    类型转换

    <结果类型> := <目标类型> ( <表达式> )

    类型断言

    <目标类型的值>,<布尔参数> := <表达式>.( 目标类型 ) // 安全类型断言

    • v.(type)在switch中不支持fallthrough
    • 类型断言针对接口变量进行操作

    如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法

    > 接口转换
    • 当判定一种类型是否满足某个接口时,Go 使用类型的方法集和接口所需要的方法集进行匹配,如果类型的方法集完全包含接口的方法集,则可认为该类型实现了该接口。
    • 某类型有m个方法,某接口有n个方法,则很容易知道这种判定的时间复杂度为 O(mn),Go 会对方法集的函数按照函数名的字典序进行排序,所以实际的时间复杂度为 O(m+n)

    接口值

    var w io.Writer
    w = os.Stdout
    w = new(bytes.Buffer)
    w = nil
    

    定义值初始化

    *os.File赋值给w

    使用w.Write([]byte("hello"))效果和os.Stdout.Write([]byte("hello"))一样

    *bytes.Bufffer赋值给w

    将nil赋值给w

    w将恢复到和他定义时相同的状态

    接口值可以持有非常大的动态值

    var x interface{} = time.Now()
    

    接口动态值的动态类型必须可比较才能进行比较,否则会导致panic(如Slice)

    var x interface{} = []int{1, 2, 3}
    fmt.Println(x == x) // panic: comparing uncomparable type []int
    
    • 值和类型都为nil的接口为nil接口

    sort.Interface

    type Interface interface {
        Len() int
        Less(i, j int) bool // i, j are indices of sequence elements
        Swap(i, j int)
    }
    // 使用组合实现Reverse
    type reverse struct{ Interface } // that is, sort.Interface
    
    func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) }
    
    func Reverse(data Interface) Interface { return reverse{data} }
    

    子类多态/非参数多态

    内存


    内存布局

    Linux内存布局

    • 在包级别声明的变量会在main入口函数执行前完成初始化

    • 内存块:元数据(大小,是否使用,下块内存地址),用户数据,对齐字段

    更快分配内存

    1. 虚拟内存:多线程共享内存
    2. 线程预分配缓存进行一次系统调用,后续线程申请小内存从缓存分配属于用户态执行
    3. 多个线程同时申请小内存时,从各自缓存分配,无需加锁

    TCMalloc(Thread Cache Malloc)

    1. Page:为系统虚拟内存页的倍数,x64为8KB
    2. Span:一组连续的Page,TCMalloc中内存管理的基本单位
    3. ThreadCache:每个线程有各自的Cache,每个Cache有多个空闲内存块链表,每个内存块链表具有相同大小内存块(访问无需加锁)
    4. CentralCache:线程共享缓存(访问需要加锁),ThreadCache无缓存时从CentralCache中取,足够时放回CentralCache
    5. PageHeap:堆内存,若干Span链表,每个链表保存Page数量相同的Span,CentralCache无缓存时从PageHeap中取,将Span拆分成内存块,添加到对应大小的链表。CentralCache足够时则放回PageHeap

    Go内存管理

    内存管理基本对象

    1. Page:x64为8KB

    2. mspan:内存管理基本单位,一组连续Page

    3. mcache:保存各种大小mspan(无锁访问),每个P拥有1个mcache

    4. mcentral:所有线程共享缓存,需加锁访问,每个级别mspan有2个链表(1个包含指针对象,1个不包含指针对象)

    5. mheap:堆内存,将申请到的内存页组织成Span,然后组织成树结构(非链表)

    大小转换

    graph TD A1[size.obj] -->B1(size.class) B1 -->C1(span.class) B1 -->|x| C2(num of page.span.size)
    • Tiny:大小在1Byte~16Byte之间,不包含指针的对象
    • 小对象:16Byte~32KB
    • 大对象:>32KB

    寻找mspan

    1. 计算对象所需内存大小size
    2. 根据size到size class映射,计算所需的size class
    3. 根据size class和对象是否包含指针计算span class
    4. span class指向span

    mcentral

    1. nonempty:链表内尚有空闲object的span,mcache释放span时加入链表
    2. empty:链表内没有空闲object,或已被cache取走的span,span交给mcache时加入链表
    3. mcache向mcentral申请span时,优先取已有资源,即使需要执行清理操作,先从nonempty中查找,没有再从empty中找。只有现有资源无法满足时,才去heap中获取span,并重新切分成object链表。

    mheap的span管理

    • 2棵二叉排序树,按照span的page数量排序
    • free:空闲并且非垃圾回收的span
    • scav:空闲并且垃圾回收的span
    • arenas:heap mrena映射

    mcentral向mheap申请span

    • mcentral向mheap提供内存页数和span class
    • mheap先从free中搜索,如果未找到,再从scav中搜索,如果未找到,向OS申请,然后在搜索free和scav
    • 如果找到的span比需要的span大,将span分割为2个,其中1个满足需求大小,提供给mcentral,另1个加入free中

    mheap申请内存

    • mheap内存不足时,向OS申请内存
    • 将申请到的内存保存到span
    • 将span插入free
    mheap初始化

    1.创建对象规格大小对照表。
    2.计算相关区域大小,并尝试从某个指定位置开始保留地址空间。
    3.在heap里保存区域信息,包括起始位置和大小。
    4.初始化heap其他属性。

    大对象分配

    • 大对象内存分配流程与mcentral向mheap申请内存类似
    • mheap会记录大对象统计信息

    Go垃圾回收和内存释放

    • 垃圾回收收集不再使用的span,并将span释放给OS(标记已不再使用,可以回收。是否回收,由内核决定).
    • 以span为基本单位,通过对比bitmap中的扫描标记,逐步将object收归原span,最终上交central或heap
    • 遍历span,收集不可达的objcet合并到freelist,如果span已回收全部objcet,将该span还给heap

    Go栈内存

    • goroutine初始大小2KB.
    • 动态扩大为原来两倍(runtime.morestack).
    • 栈缩容发生在垃圾回收期间,函数使用空间小于当前空间1/4时,执行缩容,收缩为现在的1/2.
    • 关闭缩容:GODEBUG=gcshrinkstackoff=1.

    内存分配器初始化

    保留虚拟地址空间(不分配内存)被划分成三个区域:

     页所属span指针数组  GC标记位图           用户内存分配区域 
    +-------------------+--------------------+----------------------------------------------+ 
    |spans 512MB        |bitmap 32GB         |arena 512GB                                   | 
    +-------------------+--------------------+----------------------------------------------+ 
     spans_mapped        bitmap_mapped        arena_start    arena_used            arena_end
    
    • areana:向OS申请内存
    • bitmap:为每个对象提供4个标记位,保存指针,GC标记
    • spans:创建时按页填充,回收object时,将其地址按页对齐可找到对应所属span

    newobject内存分配

    1. 获取当前线程绑定的cache
    2. 是否为<16Byte的且无需扫描的tiny对象?
      • 如果是,
        1. 先置偏移量为16,对齐判断是否为更小的对象
        2. 修改偏移量,剩余空间足够?
          • 足够,则调整偏移量,为下次分配做好准备,并返回
          • 不够,
            1. 从cache中获取新的tiny块(从sizeclass=2的span.freelist中获取一个16Byte的object)
            2. 如果没有可用的object,则从central中获取新的span,然后提取tiny块
            3. 调整span.freelist链表,增加使用计数
            4. 初始化(零值)tiny块
            5. 如果旧块剩余空间小于新块大小,则用新块替换旧块
    3. 是否为>16Byte <32KB的小对象?
        1. 查表确定对象的sizeclass
        2. 从对应的span.freelist中提取object
        3. 如果没有可用object,从central中获取新的span,并重新提取object
        4. 调整span.freelist链表,增加使用计数
        5. 初始化(零值)
    4. >32KB的大对象?
      1. 直接从heap中分配span
    5. 在bitmap中做标记
    6. 检查触发条件,启动垃圾回收

    内存扩张

    1. 计算所需页数(size>>_PageShift) _PageShift=1<<13(KB)

    Tips

    • 使用缓存提高效率.
    • 以空间换时间,提高内存管理效率(对象大小转换表).
    • 物理内存分配在写操作导致缺页异常调度时发生,而且是按页提供的.

    GMP

    • G:Goroutine。包括执行的函数指令及参数;G保存的任务对象;线程上下文切换,现场保护和现场恢复需要的寄存器(SP、IP)等信息。
    • M:线程。当指定了线程栈,则M.stack→G.stack,M的PC寄存器指向G提供的函数,然后去执行。
    • P:Processor。当P有任务时需要创建或者唤醒一个系统线程来执行它队列里的任务,包含运行goroutine的资源,M必须和一个P关联才能运行G,如果线程想运行goroutine,必须先获取P,P中还包含了可运行的G队列。
    
                                   +------------------sysmon---------------//--------+ 
                                   |                                                 | 
                                   |                                                 | 
                  +---+        +---+-------+                +--------+           +---+---+ 
    go func() ---> |G| ---> |P|local|    <===balance===>     |global|    <--//---  |P|M| 
                  +---+        +---+-------+                +--------+           +---+---+ 
                                   |                             |                   | 
                                   |     +---+                   |                   | 
                                   +----> |M| <---findrunnable---+---steal<--//------+ 
                                         +---+
                                          |      1. 语句go func() 创建G
                                          |      2. 放入P本地队列(或平衡到全局队列) 
                   +---execute<-----schedule     3. 唤醒或新建M执行任务 
                   |                      |      4. 进入调度循环schedule
                   |                      |      5. 竭力获取待执行G任务并执行 
                   +-->G.fn-->goexit--+          6. 清理现场,重新进入调度循环
    
    

    Go Scheduler

    思想
    • 复用线程

    1. work stealing:当M绑定的P没有可运行的G时,它可以从其他运行的M’那里偷取G。

    2. hand off:当M因为G进行系统调用阻塞时,释放绑定的P,把P转移给其他空闲的M上执行

    • 利用并行

    1. 默认为CPU数量GOMAXPROCS(逻辑),最多有GOMAXPROCS个线程,可能分布在不同的核上运行

    2. 可以控制并发数量,以达到控制资源利用的目的

    策略
    • 抢占

    1. 1个Goroutine最多占用CPU10ms
    2. 1个Goroutine系统调用超过20μs切换
    • 全局G队列
    1. 当work stealing从其他P中偷不到G时,从全局G队列中取G

    G

    type g struct {
      stack       stack   // 描述了真实的栈内存,包括上下界
    
      m              *m     // 当前的m
      sched          gobuf   // goroutine切换时,用于保存g的上下文      
      param          unsafe.Pointer // 用于传递参数,睡眠时其他goroutine可以设置param,唤醒时该goroutine可以获取
      atomicstatus   uint32
      stackLock      uint32 
      goid           int64  // goroutine的ID
      waitsince      int64 // g被阻塞的大体时间
      lockedm        *m     // G被锁定只在这个m上运行
    }
    
    type gobuf struct {
        sp   uintptr
        pc   uintptr
        g    guintptr
        ctxt unsafe.Pointer
        ret  sys.Uintreg
        lr   uintptr
        bp   uintptr // for GOEXPERIMENT=framepointer
    }
    

    M

    type m struct {
        g0      *g     // 带有调度栈的goroutine
    
        gsignal       *g         // 处理信号的goroutine
        tls           [6]uintptr // thread-local storage
        mstartfn      func()
        curg          *g       // 当前运行的goroutine
        caughtsig     guintptr 
        p             puintptr // 关联p和执行的go代码
        nextp         puintptr
        id            int32
        mallocing     int32 // 状态
    
        spinning      bool // m是否out of work
        blocked       bool // m是否被阻塞
        inwb          bool // m是否在执行写屏蔽
    
        printlock     int8
        incgo         bool // m在执行cgo吗
        fastrand      uint32
        ncgocall      uint64      // cgo调用的总数
        ncgo          int32       // 当前cgo调用的数目
        park          note
        alllink       *m // 用于链接allm
        schedlink     muintptr
        mcache        *mcache // 当前m的内存缓存
        lockedg       *g // 锁定g在当前m上执行,而不会切换到其他m
        createstack   [32]uintptr // thread创建的栈
    }
    

    P

    type p struct {
        lock mutex
    
        id          int32
        status      uint32 // 状态,可以为pidle/prunning/...
        link        puintptr
        schedtick   uint32     // 每调度一次加1
        syscalltick uint32     // 每一次系统调用加1
        sysmontick  sysmontick 
        m           muintptr   // 回链到关联的m
        mcache      *mcache
        racectx     uintptr
    
        goidcache    uint64 // goroutine的ID的缓存
        goidcacheend uint64
    
        // 可运行的goroutine的队列
        runqhead uint32
        runqtail uint32
        runq     [256]guintptr
    
        runnext guintptr // 下一个运行的g
    
        sudogcache []*sudog
        sudogbuf   [128]*sudog
    
        palloc persistentAlloc // per-P to avoid mutex
    
        pad [sys.CacheLineSize]byte
    }
    
    type schedt struct {
       goidgen  uint64
        lastpoll uint64
    
        lock mutex
    
        midle        muintptr // idle状态的m
        nmidle       int32    // idle状态的m个数
        nmidlelocked int32    // lockde状态的m个数
        mcount       int32    // 创建的m的总数
        maxmcount    int32    // m允许的最大个数
    
        ngsys uint32 // 系统中goroutine的数目,会自动更新
    
        pidle      puintptr // idle的p
        npidle     uint32
        nmspinning uint32 
    
        // 全局的可运行的g队列
        runqhead guintptr
        runqtail guintptr
        runqsize int32
    
        // dead的G的全局缓存
        gflock       mutex
        gfreeStack   *g
        gfreeNoStack *g
        ngfree       int32
    
        // sudog的缓存中心
        sudoglock  mutex
        sudogcache *sudog
    }
    

    Go并行机制

    初始化

    • osinit()(获取cpu个数和page的大小)
    • schedinit()(获取当前G,设置M数量10000,栈空间初始化,内存初始化,当前M初始化,设置P数量为CPU数量,调用procresize(procs)初始化P)
      • procresize(procs)(初始化P,新建P对象,将P保存到allp数组,如果P没有cache,进行分配)
      • 释放未使用的P(将本地队列中的任务从尾部取出,放入全局队列头部,释放P绑定的cache,将当前P的G复用链接到全局)

    G创建过程

    • newproc函数创建主G
    • 主函数执行runtime.main,创建一个线程负责运行时系统监控

    defer


    • 每次defer语句执行的时候,会把函数“压栈”,函数参数会被拷贝下来.
    • 当外层函数(非代码块,如一个for循环)退出时,defer函数按照定义的逆序执行.
    • 如果defer执行的是函数,则在defer定义时就把值复制给defer,并保存起来
    • 如果defer执行的函数为nil, 那么会在最终调用函数的产生panic.
    • 如果defer执行的是闭包,则会在defer函数真正调用时根据整个上下文确定当前的值

    defer 调用过程(代码)

    	return n
    

    编译后

    返回值=n
    call defer func // 可以操作返回值
    return
    

    defer 调用过程(源码)

    type _defer struct {
        siz     int32 // 函数的参数总大小
        started bool  // defer 是否已开始执行?
        sp      uintptr // 存储调用 defer 函数的函数的 sp 寄存器值
        pc      uintptr  // 存储 call deferproc 的下一条汇编指令的指令地址
        fn      *funcval // 描述函数的变长结构体,包括函数地址及参数
        _panic  *_panic // 正在执行 defer 的 panic 结构体
        link    *_defer // 链表指针
    }
    
    • 先调用runtime.deferproc
    • 再调用runtime.deferreturn
    • 最后调用函数return

    runtime.deferproc

    • 将defer后的函数及相关参数生成结构体挂在当前goroutine的_defer链表头部
    • defer指针指向上一个defer(栈)
    • 检查deferproc的返回值,0表示成功,继续处理defer声明或后续代码
    • 如果返回值不为0,那么会跳到return之前的deferreturn

    runtime.deferreturn

    • 执行defer链表中的函数,直到链表为空

    unsafe.Pointer

    • 任何类型可以转化为unsafe.Pointer
    • unsafe.Pointer可以转化为任何类型
    • uintptr可以转化为unsafe.Pointer
    • unsafe.Pointer可以转化为uintptr

    垃圾回收(GC)


    • v1.1 STW
    • v1.3 Mark STW, Sweep并行
    • v1.5 三色标记
    • v1.8 hybtid writ barrire

    SetFinalizer

    obj被回收时,需要进行一些操作,如写日志等,可通过调用函数runtime.SetFinalizer(obj interface{}, finalizer interfice{})

    obj为指针类型
    finalizer为参数为obj且无返回值的函数
    

    当GC发现obj不可达时,会在独立goroutine中执行finalizer(obj)

    三色标记

    • 起初所有对象为白色
    • 扫描找出所有可达对象,标记为灰色,放入待处理队列
    • 从队列提取灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色
    • 写屏障监视对象内存修改,重新标色或放入队列

    扫描和标记完成后,白色为回收对象黑色为活跃对象

    写屏障

    在执行写操作之前的代码,设置写屏障

    GC循环

    • 扫描终止,标记,标记终止,清理
    +-----------循环 N----------------+
    +     扫描终止、标记/标记终止      +
    +            | |                   +
    +             /                   +
    +    标记终止完成/清理             + ===>      +------循环 N+1------+
    

    plan9汇编


    程序基本分段

    • .DATA: 初始化变量
      • .DATA symbol+offset(SB)/width, value
      • .DATA msg<>+0x00(SB)/8, $"Hello, W"
    • .GLOBL: 声明全局变量
      • .GLOBL msg<>(SB),NOPRT,$16
    • .BSS: 未初始化的全局变量
    • .TEXT: 定义函数
      • .TEXT symbol(SB), [flags,] $framesize[-argsize]
    • .RODATA: 只读数据段

    <>只在当前文件中可以访问

    基本指令

    SUBQ $0x18, SP   // 对SP做减法,为函数分配函数栈帧
    ADDQ $0x18, SP   // 对SP做加法,清除函数栈帧
    
    $num  // 常数
    MOVB $1, DI      // 1 byte
    MOVW $0x10, BX   // 2 bytes
    MOVD $1, DX      // 4 bytes
    MOVQ $-10, AX     // 8 bytes
    
    // 常见计算指令
    ADDQ  AX, BX   // BX += AX
    SUBQ  AX, BX   // BX -= AX
    IMULQ AX, BX   // BX *= AX
    
    // 无条件跳转
    JMP addr    // 跳转到地址,地址可为代码中的地址,不过实际上手写不会出现这种东西
    JMP label   // 跳转到标签,可以跳转到同一函数内的标签位置
    JMP 2(PC)   // 以当前指令为基础,向前/后跳转 x 行
    JMP -2(PC)  // 同上
    
    // 有条件跳转
    JNZ target  // 如果 zero flag 被 set 过,则跳转
    
    • FP: 使用形如 symbol+offset(FP) 的方式,引用函数的输入参数。例如 arg0+0(FP)arg1+8(FP),使用 FP 不加 symbol 时,无法通过编译

    • PC: 实际上就是在体系结构的知识中常见的 pc 寄存器,在 x86 平台下对应 ip 寄存器,amd64 上则是 rip。

    • SB: 全局静态基指针,一般用来声明函数或全局变量。

    • SP: plan9 的这个 SP 寄存器指向当前栈帧的局部变量的开始位置,使用形如 symbol+offset(SP) 的方式,引用函数的局部变量。offset 的合法取值是 [-framesize, 0),注意是个左闭右开的区间。假如局部变量都是 8 字节,那么第一个局部变量就可以用 localvar0-8(SP) 来表示。

    • go语言参数和返回值都在栈上传递

    • c语言前6个参数和返回值在寄存器上传递

    源码

    package main
    
    func main() {
        s := make([]int, 3, 10)
        _ = f(s)
    }
    
    func f(s []int) int {
        return s[1]
    }
    

    汇编代码

    "".f STEXT nosplit size=53 args=0x20 locals=0x8
            // 栈帧大小为8字节,参数和返回值为32字节
            0x0000 00000 (main.go:8)        TEXT "".f(SB), NOSPLIT, $8-32
            // SP栈顶指针下移8字节
            0x0000 00000 (main.go:8)        SUBQ $8, SP
            // 将BP寄存器的值入栈
            0x0004 00004 (main.go:8)        MOVQ BP, (SP)
            // 将新的栈顶地址保存到BP寄存器
            0x0008 00008 (main.go:8)        LEAQ (SP), BP
            0x000c 00012 (main.go:8)        FUNCDATA  $0, gclocals·4032f753396f2012ad1784f398b170f4(SB)
            0x000c 00012 (main.go:8)        FUNCDATA  $1, gclocals·69c1753bd5f81501d95132d08af04464(SB)
            // 取出slice的长度len
            0x000c 00012 (main.go:8)        MOVQ "".s+24(SP), AX
            // 比较索引1是否超过len
            0x0011 00017 (main.go:9)        CMPQ AX, $1
            // 如果超过len,越界了。跳转到46
            0x0015 00021 (main.go:9)        JLS 46
            // 将slice的数据首地址加载到AX寄存器
            0x0017 00023 (main.go:9)        MOVQ "".s+16(SP), AX
            // 将第8byte地址的元素保存到AX寄存器,也就是salaries[1]
            0x001c 00028 (main.go:9)        MOVQ 8(AX), AX
            // 将结果拷贝到返回参数的位置(y)
            0x0020 00032 (main.go:9)        MOVQ AX, "".~r1+40(SP)
            // 恢复BP的值
            0x0025 00037 (main.go:9)        MOVQ (SP), BP
            // SP向上移动8个字节
            0x0029 00041 (main.go:9)        ADDQ $8, SP
            // 返回
            0x002d 00045 (main.go:9)        RET
            0x002e 00046 (main.go:9)        PCDATA $0, $1
            // 越界,panic
            0x002e 00046 (main.go:9)        CALL runtime.panicindex(SB)
            0x0033 00051 (main.go:9)        UNDEF
            0x0000 48 83 ec 08 48 89 2c 24 48 8d 2c 24 48 8b 44 24  H...H.,$H.,$H.D$
            0x0010 18 48 83 f8 01 76 17 48 8b 44 24 10 48 8b 40 08  .H...v.H.D$.H.@.
            0x0020 48 89 44 24 28 48 8b 2c 24 48 83 c4 08 c3 e8 00  H.D$(H.,$H......
            0x0030 00 00 00 0f 0b                                   .....
            rel 47+4 t=8 runtime.panicindex+0
    

    内存重排


    CPU架构

    Total store ordering (TSO)

    最终结果

    false sharing

    • 避免缓存行频繁更新
    • 使用其他数据填充缓存行

    Goroutine调度原理图


    初始化

    1. runtime.init()
    2. main.init() // 所有导入包(包括标准包)初始化
    3. main.main()
    • 所有init都在一个goroutine中执行
    • 所有init结束后才会执行main.main

    命令行


    • 编译
    go bulid -gcflags "-N -l" -o xxx
    

    -N -l:禁用编译器初始化

    • 反编译

    最终机器码汇编

    go tool objdump -s "runtime.init" xxx
    

    过程汇编

    go build -gcflags -S xxx.go
    go tool compile -N -l -S xxx.go
    
    • trace
    package main
    import (	
        "os"
        "runtime/trace"
    )
    
    func main() {
        f, err := os.Create("trace.out")	
        if err != nil {		
           panic(err)
        }	
        defer f.Close()
    
        err = trace.Start(f)
         if err != nil {
     	panic(err)
        }	
        defer trace.Stop()  
        // Your program here
    }
    

    查看

    go tool trace trace.out
    
    • cpu profile
    go test -run=none -bench=ClientServerParallel4 -cpuprofile=cprof net/http
    

    将分析结果写入cprof

    • pprof
    go tool pprof --text http.test cprof
    	--list funcname // 查看
    	--web // web可视化
    
    • go test [,-op]=[regexp]

    go test默认测试所有*_test.go文件
    后跟具体文件名表示测试指定文件
    -bench=regexp 指向相应的benchmarks
    -cover 覆盖率测试
    -run=regexp 只运行regexp匹配的函数
    -v 显示详细测试命令

    编译指示


    指示 作用
    //go:noinline 不要内联。
    //go:nosplit 跳过栈溢出检测。
    //go:noescape 禁止逃逸,而且它必须指示一个只有声明没有主体的函数
    //go:norace 跳过竞态检测

    encoding/gob

    	s := test{
    		data: make(map[string]interface{}, 8),
    	}
    	s.data["count"] = 8
    	buf := new(bytes.Buffer)
    	enc := gob.NewEncoder(buf)
    	enc.Encode(s.data)
    	b := buf.Bytes()
    	fmt.Println(b)
    	dec := gob.NewDecoder(bytes.NewBuffer(b))
    	s2 := test{
    		data: make(map[string]interface{}, 8),
    	}
    	ds := dec.Decode(&s2.data)
    	fmt.Println(ds)
    	for _, v := range s2.data {
    		fmt.Printf("%v	%[1]T", v)
    	}
    
    • 使用gob序列化Go对象,反序列化时可以保持原有Go对象类型(JSON无法保持浮点,整型)

    • 引用类型或包含引用类型在32bit平台为4Byte,64bit平台为8Byte

  • 相关阅读:
    红楼【建筑位置】
    红楼【人物关系】
    jenkins【shared-libraries】
    linux 安装mysql8.0 tar.xz
    MySQL 输入字符串对日期进行模糊查询
    解决kali linux 和 win10 双系统时间不一致问题
    linux idea桌面图标
    linux卸载openjdk11
    tar.xz解压
    pandas模块高性能使用方法总结
  • 原文地址:https://www.cnblogs.com/quanee/p/11747015.html
Copyright © 2020-2023  润新知