文章目录
- [Context 相关:](#Context 相关:)
- [1、context 结构是什么样的?](#1、context 结构是什么样的?)
-
- 回答
-
- [1. go语言里的**context实际上是一个接口,提供了四种方法**:](#1. go语言里的context实际上是一个接口,提供了四种方法:)
- [2. 有emptyCtx、cancelCtx、timerCtx、valueCtx四种实现](#2. 有emptyCtx、cancelCtx、timerCtx、valueCtx四种实现)
- cancelCtx会在cancelFunc的时候关闭子协程吗?是直接关闭吗?
- [2、context 使用场景和用途?(基本必问)](#2、context 使用场景和用途?(基本必问))
- [Channel 相关:](#Channel 相关:)
- [1、channel 是否线程安全?锁用在什么地方?](#1、channel 是否线程安全?锁用在什么地方?)
- [2、channel 的底层实现原理(数据结构)](#2、channel 的底层实现原理(数据结构))
- [3、nil、关闭的 channel、有数据的 channel,再进行读、写、关闭会怎么样?(各类变种题型)](#3、nil、关闭的 channel、有数据的 channel,再进行读、写、关闭会怎么样?(各类变种题型))
- [4. 对channel 进行读写数据的流程是怎样的](#4. 对channel 进行读写数据的流程是怎样的)
- [5、select 的底层原理](#5、select 的底层原理)
Context 相关:
1、context 结构是什么样的?
分析
关于context要清楚具体是什么,context其实是一个接口,提供了四种方法,而在官方go语言中对context接口提供了4种基本类型的实现,回答的时候,要答出接口以及几种实现结构
回答
1. go语言里的context实际上是一个接口,提供了四种方法:
go
Deadline() (deadline time.Time, ok bool) // 第一个返回值返回 context.Context 被取消的时间,即截止时间;第二个返回值如果未设置截止日期,Deadline 函数返回 ok==false。连续调用 Deadline 函数会返回相同的结果。true代表当前这个context有超时时间,或者其父context(或其祖宗)有超时时间 flase就是相反的情况,没有超时,其先辈也没有设置超时 更简单来说就是这个context有没有被超时时间控制
Done() <-chan struct{} // Done() 返回一个 只读channel,当这个channel被关闭时,说明这个context被取消
Err() error // Err() 返回一个错误,表示channel被关闭的原因,例如是被取消,还是超时关闭
Value(key interface{}) interface{} // value方法返回指定key对应的value,这是context携带的值
2. 有emptyCtx、cancelCtx、timerCtx、valueCtx四种实现
- a. emptyCtx:emptyCtx 虽然实现了 context 接口,但是不具备任何功能,因为实现很简单,基本都是直接返回空值
- i. 我们一般调用 context.Background() 和 context.TODO() 都是返回一个 *emptyCtx 的动态类型(通过静态类型 context.Context 传递)

-
b. cancelCtx:cancelCtx 同时实现 Context 和 canceler 接口 ,通过取消函数 cancelFunc 实现退出通知。注意其退出通知机制不但通知自己,同时也通知其children节点。
- i. 我们一般调用 context.WithCancel() 就会返回一个 *cancelCtx 和 cancelFunc
- 通俗点理解,就是WithCancel(parent) 返回一个 Context(底层通常是 *cancelCtx)和一个 CancelFunc。调用 CancelFunc 会触发底层的取消逻辑:设置 Err() 为 context.Canceled,关闭 Done() channel,并把取消向子 context 传播。于是 select 里的 case <-ctx.Done(): 会立刻触发,不再阻塞。
-
c. timerCtx:timerCtx 是一个实现了 Context 接口的具体类型,其内部封装了 cancelCtx 类型实例,同时也有个 deadline 变量,用来实现定时退出通知。timerCtx 实际上是在 cancelCtx 之上构建的,唯一的区别就是增加了计时器和截止时间。
- i. 我们一般调用 context.WithTimeout() 就会返回一个 *timerCtx 和 cancelFunc,不仅可以定时通知,也可以调用 cancelFunc 进行通知
- ii. 调用 context.WithDeadline() 也可以,WithTimeout 是多少秒后进行通知,WithDeadline 是在某个时间点通知,本质上,WithTimeout 会转而 WithDeadline
-
d. valueCtx:valueCtx 是一个实现了 Context 接口的具体类型 ,其内部封装了 Context 接口类型,同时也封装了一个 k/v 的存储变量,其是一个实现了数据传递
- i. 我们一般 context.WithValue() 来得到一个 *valueCtx,valueCtx 可以继承它的 parent valueCtx 中的 {key, value}
如果一个实例既实现了 context 接口又实现了 canceler 接口,那么这个 context 就是可以被取消的,比如 cancelCtx 和 timerCtx。如果仅仅只是实现了 context 接口,而没有实现 canceler,就是不可取消的,比如 emptyCtx 和 valueCtx。
cancelCtx会在cancelFunc的时候关闭子协程吗?是直接关闭吗?
cancelFunc() 不会强制杀死子 goroutine,它只是关闭了 context 的信号通道,子 goroutine 需要自己监听并优雅退出

2、context 使用场景和用途?(基本必问)
分析
这个问题其实可以以上一个问题的补充提问,在明确了 context 是什么之后,即 context 接口提供了哪些哪些方法,以及有哪些实践之后,看似联想出这些实现是为了解决什么问题,主要突出两点:上下文信息传递和协程的取消控制
回答
-
context 主要用来在 goroutine 之间传递上下文信息,比如传递请求的 trace_id,以便于追踪全局唯一请求【在微服务调用链中,可以确定该请求是在哪个微服务出错了】
-
另一个用处是可以用来做
取消控制,通过取消信号和超时时间来控制子 goroutine 的退出,防止 goroutine 泄漏包括:取消信号、超时时间、截止时间、k-v 等。
Channel 相关:
1、channel 是否线程安全?锁用在什么地方?
分析
channel 配合 goroutine 可以用来实现并发编程,并且是 go 语言推荐的并发编程模式,那么肯定是可以保证线程安全的,可以先回顾下 channel 的底层定义,channel 用 make 函数创建初始化的时候会在堆上分配一个 runtime.hchan 类型的数据结构
go
type hchan struct {
qcount uint // channel 循环队列中的元素总数
dataqsiz uint // channel 循环队列大小
buf unsafe.Pointer // 指向channel 环形数组的一个指针
elemsize uint16 // 循环队列中的每个元素所占的字节数
closed uint32 // 标记位,标记channel是否关闭
timer *timer // timer feeding this chan
elemtype *_type // 循环队列的元素类型
sendx uint // send index 下一次写的位置
recvx uint // receive index 下一次读的位置
recvq waitq // list of recv waiters 等待从channel接收消息的sudog队列
sendq waitq // list of send waiters 等待向channel写入消息的sudog队列
bubble *synctestBubble
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex // runtime.mutex,对channel的数据读写操作加锁,保证并发安全
}
可以看到 channel 的底层实现中是有锁的,是通过 mutex 来保证线程安全的,所以在回答的时候要突出底层实现有锁
回答
一般来说,我们对 channel 就只有读、写、关闭三种操作,这三种操作,channel 底层数据结构都用同一把 runtime.Mutex 进行保护
2、channel 的底层实现原理(数据结构)
分析
这个问题其实是上一个问题的补充,channel 的底层实现是一个 hchan 的结构,hchan 的结构定义
回顾这个图

go
type hchan struct {
qcount uint // channel 循环队列中的元素总数
dataqsiz uint // channel 循环队列大小
buf unsafe.Pointer // 指向channel 环形数组的一个指针
elemsize uint16 // 循环队列中的每个元素所占的字节数
closed uint32 // 标记位,标记channel是否关闭
timer *timer // timer feeding this chan
elemtype *_type // 循环队列的元素类型
sendx uint // send index 下一次写的位置
recvx uint // receive index 下一次读的位置
recvq waitq // list of recv waiters 等待从channel接收消息的sudog队列
sendq waitq // list of send waiters 等待向channel写入消息的sudog队列
bubble *synctestBubble
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex // runtime.mutex,对channel的数据读写操作加锁,保证并发安全
}
回答
-
对于包含缓冲的channel,go语言的channel底层是一个hchan的结构,里面包含一个指向循环数组的指针,这个循环数组就是用于存储数据的。当然还包含下次读取和下次发送的数据索引位置recvx和sendx。无缓冲 channel(dataqsiz=0):逻辑上没有缓冲区,buf 不用于存数据(可能为 nil),发送/接收主要走"直接把值从发送方拷到接收方"的路径 + 等待队列。
-
还包含两个goroutine等待队列 ,在一个goroutine对这个channel读写阻塞的时候会分情况放到这两个队列里,发送数据阻塞就放到sendq这个等待队列,接收数据阻塞就放到recvq这个等待队列
-
为了保证channel的线程安全,hchan结构还有一个互斥锁,用作数据读写时候加锁,当前close channel也会用到这个互斥锁
3、nil、关闭的 channel、有数据的 channel,再进行读、写、关闭会怎么样?(各类变种题型)
分析
主要是考察对channel在各个状态下进行读写操作会出现什么结果,这块建议自己代码跑一下各个场景,加深一下理解


-
对nil的channel进行读和写,都会造成当前goroutine永久阻塞(如果当前goroutine是main goroutine,则会让整个程序直接报fatal error退出,也就是报错deadlock ),关闭则会发生panic
-
对已经关闭的channel进行写和再次关闭,都会导致panic,而读操作的话,会一直将channel中的数据读完,读完之后,每次读channel都会获得一个对应类型的零值
-
对一个正常的channel进行读写都有两种情况
-
a. 读:
- 成功读取: 如果channel中有数据,直接从channel里面读取,如果此时写等待队列里面有goroutine,还需要将队列头部goroutine数据写入到channel中,并唤醒这个goroutine;如果channel没有数据,就尝试从写等待队列中读取数据,并做对应的唤醒操作【指channel没有数据,但有写等待者,就是无缓冲的channel】
- 阻塞挂起(读操作无法及时完成): channel里面没有数据 并且 写等待队列为空,则当前goroutine 加入读等待队列中,并挂起,等待唤醒
-
b. 写:
- 成功写入: 如果channel 读等待队列不为空,则取 头部goroutine,将数据直接复制给这个头部goroutine,并将其唤醒,流程结束;否则就尝试将数据写入到channel 环形缓冲中
- 阻塞挂起(写操作无法及时完成): 通道里面buf满了 并且 读等待队列为空,则当前goroutine 加入写等待队列中,并挂起,等待唤醒
-
c. 关闭:正常close,在加锁后把 hchan.closed 置为 1,然后将 recvq 中等待接收的 goroutine 以及 sendq 中等待发送的 goroutine 都取出加入待唤醒列表并统一唤醒
下面是与回答无关的内容
给出对nil channel 操作的测试代码
go
package main
import (
"fmt"
"time"
)
// 为nil的channel
var ch chan int
func main() {
// 对nil channel进行读操作
// receiveExample1()
// receiveExample2()
// 对nil channel进行写操作
// sendExample1()
// sendExample2()
// 对nil channel进行close操作
// close(ch)
// 非阻塞模式
select {
case <-ch:
fmt.Println("1")
default:
fmt.Println("default")
}
time.Sleep(1 * time.Second)
}
// 在主goroutine对nil channel进行读
func receiveExample1() {
<-ch
}
// 在普通goroutine对nil channel进行读
func receiveExample2() {
go func() {
<-ch
}()
}
// 在主goroutine对nil channel进行写
func sendExample1() {
ch <- 1
}
// 在普通goroutine对nil channel进行写
func sendExample2() {
go func() {
ch <- 1
}()
}
go
root@GoLang:~/proj/goforjob# go run main.go
default
root@GoLang:~/proj/goforjob#
4. 对channel 进行读写数据的流程是怎样的
分析
考察对channel 底层结构以及chansend和chanrecv流程的掌握程度,下面回答不区分有缓冲channel 和 无缓冲channel,注意理解
下面是对一个非nil,且未关闭的channel进行读写的流程
回答
操作一个不为nil,并且未关闭的channel,读和写都有两种情况
recv:优先从缓冲读,其次直接配对 sendq 的发送者,否则入 recvq 阻塞;send:优先直接配对 recvq 的接收者,其次写入缓冲,否则入 sendq 阻塞
读操作:
-
a. 成功读取:
- 如果channel中有数据,直接从channel里面读取,并且此时如果写等待队列里面有goroutine,还需要将队列头部goroutine数据放入到channel中,并唤醒这个goroutine【对于有缓冲Channel读取(有写等待队列那么缓冲区必满),首先拿环形缓冲区recvx下标对应的元素,并将写等待队列对头sudog中的elem写入到缓冲区刚释放出来的队头槽位(即队尾元素)】
- 如果channel没有数据,就尝试从 写等待队列 头部goroutine读取数据,并做对应的唤醒操作
-
b. 阻塞挂起: channel里面没有数据 并且 写等待队列为空,则将当前goroutine加入 读等待队列中,并挂起,等待唤醒
写操作
- a. 成功写入:
- 如果channel 读等待队列不为空,则取 头部goroutine,将数据直接复制给这个头部goroutine,并将其唤醒,流程结束
- 否则就尝试将数据写入到channel 环形缓冲中
- b. 阻塞挂起: 通道里面无法存放数据 并且 读等待队列为空,则当前goroutine 加入写等待队列中,并挂起,等待唤醒
5、select 的底层原理
分析
select也被称为多路select,指的是一个goroutine 可以服务多个 channel的读或写操作 ,要清楚的知道select分为两种,包含非阻塞型select(包含default分支的) 和 阻塞型select(不包含default分支的) 然后再回答对应原理
回答
select的核心原理是,按照随机的顺序执行case,直到某个case完成操作,如果所有case的都没有完成操作,则看看有没有default分支,如果有default分支,则直接走default,防止阻塞
如果没有的话,需要将当前goroutine 加入到所有case对应channel的等待队列中,并挂起当前goroutine,等待唤醒。
如果当前goroutine被某一个case上的channel操作唤醒后,还需要将当前goroutine从所有case对应channel的等待队列中删除
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!