Golang八股

200 阅读6分钟

数据结构

数组和切片的区别

数组是由固定长度的同一类型元素组成的序列,可以通过显示声明长度或给定初值来初始化一个数组。数组长度是数组类型的一个组成部分。

Slice切片是由同一类型元素组成的变长序列,一般写作[]T。

数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。

slice切片的初始化方式:

(1) 通过下标的方式获得数组或者切片的一部分:arr[0:3] or slice[0:3]

(2) 使用字面量初始化新的切片:slice := []int{1, 2, 3}

1,根据切片中的元素数量对底层数组的大小进行推断并创建一个数组;

2,将这些字面量元素存储到初始化的数组中;

3,创建一个同样指向 [3]int 类型的数组指针;

4,将静态存储区的数组 vstat 赋值给 vauto 指针所在的地址;

5,通过 [:] 操作获取一个底层使用 vauto 的切片;

(3) 使用关键字 make 创建切片:slice := make([]int, 10)

和数组不同的是,slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。

image.png

image.png

append原理和扩容机制

每次调用appendInt函数,必须先检测slice底层数组是否有足够的容量来保存新添加的元素。

如果有足够空间的话,直接扩展slice(依然在原有的底层数组之上),将新添加的y元素复制到新扩展的空间,并返回slice。

如果没有足够的增长空间的话,appendInt函数则会先分配一个足够大的slice用于保存新的结果,先将输入的x复制到新的空间,然后添加y元素。

copy函数的第一个参数是要复制的目标slice,第二个参数是源slice,目标和源的位置顺序和dst = src赋值语句是一致的。两个slice可以共享同一个底层数组,甚至有重叠也没有问题。若原slice容量小于1024就会将容量扩大为原来的两倍,否则扩大为原来的1.25倍,这样可以节约资源。

当且仅当待append的元素数量 + 当前长度 > cap时,slice需要扩容和数据拷贝,这种时候cap和pointer就会被修改。

go的map(数据结构)

golang中map的底层数据结构是一个叫hmap的结构体struct。

type hmap struct {

   // map中存入元素的个数, golang中调用len(map)的时候直接返回该字段
   count     int
   // 状态标记位,通过与定义的枚举值进行&操作可以判断当前是否处于这种状态
   flags     uint
   B         uint8  // 2^B 表示bucket的数量, B 表示取hash后多少位来做bucket的分组
   noverflow uint16 // overflow bucket 的数量的近似数
   hash0     uint32 // hash seed (hash 种子) 一般是一个素数

   buckets    unsafe.Pointer // 共有2^B个 bucket ,但是如果没有元素存入,这个字段可能为nil
   oldbuckets unsafe.Pointer // 在扩容期间,将旧的bucket数组放在这里, 新buckets会是这个的两倍大
   nevacuate  uintptr        // 表示已经完成扩容迁移的bucket的指针, 地址小于当前指针的bucket已经迁移完成

   extra *mapextra // optional fields
}

hmap里的buckets指向一个bmap桶的数组,数组长度2的B次方,而一个桶里最多放入8个key-value键值对。

image.png

image.png

map的有序性

image.png

go的map(插入和查询)

go的map的插入和查询都首先需要做的一个工作就是,查询当前key/value应该存储/读取的位置。然后再赋值或取值。

image.png

image.png

image.png

image.png

go的map(扩容机制)

扩容触发条件

image.png

扩容的过程

这边文章写的较清晰:juejin.cn/post/710200…

go的map(协程安全)

首先golang中的map并不是并发安全的数据结构,golang提供了额外的sync.map并发安全的map。

image.png

image.png

什么样的场景会有并发安全问题呢?

关于如何实现并发安全的map,这篇文章挺好的 zhuanlan.zhihu.com/p/356739568 而且也有对sync.map的介绍,还挺清晰的

go的map(sync.map原理)

这篇简单清楚点 juejin.cn/post/684490…

image.png

Channel概念

channel不仅是go语言中重要的一个数据结构(像map一样),同样是goroutine之间的通信方式。Go语言鼓励的并发模型:通过通信的方式共享内存而不是通过共享内存的方式通信,简单说就是goroutine之间通过channel传递数据。

channel遵循先进先出原则,先从 Channel 读取数据的 Goroutine 会先接收到数据;先向 Channel 发送数据的 Goroutine 会得到先发送数据的权利。

channel的创建都会用到make关键字,ch := make(chan int, 10)。

发送数据:ch <- i主要调用runtime.chansend 函数,该函数的执行过程分成三种情况:发送直接接收、发送至缓冲区、发送阻塞。接收数据:i <- ch 或 i, ok <- ch,主要调用runtime.chanrecv函数,也是相应的三种情况。

type hchan struct {
	qcount   uint                    // Channel 中的元素个数;
	dataqsiz uint                   // Channel 中的循环队列的长度;
	buf      unsafe.Pointer    // Channel 的缓冲区数据指针;
	elemsize uint16        // Channel 能够收发的元素大小
	closed   uint32          // 该Channel是否关闭
	elemtype *_type     // Channel 能够收发的元素类型
	sendx    uint           // Channel 的发送操作处理到的位置
	recvx    uint           // Channel 的接收操作处理到的位置
	recvq    waitq         //  存储了当前 Channel 由于缓冲区空间不足而阻塞的接受Goroutine 列表
	sendq    waitq      // 存储了当前 Channel 由于缓冲区空间不足而阻塞的发送 Goroutine 列表

	lock mutex          // 用于保护成员变量的互斥锁
}

各类channel

无缓存channel

image.png

单方向channel

image.png

带缓存的channel

image.png

并发

并发控制之Context

image.png

image.png

理解 context.Context 的使用方法和设计原理 — 多个 Goroutine 同时订阅 ctx.Done() 管道中的消息,一旦接收到取消信号就立刻停止当前正在执行的工作。

默认上下文

image.png

取消信号、传值方法等点。

并发控制之原语和锁

image.png

image.png

image.png

image.png

image.png

image.png

协程(基础概念)

进程是程序实体一次运行的过程,是操作系统资源分配的基础单元。

线程是轻量级进程,是操作系统任务执行的基本单元。同一进程下可以有多个线程,线程共享进行的内存空间,同时又有线程私有的少量内存空间。线程相对于进程切换开销更小、同一进程下线程通信更容易。

协程是用户态的线程。线程虽然是轻量级的,但在线程较多的情况下仍然有较大的切换开销和内存占用。而golang的协程在用户态切换,开销比线程更小,而且协程占用的内存也更少,线程占用内存至少在1MB,而协程占用的内存可以在几KB。

GMP模型

关于模型本身的内容可以看Xmind思维导图,总结的挺好的

image.png

G、M的数量,G、M各有集中状态,这些都要指导

调度器P的调度策略和调度流程

调度策略或者叫调度机制,也可以看思维导图,总结的真挺好

image.png

系统调用M会阻塞,而用户态阻塞是G阻塞而不是M。

特殊的M0和G0,与调度的生命周期相关。顺哥还是有东西的呀。

协程(进程间通信)

管道、共享内存、信号量P/V、信号等。

协程(网络服务)

这篇真的很厉害,主要是对于goroutine在网络编程、服务器这块与异步网络编程select()的对比,真的会填补知识空白 www.cnblogs.com/liang1101/p…

函数

闭包

golang的函数闭包指,在函数内部,引用函数局部变量的内部函数,这里的内部函数可以是内部实名函数、内部匿名函数、或一段lambda表达式。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。

这个文章挺扫盲的 studygolang.com/articles/35…

image.png

defer就是golang函数闭包的一个应用!!

在函数里用go func启一个协程也是在用闭包啊!!c.biancheng.net/view/98.htm…

逃逸机制

这个博客挺通俗易懂的 geektutu.com/post/hpg-es…

前置扫盲

image.png

逃逸分析是什么

image.png

逃逸分析来看传值和传指针

image.png

defer

这篇感觉挺好的,基础应该有了 jincheng9.github.io/post/go-def…

  • defer的函数或方法什么时候执行?
  • defer的函数或方法的参数的值是什么时候确定的?
  • defer的函数或方法如果存在多级调用是什么机制?

panic和recover原理

panic 与 recover 是 Go 的两个内置函数,这两个内置函数用于处理 Go 运行时的错误,panic 用于主动抛出错误,recover 用来捕获 panic 抛出的错误。

image.png