GoLang
Go基础
分享一些我的Golang笔记,已助我拿到多个18k+的Offer,在面向Golang岗位时,Golang考察占比40%,其中问内置的数据结构如Map,channel,slice占比又是最大的。
当然语言不是根本,还要配合其他计算机八股文和一些基础的算法知识。
与java的区别
- go不允许重载
- go速度>java
- go没有多态
- go通过匿名组合实现继承,java使用extends关键字,且go可以多继承,java不行
- go有协程
- GC使用三色标记法
print格式化输出函数
Printf(),Sprintf(),FprintF() 虽然这三个函数,都是格式化输出,但是输出的目标不一样
Printf 是标准输出,一般是屏幕,也可以重定向。
Sprintf()是把格式化字符串输出到指定的字符串中。
Fprintf()是把格式化字符串输出到文件中。
defer
作用:
- defer延迟函数,
- 释放资源,收尾工作(如释放锁,关闭文件,关闭链接)
- 捕获panic;
多个 defer 调用顺序是 LIFO(后入先出),defer后的操作可以理解为压入栈中
defer,return,return value(函数返回值) 执行顺序:
- 首先return,
- 其次return value,
- 最后defer,且可以修改函数最终返回值,修改时机:有名返回值或者函数返回指针
type
type
可以定义结构体、接口、新类型,定义高阶函数类型
1 |
|
1 |
|
引用类型与值类型
引用类型
变量存储的是一个地址,这个地址存储最终的值。内存通常在堆上分配。通过 GC 回收。包括 指针、slice 切片、管道 channel、接口 interface、map、函数等。引用类型
的变量,我们不光要声明它,还要为它分配内容空间
值类型
是 基本数据类型,int,float,bool,string, 以及数组和 struct 特点:变量直接存储值,内存通常在栈中分配,栈在函数调用后会被释放,值类型
的则不需要显示分配内存空间,是因为go会默认帮我们分配好
make & new
变量初始化,一般包括2步:
- 声明,var
- 内存分配,new或者make
var声明值类型的变量时,系统会默认为他分配内存空间,并赋该类型的零值比如布尔、数字、字符串、结构体
如果指针类型或者引用类型的变量,系统不会为它分配内存,默认就是**nil
**。此时如果你想直接使用,那么系统会抛异常,必须进行内存分配后,才能使用。
关于make,第一个参数是类型,第二个参数是分配的空间,第三个参数是预留分配空间
1 |
|
new 和 make 两个内置函数,主要有以下2点区别:
-
使用场景区别:
make 只能用来分配及初始化类型为slice、map、chan 的数据。
new 可以分配任意类型的数据,并且置零。
-
返回值区别:
make返回的是slice、map、chan类型本身,这3种类型是引用类型,就没有必要返回他们的指针
new返回一个指向该类型内存地址的指针
rune 类型
相当int32
golang中的字符串底层实现是通过byte数组的,中文字符在unicode下占2个字节,在utf-8编码下占3个字节,而golang默认编码正好是utf-8
byte 等同于int8,常用来处理ascii字符
rune 等同于int32,常用来处理unicode或utf-8字符
单引号,双引号,反引号的区别
单引号,表示byte类型或rune类型,对应 uint8和int32类型,默认是 rune 类型。byte用来强调数据是raw data,而不是数字;而rune用来表示Unicode的code point。
双引号,才是字符串,实际上是字符数组。可以用索引号访问某字节,也可以用len()函数来获取字符串所占的字节长度。
反引号,表示字符串字面量,但不支持任何转义序列。字面量 raw literal string 的意思是,你定义时写的啥样,它就啥样,你有换行,它就换行。你写转义字符,它也就展示转义字符。
select
golang 的 select 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作每个case语句里必须是一个IO操作,确切的说,应该是一个面向channel的IO操作。
- select 机制用来处理异步 IO 问题
- select 机制最大的一条限制就是每个 case 语句里必须是一个 IO 操作
- golang 在语言级别支持 select 关键字
Slice
Slice与Array的区别
array是固定长度的数组,是值类型的,如果进行赋值或者作为函数参数,实际上整个数据都会被重新拷贝一份,使用前必须声明长度。
slice切片是基于数组实现的,可以理解是为对底层数组的抽象,属于引用类型,作为函数参数时,slice传递的是指针。复制使用copy进行深拷贝,:=
是浅拷贝,slice不是线程安全的。
slice的数据结构定义如下:
1 |
|
扩容
Go 中切片扩容的策略:
- 首先判断,如果新申请容量大于 2 倍的旧容量,最终容量就是新申请的容量
- 否则判断,如果旧切片的长度<1024,则最终容量就是旧容量的两倍
- 否则判断,如果旧切片长度≥1024,则最终容量从旧容量开始循环 增加原来的 1/4, 直到最终容量大于等于新申请的容量
- 如果最终容量计算值溢出,则最终容量就是新申请容量
注意:如果 slice 在 append() 过程中没有发生扩容,那么修改就在原来的内存中,如果发生了扩容,就修改在新的内存中。
删除元素
指定index
,调用append
链接前后切片append(str[:index], str[index + 1]…)
内存分配
有可能分配到栈上,也有可能分配到栈上。当开辟切片空间较大时,会逃逸到堆上。
通过命令**go build -gcflags "-m -l" xxx.go
**观察golang是如何进行逃逸分析的
1 |
|
Map
map默认是并发不安全的,同时对map进行并发读写时,程序会panic,因为Go 官方认为 Go map 更应**适配典型使用场景,**而不是为了小部分情况(并发访问),导致性能损失。
map 因扩张⽽重新哈希时,各键值项存储位置都可能会发生改变,所以官方避免大家依赖顺序,直接打乱处理,都会从一个随机值序号的bucket,再从其中随机的cell开始遍历,所以是无序的。
可以作为Map Key的只能是可比较的,则不能作为map key 的类型包括:
- slices
- maps
- functions
Map的实现
Map 底层是由hmap
和bmap
两个结构体实现的。
1 |
|
bmap
就是我们常说的“桶”,一个桶里面会最多装 8 个 key,低B位是相同的会落入同一个桶,高 8 位来决定 key 到底落入桶内的哪个位置
bmap
中还存储一些状态值,且都是小于minTopHash的
为了避免tophash
和这些状态值相等,产生混淆,所以若tophash
<minTopHash时候,自动将其值加上minTopHash作为该key的tophash
1 |
|
读取
Go 语言中读取 map 有两种语法:带 comma 和 不带 comma。
1 |
|
根据 key 的不同类型/返回参数,编译器会将查找函数用更具体的函数替换
-
写保护监测
map 的标志位 flags。如果 flags 的写标志位此时被置 1 了,说明有其他协程在执行“写”操作,进而导致程序 panic,这也说明了 map 不是线程安全的
-
计算hash
看高8位和低B位
10010111 | ... │ 01010
-
找到hash对应的bucket
哈希值的低B个bit 位,用来定位key所存放的bucket,如果当前正在扩容中,并且定位到的旧bucket数据还未完成迁移,则使用旧的bucket(扩容前的bucket)
-
遍历bucket查找
用步骤2中的hash值,得到高8个bit位,用来快速判断key是否已在当前bucket中,也就是
10010111
,转化为十进制,也就是151。在 bucket 及bucket的overflow中寻找tophash 值(HOB hash)为 151* 的 槽位,即为key所在位置
5. 返回key对应的指针
流程如下图:
Map遍历
通过对 **mapiterinit
**方法阅读,可得知其主要用途是在 map 进行遍历迭代时进行初始化动作。共有三个形参,用于读取当前哈希表的类型信息、当前哈希表的存储信息和当前遍历迭代的数据。
更具体的话就是根据随机数,选择一个桶位置作为起始点进行遍历迭代
哈希冲突
比较常用的Hash冲突解决方案有链地址法和开放寻址法:
-
链地址法
当哈希冲突发生时,创建新单元,并将新单元添加到冲突单元所在链表的尾部。 -
开放寻址法
当哈希冲突发生时,从发生冲突的那个单元起,按照一定的次序,从哈希表中寻找一个空闲的单元,然后把发生冲突的元素存入到该单元。开放寻址法有多种方式:线性探测法(冲突则尝试加1)、平方探测法和双重哈希法。
扩容
负载因子(load factor),用于衡量当前哈希表中空间占用率的核心指标
负载因子 = 哈希表存储的元素个数/桶个数
(默认6.5)
Map的扩容条件:
- map元素个数 > 6.5 * 桶个数,双倍扩容
双倍扩容:新建一个buckets数组,新的buckets大小是原来的2倍,然后旧buckets数据搬迁到新的buckets - 当桶总数 <2^15时,如果溢出桶总数 >= 桶总数,则认为溢出桶过多。
当桶总数 ≥2^15时,且溢出桶总数 >= 2 ^ 15 时,即认为溢出桶太多了。
此时是等量扩容
等量扩容:并不扩大容量,buckets数量维持不变,重新做一遍类似双倍扩容的搬迁动作,把松散的键值对重新排列一次,使得同一个 bucket 中的 key 排列地更紧密,节省空间,提高 bucket 利用率,进而保证更快的存取。
对于条件2,其实算是对条件1的补充。因为在负载因子比较小的情况下,有可能 map 的查找和插入效率也很低,而第 1 点识别不出来这种情况,如overflow的bucket数量较多,因此有了第 2 扩容条件。
搬迁 buckets 的动作在 growWork()
函数中,而调用 growWork()
函数的动作是在 mapassign 和 mapdelete 函数中。也就是插入或修改、删除 key 的时候,都会尝试进行搬迁 buckets 的工作。每次最多移动2个桶,每次调用都会查看是否迁移完毕,如果没有迁移完毕就尝试继续搬迁。
线程安全的Map
三种方式实现:
-
加读写锁
加锁方式简单
-
sync.Map
-
分片加锁
性能更好,可以降低锁的粒度,提高map对象的吞吐
sync.Map
采用读写分离和用空间换时间的策略保证 Map 的读写安全,使用 read 和 dirty 两个 map 来进行读写分离,降低锁时间来提高效率。
- 无需初始化,直接声明即可使用
- 读写需要使用sync.Map提供的方法,Store(key, value)用于存储,Load(key)用于取值,Delete(key)表示删除。
优点:
适合读多写少的场景
缺点:
但是大量并发读写的情况下,锁的竞争会很激烈,导致性能降低。
如何解决这个问题:尽量减少锁的粒度和锁的持有时间,减少锁的粒度,常用方法就是分片 Shard,将一把锁分成几把锁,每个锁控制一个分片。
内存管理
GC算法
Go 的 GC 回收有三次演进过程:
- Go V1.3 之前普通标记清除(mark and sweep)方法,整体过程需要启动 STW,效率极低
- GoV1.5 三色标记法,堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要 STW),效率普通。
- GoV1.8 三色标记法,混合写屏障机制,栈空间不启动(全部标记成黑色),堆空间启用写屏障,整个过程不要 STW,效率高。
三色标记法
- 创建:白、灰、黑 三个集合
- 将所有对象放入白色集合中
- 遍历所有root对象,把遍历到的对象从白色集合放入灰色集合 (这里放入灰色集合的都是根节点的对象)
- 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,自身标记为黑色
- 重复步骤4,直到灰色中无任何对象,其中用到2个机制:
- 收集所有白色对象(垃圾)
混合写屏障
当gc进行中时,新创建一个对象. 按照三色标记法的步骤,对象会被标记为白色,这样新生成的对象最后会被清除掉,这样会影响程序逻辑。golang引入写屏障机制.可以监控对象的内存修改,并对对象进行重新标记。gc一旦开始,无论是创建对象还是对象的引用改变,都会先变为灰色。
- GC开始将栈上的对象全部扫描并标记为黑色。
- GC期间,任何在栈上创建的新对象,均为黑色。
- 被删除的对象标记为灰色。
- 被添加的对象标记为灰色。
GC流程
次完整的垃圾回收会分为四个阶段,分别是标记准备、标记开始、标记终止、清理:
- 标记准备(Mark Setup):打开写屏障(Write Barrier),需 STW(stop the world)
- 标记开始(Marking):使用三色标记法并发标记 ,与用户程序并发执行
- 标记终止(Mark Termination):对触发写屏障的对象进行重新扫描标记,关闭写屏障(Write Barrier),需 STW(stop the world)
- 清理(Sweeping):将需要回收的内存归还到堆中,将过多的内存归还给操作系统,与用户程序并发执行
其他语言GC算法
- 引用计数:每个对象维护一个引用计数,当引用该对象的对象被销毁时,引用计数减1,当引用计数器为0时回收该对象。
优点:对象可以很快的被回收,不会出现内存耗尽或达到某个阀值时才回收。
缺点:不能很好的处理循环引用,而且实时维护引用计数,有也一定的代价。
代表语言:Python、PHP - 标记-清除:从根变量开始遍历所有引用的对象,引用的对象标记为"被引用",没有被标记的进行回收。
优点:解决了引用计数的缺点。
缺点:需要STW,即要暂时停掉程序运行。
代表语言:Golang - 分代收集:按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,而短的放入新生代,不同代有不能的回收算法和回收频率。
优点:回收性能好
缺点:算法复杂
代表语言: JAVA
GC什么时候触发
- 主动触发,手动调用
runtime.GC
来触发GC,此调用阻塞式地等待当前GC运行完毕。 - 被动触发,分为两种方式:
- 使用步调(Pacing)算法,其核心思想是控制内存增长的比例,每次内存分配时检查当前内存分配量是否已达到阈值(环境变量GOGC,默认100%),即当内存扩大一倍时启用GC。
- gcTriggerTime:当超过两分钟没有产生任何GC时,强制触发 GC。
内存逃逸
逃逸:本该分配到栈上的变量,跑到了堆上,这就导致了内存逃逸。
栈是高地址到低地址,栈上的变量,函数结束后变量会跟着回收掉,不会有额外性能的开销。
变量从栈逃逸到堆上,如果要回收掉,需要进行 gc,那么 gc 一定会带来额外的性能开销。编程语言不断优化 gc 算法,主要目的都是为了减少 gc 带来的额外性能开销,变量一旦逃逸会导致性能开销变大。逃逸分析在编译阶段完成的
编译器会根据变量是否被外部引用来决定是否逃逸:
- 如果函数外部没有引用,则优先放到栈中;
- 如果函数外部存在引用,则必定放到堆中;
- 如果栈上放不下,则必定放到堆上;
通过编译参数**-gcflag=-m
**可以查看编译过程中的逃逸分析
内存逃逸的情况如下:
- 指针逃逸:方法内返回局部变量指针。
- 变量大小不确定:如编译期间无法确定slice的长度
- 栈空间不足:切片(扩容后)长度太大。
- 动态类型:如果函数参数为 interface{},也会发生逃逸
- 在 slice 或 map 中存储指针
- 向 channel 发送指针数据。
- 在闭包中引用包外的值。
调度模型
GMP模型
-
G(Goroutine)
:其中保存着栈,寄存器,以及指令等信息。 -
M(Machine)
:代表一个操作系统的主线程,是对内核级线程的封装,数量对应真实的 CPU 数,用于执行 G。M 会优先从关联的 P 的本地队列中直接获取待执行的 G。M 保存了 M 自身使用的栈信息、当前正在 M上执行的 G 信息、与之绑定的 P 信息。 -
P(Processor)
:可以看作为一个局部调度器,当 P 有任务时,就需要创建或者唤醒一个系统线程来执行它队列里的任务,所以 P 和 M 是相互绑定的。P的数量默认是CPU核数,也可以通过GOMAXPROCS来指定数量。
每个P都会维护一个本地队列,用于保存G,P会从队列头获取G交给M执行,执行完后放入到队列尾(如果需要继续执行)
调度流程
- 每个 P 有个局部队列,局部队列保存待执行的 goroutine,局部队列已经满了之后放到全局队列
- 每个 P 和一个 M 绑定,M 是真正的执行 P 中 goroutine 的实体(流程 3),M 从绑定的 P 中的局部队列获取 G 来执行
- 当局部队列为空时,M会从全局队列获取到本地队列来执行 G,当从全局队列中没有获取到可执行的 G 时候,M 会从其他 P的局部队列中偷取 G 来执行,也就是称为work stealing
- 当 G 因系统调用(syscall)阻塞时会阻塞 M,此时 P 会和 M 解绑即 hand off,并寻找空闲的M
- 当 G 因 channel 或者 network I/O 阻塞时,不会阻塞 M,M 会寻找其他runnable 的 G;当阻塞的 G 恢复后会重新进入 runnable 进入 P 队列等待执行
GMP 调度过程可能存在的阻塞
- I/O,select
- block on syscall
- channel
- 等待锁
- runtime.Gosched()
并发编程
1.互斥锁;2.读写锁;3.原子操作;4.sync.once;5. sync.atomic;6.channel
channel
channel是Go语言中的一个数据类型,可以把它看成管道,用来解决数据通信的问题。
默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得goroutine同步变的更加的简单,而不需要显式的lock。
根据通道的容量,可以将通道分为无缓冲通道和缓冲通道
根据通道传输方向,还可以通道分为双向通道,只读通道和只写通道
常见问题
channel数据结构
1 |
|
channel内部数据结构主要包含:
- buf指向的一个底层的循环数组,只有设置为有缓存的channel才会有buf
- sendx和recvx分别指向底层循环数组的发送和接收元素位置的索引
- sendq和recvq分别表示发送数据的被阻塞的goroutine和读取数据的goroutine,这两个都是一个双向链表结构
hchan结构体中采用Mutex锁来保证数据读写安全。在对循环数组buf中的数据进行入队和出队操作时,必须先获取互斥锁,才能操作channel数据。
channel应用场景
channel适用于数据在多个协程中流动的场景,有很多实际应用:
- 任务定时
- 解耦生产者和消费者
- 控制并发数
- 协程间数据传递
同一个协程里,不能对无缓冲channel同时发送和接收数据,如果这么做会直接报错死锁。
对于一个无缓冲的channel而言,只有不同的协程之间一方发送数据一方接受数据才不会阻塞。channel无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据。
锁
悲观锁
悲观锁:当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制
乐观锁
乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量
同步原语
sync.Mutex
(互斥锁) 可以限制对临界资源的访问,保证只有一个 goroutine 访问共享资源sync.RWMutex
(读写锁) 可以限制对临界资源的访问,保证只有一个 goroutine 写共享资源,可以有多个goroutine 读共享资源。
使用场景:大量并发读,少量并发写,有强烈的性能要求sync.WaitGroup
可以等待一组 Goroutine 的返回。
使用场景:并发等待,任务编排,一个比较常见的使用场景是批量发出 RPC 或者 HTTP 请求sync.Cond
可以让一组的 Goroutine 都在满足特定条件时被唤醒。
使用场景:利用等待 / 通知机制实现阻塞或者唤醒sync.Once
可以保证在 Go 程序运行期间的某段代码只会执行一次
使用场景:常常用于单例对象的初始化场景sync.Pool
可以将暂时将不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配,复用对象的内存,减轻 GC 的压力,提升系统的性能(频繁地分配、回收内存会给 GC 带来一定的负担,严重的时候会引起 CPU 的毛刺)
使用场景:对象池化, TCP连接池、数据库连接池、Worker Poolsync.Context
可以进行上下文信息传递、提供超时和取消机制、控制子 goroutine 的执行
并发控制
golang控制并发有三种经典的方式:
-
channel
使用无缓冲的通道作为同步通道
-
WaitGroup
通过sync包中的WaitGroup实现并发控制,在主 goroutine 中
Add(delta int)
索要等待goroutine 的**数量,**在每一个 goroutine 中调用Done()
通知完成1
2
3
4
5wg.Add(1)
go func(url string) {
defer wg.Done()
http.Get(url)
}(url) -
Context
Context 包主要是用来处理多个 goroutine 之间共享数据,及多个 goroutine 的管理
例如提供了超时 Timeout 和 Cancel 机制。总的来说,在下面这些场景中,可以考虑使用 Context:
- 上下文信息传递
- 控制子 goroutine 的运行
- 超时控制的方法调用
- 可以取消的方法调用
Goroutine泄露
泄露情况分类
- channel 导致的泄露
发送不接收,接收不发送 - 传统同步机制导致的泄露(主要指面向共享内存的同步机制,比如排它锁、共享锁)
WaitGroup ,如果设置了错误的任务数,也可能会导致阻塞,导致泄露发生。
channel和锁的对比
- 并发问题可以用channel解决也可以用Mutex解决,但是它们的擅长解决的问题有一些不同。
channel关注的是并发问题的数据流动,适用于数据在多个协程中流动的场景。
而mutex关注的是是数据不动,某段时间只给一个协程访问数据的权限,适用于数据位置固定的场景。 - channel的性能比锁代价要大很多
channel和共享内存有什么优劣势
Go的设计思想就是, 不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。
共享内存是在操作内存的同时,通过互斥锁、CAS等保证并发安全,
而channel虽然底层维护了一个互斥锁,来保证线程安全,但其可以理解为先进先出的队列,通过管道进行通信。
共享内存优势是资源利用率高、系统吞吐量大,劣势是平均周转时间长、无交互能力。
channel优势是降低了并发中的耦合,劣势是会出现死锁。
面向对象
结构比较
如果struct中含有不能被比较的字段类型,就不能被比较
如果struct中所有的字段类型都支持比较,那么就可以被比较。对应字段相等则认为两个式相等的
不可被比较的类型
- slice,因为slice是引用类型,除非是和nil比较
- map,和slice同理,如果要比较两个map只能通过循环遍历实现
- 函数类型
其他的类型都可以比较。
还有两点值得注意:
- 结构体之间只能比较它们是否相等,而不能比较它们的大小。
- 只有所有属性都相等而属性顺序都一致的结构体才能进行比较。
方法与函数
函数是指不属于任何结构体、类型的方法,也就是说函数是没有接收者的;而方法是有接收者的。
- 如果方法的接收者是指针类型,无论调用者是对象还是对象指针,修改的都是对象本身,会影响调用者;
- 如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;
接口
在Go语言中接口(interface)是一种类型,一种抽象的类型。
Go 引入了动态语言的便利,同时又会进行静态语言的类型检查,Go 采用了折中的做法:不要求类型显示地声明实现了某个接口,只要实现了相关的方法即可,编译器就能检测到。
值接收者和指针接收者实现接口的区别
接口接收者是指针类型,则只能接受指针类型的赋值
接口是值类型,则都可以
参考:
https://www.liwenzhou.com/posts/Go/12_interface/#autoid-1-6-2
接口断言
接口断言分为安全断言和非安全断言,使用非安全断言可能造成panic
1 |
|
或者使用switch
1 |
|
空接口应用
- 空接口类型的变量可以存储任意类型的变量。
- 使用空接口实现可以接收任意类型的函数参数。
- 空接口作为map的值
1 |
|
Go中的多态性是在接口的帮助下实现的
类型接口的变量可以保存实现接口的任何值。接口的这个属性用于实现Go中的多态性。
继承
Go语言的继承通过匿名组合完成:基类以Struct的方式定义,子类只需要把基类作为成员放在子类的定义中,支持多继承。
Java的继承通过extends关键字完成,不支持多继承。
反射
recflect是golang用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。它提供了两种类型(或者说两个方法)让我们可以很容易的访问接口变量内容,分别是reflect.ValueOf() 和 reflect.TypeOf()。
- ValueOf用来获取输入参数接口中的数据的值,如果接口为空则返回0
- TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil
错误
panic
假如函数F中调用了panic语句,会终止其后要执行的代码,并会执行defer
几个容易出现panic的点:
- 函数返回值或参数为指针类型,nil, 未初始化结构体,此时调用容易出现panic,可加 != nil 进行判断
- 数组切片越界
- 如果我们关闭未初始化的通道,重复关闭通道,向已经关闭的通道中发送数据,这三种情况也会引发 panic,导致程序崩溃
- 如果我们直接操作未初始化的map,并发读写,也会引发 panic
- 除数为0
- 调用 panic 函数
recover
在一个 defer 延迟执行的函数中调用 recover ,它便能捕捉在这之后的panic,然后正常处理。