《 九 阴 真 经 卷 一 》Golang

编程入门 行业动态 更新时间:2024-10-14 00:24:48

《 九 阴 真 经 卷 一 》<a href=https://www.elefans.com/category/jswz/34/1769831.html style=Golang"/>

《 九 阴 真 经 卷 一 》Golang

《 九 阴 真 经 卷 一 》Golang

天之道,损有余而补不足。人之道,则不然,损不足以奉有余。——《道德经》

文章目录

  • 《 九 阴 真 经 卷 一 》Golang
    • 一. Go语言的原语、关键字、语言特性
      • 1. make、new
      • 2. range
      • 3. defer、recover
      • 4. rune类型
      • 5. select
      • 6. init函数
      • 7. interface与类型断言
      • 8. 闭包
    • 二、Go的三种引用类型
      • 1. Slice
      • 2. Map
      • 3. Channel
    • 三、 Go的内置包
      • 1. Context
      • 2. Sync
      • 3. Sort
      • 4. Reflect
    • 四、内存相关
      • 1. 虚拟内存
      • 2. 内存管理策略
      • 3. 内存对齐
      • 4. 内存逃逸
      • 5. GC
    • 五、goroutine
      • 1. 进程、线程与协程的区别
      • 2. GMP模型
      • 3. goroutine池的实现

一. Go语言的原语、关键字、语言特性

1. make、new

  • make与new的区别
    • 相同点:
      • make和new都是在堆上分配内存
    • 不同点:
      • make仅能用于三种引用类型
      • new会将开辟的内存区域清零,make不是简单的清零,而是初始化底层数据结构
      • make返回的是引用类型对象本身,new返回的是指针

2. range

  • range时局部变量的生存周期
    • range中的局部变量随着range循环,地址不会发生变化

3. defer、recover

  • 底层实现
    • defer与panic的底层实现都是链表,链表头的指针存储在goroutine结构体中
    • recover的实现是修改panic结构体,将其中代表是否恢复的字段设置为true
  • 多个defer的执行顺序
    • 多个defer的执行顺序类似栈,先进后出
  • defer与return的执行顺序
    • return先执行,defer后执行
  • defer与具名返回值
    • 具名返回值会在函数开始时自动初始化,作用域为整个函数
    • defer可以修改具名返回值,佐证了defer的执行时机晚于return
    • 同理,函数返回指针指向的内容也可以被defer修改
  • 在defer之前panic会如何
    • 已经执行到的defer,即使后续出现了panic也会执行defer中的内容,利用这个特性可以在defer中完成一些关闭资源的操作,避免资源泄露
    • 如果代码没有执行到defer那一行,则无法执行该defer
  • 多个recover会如何
    • panic只会被最后一个recover捕获
  • 参考
  • 参考/?spm_id_from=333.999.0.0

4. rune类型

  • Go的默认编码类型是utf-8
  • 介绍一下rune类型
    • rune等同于int32类型,用于处理unicode或utf-8字符
    • byte等同于int8类型,用于处理ascii字符
  • 单引号’’ 双引号"" 反引号``的区别
    • 单引号表示byte类型或rune类型,即一个字符
    • 双引号表示字符串,即字符数组
    • 反引号表示字符串,且不支持转义

5. select

  • select的作用

    • 用多个case语句监视多个channel,类似多路复用
    • 有default分支的select就不会阻塞
  • select的实现

    • select的实现在runtime.selectgo(),用一个数组储存所有case分支,顺序为send在前,recv在后。在轮询这些case时,是以随机打乱的顺序进行的,以保障公平性。所以会有一个长度是case数组两倍的数组,用以保存轮询顺序(保证公平)和加锁顺序(防止死锁)。
    • select的工作原理是轮询所有case,在轮询前会加锁,轮询时检查channel的等待队列和缓冲区,如果有channel可以操作,就进入对应的case分支,如果没有就将当前协程加入到channel的发送和接收的等待队列中,然后释放锁,等待channel变为可操作状态。当有channel可操作时,也会按加锁顺序把所有channel上锁,从等待队列中移除当前的协程,再释放锁。
  • 参考:=29&vd_source=d0c878ce37e7bc95bc121f75c62a5fc3

6. init函数

  • 不同包的init函数执行顺序
    • 从被导入的最深层的包开始执行,直至main包
    • 每个包的init函数只会执行一次
  • 同一个包的多个文件的init函数执行顺序
    • 按文件名顺序执行
  • init函数和包级别变量初始化的先后
    • 包级别变量的初始化先于init函数

7. interface与类型断言

  • 空接口和非空接口各自的实现
    • 空接口的实现
      • 空接口的结构体为eface,主要字段为接口的动态类型,是一个类型元数据的指针,和一个unsafe.Pointer,指向接口的动态值。赋值之前,这两个字段都为nil
    • 非空接口的实现
      • 非空接口的结构体为iface,主要字段为一个接口动态值指针和一个指向itab,itab结构体中会存放接口需要的方法列表、动态类型元数据和从类型元数据中拷贝而来的,接口所需方法实现的地址(这样在使用接口调用方法的时候就不需要查询类型元数据)。特殊的,itab结构体代表接口和实现接口类型的一种映射,是可复用的,所以会将其以接口类型+动态类型的组合为key进行缓存,每次给非空接口赋值时都会先检查itab缓存
  • 类型断言
    • 类型断言根据断言的主题和断言成为的类型可以组合出四种:
      • 空接口.(具体类型):比较空接口动态类型和具体类型的类型元数据
      • 非空接口.(具体类型):检查非空接口的itab是否是缓存中的特定itab
      • 空接口.(非空接口):先检查itab缓存,若没有再检查类型的方法列表
      • 非空接口.(非空接口):先检查itab缓存,若没有再检查类型的方法列表
    • 类型断言失败时,也会把失败记录缓存到itab缓存
  • 参考/?spm_id_from=333.999.0.0

8. 闭包

  • Go语言的函数
    • Go的函数在语法中可以作为对象,可以作为变量的值、函数返回值或函数参数,这样使用时称为function value,是一个指针,不指向代码段中的函数指令,而是指向一个runtime.funcval结构体,这个结构体中存储都代码段中的函数指令地址
  • 闭包
    • 闭包的作用:让函数可以直接使用在函数外部定义的变量
    • 闭包的实现:
      • 闭包相当于一个结构体,存储了一个函数和一个关联的环境,这个环境内部是若干符号和值的对应关系
      • 例如,调用一个使用了在函数外部定义的变量的函数,会在堆上生成一个闭包结构体,保存着函数指针和捕获列表,这个用到的外部变量就在捕获列表中
      • 被闭包捕获的变量,如果从初始化赋值之后就没有被修改过,就直接拷贝值到捕获列表中;如果是被修改过的值变量,这个值变量会改为堆分配,在栈上储存地址,捕获列表中也储存地址,就实现了闭包函数和外层函数使用同一个变量。显然,这种情况也会涉及变量逃逸;如果捕获的是函数参数或返回值,情况也不同,但目标都是为了保证闭包捕获对象和外层对象的一致性

二、Go的三种引用类型

1. Slice

  • Slice的底层数据结构
    • Slice的结构体中存有两个int变量,cap储存切片的最大容量,len储存目前已经存储的元素数量,另外还有一个指针变量,指向底层数组
    • 这种实现方式导致了将一个切片赋值给另一个切片时,二者会指向同一个底层数组
  • Slice的扩容策略
    • 在切片容量小于1024时,每次会扩容到之前的2倍;当切片容量大于1024时,每次会扩容到之前的1.25倍
  • 空切片和nil切片的区别
    • 空切片的指向底层数组的指针变量已经被初始化,nil切片的指针变量是一个nil指针

2. Map

  • Map的底层数据结构
    • Map本身是一个指针,指向底层哈希表
    • Map的哈希表数据结构有下列成员
      • 当前键值对数量
      • B:哈希函数的参数,等于log2桶数,所以该值每增大1,桶数都会翻倍,这个参数也能表征当前Map所能装载的最多键值对数目,等于负载因子 * 2的B次方
      • 指向当前桶的指针
      • 溢出桶的数量
      • 溢出桶信息
    • 桶的数据结构
      • 一个桶里可以存放8个键值对,为了使内存排布紧凑,会将8个key存放在一起,再将8个value存放在一起,而且结构体最前面还会存放8个tophash值,为对应哈希值的高8位,有助于快速检索key
      • 桶中还存放着一个指向溢出桶的指针,溢出桶和桶的数据结构一样,为了避免频繁扩容,在桶存满时,会向连接的溢出桶中存放
      • 当哈希表要分配的桶数目大于2^4时,就认为使用到溢出桶的几率较大,就会预分配2 ^(B-4)个溢出桶,溢出桶与常规桶在内存中连续
  • Map是否有序
    • 无序,因为扩容时Map的键值顺序会被打乱,所以Go在遍历Map时会随机打乱顺序
  • Map的哈希函数
  • 负载因子
    • 负载因子是Map的重要参数,等于目前的键值对数除以桶数,表征目前Map装载的键值对数量是否过多。当负载因子大于6.5时,就会发生扩容
  • Map的扩容策略、渐进式扩容
    • Go Map的扩容行为受到负载因子和目前使用溢出桶数量这两个因素的制约
      • 负载因子大于6.5时,发生翻倍扩容,B++,桶数目翻倍,每个旧桶的键值对都分流到两个新桶中
      • 负载因子不大于6.5,但使用的溢出桶较多时(B<15&&溢出桶数目>=2 ^ B 或 B>=15&&溢出桶数目>2 ^ 15),发生等量扩容,创建和旧桶数目一样多的新桶,将所有键值对重哈希到新桶,这样相当于释放了旧桶中之前删除掉的键值对空间,使目前有效的键值对排列更为紧凑
    • 在扩容时会一次性开辟出新桶的内存,使用两个字段记录旧桶的位置(指针)和迁移进度,每次对Map进行读写操作时,会完成一部分键值对迁移
  • 参考/?spm_id_from=333.999.0.0

3. Channel

  • Channel的底层数据结构
    • 因为channel需要支持协程并发访问,其结构体中有一把锁
    • 有缓冲channel的缓冲区是一个数组,和两个指针,对应读写进度,缓冲区可以看作一个环形,因为读写指针在到达尾部后都会回到头部
    • 一个变量记录关闭状态
    • 最重要的,channel有两个等待队列,分别针对读和写
      • 当缓冲区已满时,写入的元素会进入发送等待队列,写入队列里存储的结构体会记录是哪个协程在等待
      • 读取同理
    • 参考=29&vd_source=d0c878ce37e7bc95bc121f75c62a5fc3
  • Channel的读写特性
    • 空读写阻塞,写关闭异常,读关闭空零
      • 空读写阻塞:向一个nil channel写入数据或试图从中读数据都会造成永久阻塞
      • 写关闭异常:向一个已经关闭的channel中写入数据,会panic
      • 读关闭空零:从一个已经关闭的channel中读取数据,会返回对应类型的零值
    • 参考

三、 Go的内置包

1. Context

  • context包的构成:
    • context接口
    • 四种实现:
      • emptyCtx:实现为一个int变量
      • cancelCtx:用于获取取消通知的context,会储存以当前节点为根节点的所有子context,再根节点取消时,会同步取消所有子节点
      • timerCtx:包装了cancelCtx,可以手动取消,也可以在规定时间到达之后自动触发取消
      • valueCtx:利用context存储键值对,相同的key,子节点的值会覆盖父节点的值
    • 六个方法:
      • Background():返回一个emptyCtx
      • TODO()
      • WithCancel():把一个context包装为cancelCtx,并且提供一个取消函数,调用取消函数即可取消该context
      • WithDeadline():指定一个时间点创建一个timerCtx
      • WIthTimeout():指定一个时间段创建一个timerCtx
      • WithValue():创建valueCtx
  • context的作用
    • 在协程之间
    • context的创建都是基于父context,所以会形成一颗context树
  • context的应用
    • http包、gorm都实现了context,便于http请求的超时自动取消等操作

2. Sync

  • sync.Mutex 互斥锁
    • 底层数据结构的成员是一个int32的state字段和一个uint32的sema字段,state代表锁的状态,加锁和解锁都是通过atomic包提供的原子操作修改state字段。sema为信号量。state的每一位的代表不同的含义,最低位代表加锁和解锁
    • 正常模式与饥饿模式:在state=1的正常状态下,一个尝试加锁的goroutine会先自旋几次,尝试通过原子操作获得锁,若几次操作后仍不能获得锁,会通过信号量排队等待。释放锁后,信号量上等待的goroutine会和尚未进入信号量等待队列的自旋goroutine竞争,往往自旋goroutine更容易获得锁,所以如果等待的goroutine没有拿到锁,就会被重新插入队列头部而非尾部。当一个goroutine等待锁的时间超过1ms之后,会将当前的锁状态置为,即饥饿模式,在此模式下,锁的所有权会从释放锁的goroutine直接交给等待队列头部的goroutine,后来的goroutine会直接排队,而非自旋。如果有一个等待时间小于1ms的goroutine,或者等待队列最后一个goroutine获得了锁,就会把锁从饥饿模式转换回正常模式
    • 正常模式下自旋和等待同时存在,吞吐量更大,性能更好
    • 饥饿模式下只有等待,不会出现尾端延迟
  • sync.RWMutex
  • sync.WaitGroup
  • sync.Map
  • sync.Pool
  • sync.Once
  • sync.Cond

3. Sort

4. Reflect

  • 介绍反射
    • 反射的本质是把类型元数据暴露给用户使用。类型元数据、方法元数据这些信息本来都是未导出的(私有的),所以Reflect包中又保存了一套可导出的(公有的)
  • 参考/?spm_id_from=333.999.0.0

四、内存相关

1. 虚拟内存

  • 介绍虚拟内存
    • OS负责将虚拟内存映射到物理内存。映射的方法是分页,32位机上一页为4KB。OS会以链表记录进程的控制信息,即PCB进程控制块,PCB中会有一个指针指向当前进程页目录的物理地址,页目录也是一个内存页,存储着一系列指针,指向页表,页表中记录物理内存页的地址。32位机的页目录记录1024个页表地址,一个页表中记录1024个页地址,一个内存页为4KB,正好符合32位机最大寻址1024 * 1024 * 4 = 4GB内存,在32位地址中,前10位代表页目录,中间10位代表内存页,最后12位用于存储内存偏移量。
    • 因为每个进程都有自己的页目录,所以可以实现进程之间的内存的地址隔离,也可以通过映射到同一块物理内存实现进程间的内存共享
    • MMU:CPU的内存管理单元,负责将程序的线性地址转换为物理地址
    • TLB:CPU会把已经转换过的地址映射关系缓存起来。切换进程时,TLB缓存就会失效,这也是切换进程代价较高的一个原因

2. 内存管理策略

  • 堆内存
  • 栈内存
    • 栈的内存分配也使用mspan
    • 在调度器初始化时,会初始化两个用于栈分配的全局对象,stackPool面向32KB以下的栈分配,栈大小必须是2的幂,最小为2KB。stackLarge负责大于等于32KB的栈
    • 同堆内存分配一样,每个P也有用于栈的本地缓存,分配内存时先从本地缓存开始,若没有再查找stackpool,若没有再从堆内存分配一个32KB的内存块,划分成对应大小的块后放入stackpool;如果是大于等于32KB的对象,就计算需要的页数(每页8KB),然后求以2为底求页的对数,从stackLarge数组的对数下标位置寻找mspan链表进行分配
    • 栈的增长:每次增长到之前的两倍
    • 栈的收缩:GC时会收缩,最小收缩到2KB
    • 协程运行结束时,如果栈没有增长过,仍未2KB,会被加入有栈空闲协程队列。如果增长过,就释放栈,把协程放入无栈队列
    • 栈释放时,也会分小于32KB和大于等于32KB两种情况。小于32KB的栈,释放时先放回P的本地缓存,如果本地缓存对应类型链表的内存总量大于32KB了,就放入stackPool全局栈缓存中,如果某个mspan的所有内存都释放了,就会将mspan归还堆内存;大于32KB的栈,在GC时会被直接释放还给堆内存,否则会先放回stackLarge

3. 内存对齐

  • 为什么需要内存对齐
    • CPU读取内存的方式通过地址总线,32位机的总线宽度(即机器字长)为32,最大可以寻址4G的内存,同理64位机的总线宽度为64
    • 虽然虚拟内存是连续的,但其实硬件的物理内存的布局是8个bank构成一个chip,并不连续。所以读取一个chip的8个字节只需要一次读取操作,但如果这8个字节横跨了两个chip,就需要两次读取操作。为了减少读取次数,需要内存对齐
  • 介绍内存对齐
    • 每种类型都会有一个对齐边界,内存对齐规定每个对象的地址都需要是其类型的对齐边界的整数倍
    • 32位机的字长、指针长度和寄存器宽度都是4字节,64位机都是8字节;字长就是最大的对齐边界,数据类型的对齐边界是类型长度和机器字长的较小值
    • 另外,在不同机器上Go类型的长度也可能不同,比如32位机的string长度为8字节,64位机为16字节
  • 结构体的内存对齐
    • 结构体的内存对齐边界是所有成员的对齐边界的最大值
    • 其中的每个成员也要按各自的对齐边界进行对齐,这就导致了结构体成员顺序不同可能造成其大小不同
    • 在对齐的存放所有成员后,还需要将结构体长度扩充到其类型对齐边界的倍数。这种设计的初衷是保证结构体数组中每个元素的内存依然是对齐的
  • 参考/?spm_id_from=333.999.0.0

4. 内存逃逸

5. GC

  • 三色标记法
  • 混合写屏障

五、goroutine

1. 进程、线程与协程的区别

  • 内核空间
    • 虚拟地址空间被划分为用户空间和内核空间,操作系统运行在内核空间,用户进程运行在用户空间。内核空间由所有进程的地址空间共享,但不允许用户进程直接访问。进程的信息,如PCB进程控制块就是保存于内核空间
  • 线程
    • 线程是进程的执行体,线程执行时需要从进程的虚拟内存中分配栈空间存储数据,称为线程栈,在内核空间和用户空间各会分配一个段, 成为用户栈和内核栈。线程切换到内核空间时就使用内核栈。WIndows线程的控制信息TCB也存储在PCB中,Linux的线程信息和进程信息共用的是同一类结构体,其中的某些字段相同,实现多线程复用一些资源。同一个进程的线程共享地址空间、打开的文件句柄表。在CPU执行时,寄存器中保存的都是线程信息,所以线程是OS调度和执行的基本单位。一个进程中至少有一个线程,即主线程,由操作系统创建,再由主线程创建其他线程。线程中再调用函数时,又会在线程的栈上再分配函数调用栈。线程进行系统调用时,会进入内核态,此时就可以访问内核空间
  • 进程 & 线程的切换:
    • 线程的切换:线程的CPU时间片用完时,CPU的硬件时钟会触发一次硬件中断,从已经就绪的线程中挑选一个来执行。如果是同一进程的线程,只需要把上一个线程的执行现场保存以来,修改指令指针、栈指针等几个寄存器修改为下个线程的就可以,所以同一个进程的多个线程切换成本较小。
    • 进程的切换:除了线程切换也需要的寄存器内容,进程切换还需要页目录的切换,地址空间发生变化,TLB缓存失效
  • 协程
    • 线程自己再创建几个执行体,给他们分配并保存内存和执行入口等信息,这些执行体就是协程,协程的切换完全不涉及内核态,内核也感知不到协程的存在,所以协程又被称为**”用户态线程“**。协程切换时也需要像线程一样保存或恢复执行现场,但是是由用户态完成的。

2. GMP模型

3. goroutine池的实现

更多推荐

《 九 阴 真 经 卷 一 》Golang

本文发布于:2024-02-10 16:56:43,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1676309.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:Golang

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!