Golang面试题库(Context、Channel)

文章目录

  • [Context 相关:](#Context 相关:)
  • [1、context 结构是什么样的?](#1、context 结构是什么样的?)
  • [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 接口提供了哪些哪些方法,以及有哪些实践之后,看似联想出这些实现是为了解决什么问题,主要突出两点:上下文信息传递和协程的取消控制

回答

  1. context 主要用来在 goroutine 之间传递上下文信息,比如传递请求的 trace_id,以便于追踪全局唯一请求【在微服务调用链中,可以确定该请求是在哪个微服务出错了】

  2. 另一个用处是可以用来做取消控制通过取消信号和超时时间来控制子 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的数据读写操作加锁,保证并发安全
}

回答

  1. 对于包含缓冲的channel,go语言的channel底层是一个hchan的结构,里面包含一个指向循环数组的指针,这个循环数组就是用于存储数据的。当然还包含下次读取和下次发送的数据索引位置recvx和sendx。无缓冲 channel(dataqsiz=0):逻辑上没有缓冲区,buf 不用于存数据(可能为 nil),发送/接收主要走"直接把值从发送方拷到接收方"的路径 + 等待队列。

  2. 还包含两个goroutine等待队列 ,在一个goroutine对这个channel读写阻塞的时候会分情况放到这两个队列里,发送数据阻塞就放到sendq这个等待队列,接收数据阻塞就放到recvq这个等待队列

  3. 为了保证channel的线程安全,hchan结构还有一个互斥锁,用作数据读写时候加锁,当前close channel也会用到这个互斥锁

3、nil、关闭的 channel、有数据的 channel,再进行读、写、关闭会怎么样?(各类变种题型)

分析

主要是考察对channel在各个状态下进行读写操作会出现什么结果,这块建议自己代码跑一下各个场景,加深一下理解

  1. 对nil的channel进行读和写,都会造成当前goroutine永久阻塞(如果当前goroutine是main goroutine,则会让整个程序直接报fatal error退出,也就是报错deadlock ),关闭则会发生panic

  2. 对已经关闭的channel进行写和再次关闭,都会导致panic,而读操作的话,会一直将channel中的数据读完,读完之后,每次读channel都会获得一个对应类型的零值

  3. 对一个正常的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的等待队列中删除

之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

相关推荐
2601_949817723 小时前
Spring Boot3.3.X整合Mybatis-Plus
spring boot·后端·mybatis
uNke DEPH3 小时前
Spring Boot的项目结构
java·spring boot·后端
zhenxin01224 小时前
Spring Boot 3.x 系列【3】Spring Initializr快速创建Spring Boot项目
spring boot·后端·spring
超级无敌暴龙兽4 小时前
和我一起刷面试题呀
前端·面试
前端一小卒4 小时前
前端工程师的全栈焦虑,我用 60 天治好了
前端·javascript·后端
不停喝水4 小时前
【AI+Cursor】 告别切图仔,拥抱Vibe Coding: AI + Cursor 开启多模态全栈新纪元 (1)
前端·人工智能·后端·ai·ai编程·cursor
oyzz1205 小时前
Spring EL 表达式的简单介绍和使用
java·后端·spring
zhenxin01225 小时前
【wiki知识库】07.用户管理后端SpringBoot部分
spring boot·后端·状态模式
码事漫谈5 小时前
OpenSpec 简明教程
后端
程序员小假5 小时前
向量检索的流程是怎样的?Embedding 和 Rerank 各自的作用?
java·后端