golang面经——channel模块

一、面试题相关

1.channel 是否线程安全?锁用在什么地方?

分析:

channel配合goroutine可以用来实现并发编程,并且是go语言推荐的并发编程模式,那么肯定是可以保证线程安全的,可以先回顾下channel的底层定义,channel用make函数创建初始化的时候会在堆上分配一个hchan(runtime/chan.go包中)类型的数据结构

可以看到channel的底层实现中是有锁的,是通过mutex来保证线程安全的,所以在回答的时候要突出底层实现有锁。

回答:

一般来说,我们对channel就只有读,写,关闭三种操作,这三种操作,channel底层数据结构都用同一把runtime.Mutex来进行保护。

2.channel 的底层实现原理(数据结构)

分析:

这个问题其实是上一个问题的补充,channel的底层实现是一个hchan的结构,hchan的结构定义。

Go 复制代码
type hchan struct {
    qcount   uint           // 当前队列中剩余的元素个数
    dataqsiz uint           // 环形队列的大小,即可以缓存的元素数量(make(chan T, N) 中的 N)
    buf      unsafe.Pointer // 指向环形队列的指针(有缓冲 channel 才非 nil)
    elemsize uint16         // 每个元素的大小
    closed   uint32         // channel 是否已关闭的标志
    elemtype *_type         // 元素类型,用于在运行时进行类型检查
    sendx    uint           // 发送索引(send index),指向环形队列中下一个要发送的位置
    recvx    uint           // 接收索引(receive index),指向环形队列中下一个要接收的位置
    recvq    waitq          // 等待接收的 goroutine 队列(链表结构)
    sendq    waitq          // 等待发送的 goroutine 队列(链表结构)
    lock     mutex          // 互斥锁,保护 hchan 中的所有字段
}

// waitq 是一个 sudog 的链表,sudog 代表了一个等待中的 goroutine
type waitq struct {
    first *sudog
    last  *sudog
}

回答:

补充讲一下等待队列

  1. 发送等待队列 (sendq)

作用:存储所有因无法立即发送数据而被阻塞的 goroutine。

触发条件:

无缓冲 Channel:没有接收者在等待时

有缓冲 Channel:缓冲区已满时

队列中的元素:每个被阻塞的发送者 goroutine 被打包成一个 sudog 节点。

  1. 接收等待队列 (recvq)

作用:存储所有因无法立即接收数据而被阻塞的 goroutine。

触发条件:

无缓冲 Channel:没有发送者在等待时

有缓冲 Channel:缓冲区为空时

队列中的元素:每个被阻塞的接收者 goroutine 被打包成一个 sudog 节点。

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

分析:

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

回答:

1.对nil的channel进行读和写

都会造成当前goroutine永久阻寨(如果当前goroutine是main goroutine,则会让整个程序直接报fatal error 退出,也就是报错deadlock),关闭则会发生panic。

2.对已经关闭的channel进行写 和 再次关闭

都会导致panic,而读操作的话,会一直将channel中的数据读完2读完之后,每次读channel都会获得一个对应类型的零值。

3.对一个正常的channel进行读写都有两种情况:

a.读:阻塞挂起或者成功发送

b.写:阻塞挂起或者成功接收

c.关闭:正常close

4、Go 语言中有缓存(Buffered)和无缓存(Unbuffered)的 Channel

性能考虑

无缓存 Channel 由于涉及更直接的 goroutine 调度(挂起和唤醒),在频繁通信且没有同步需求的场景下,性能开销可能稍大。

有缓存 Channel 在缓冲区未满/未空时,操作接近于一个简单的队列,性能更好。但如果缓冲区大小设置不当(太小仍会频繁阻塞,太大浪费内存并增加延迟),也会影响性能。

Go 复制代码
package main

import (
    "fmt"
    "sync"
    "time"
)

// 场景1:任务协同(无缓存 - 适合)
func taskCoordination() {
    fmt.Println("=== 任务协同(无缓存Channel)===")
    done := make(chan struct{}) // 无缓存,用于信号通知
    
    go func() {
        fmt.Println("Worker: 处理任务中...")
        time.Sleep(1 * time.Second)
        fmt.Println("Worker: 任务完成!")
        done <- struct{}{} // 发送完成信号
    }()
    
    <-done // 等待任务完成
    fmt.Println("Main: 收到完成信号,继续执行")
}

// 场景2:限流器(有缓存 - 适合)
func rateLimiter() {
    fmt.Println("\n=== 限流器(有缓存Channel)===")
    limiter := make(chan time.Time, 3) // 容量3的限流器
    
    // 初始化限流器
    for i := 0; i < 3; i++ {
        limiter <- time.Now()
    }
    
    // 每秒补充一个令牌
    go func() {
        for t := range time.Tick(1 * time.Second) {
            limiter <- t
        }
    }()
    
    // 模拟10个请求
    var wg sync.WaitGroup
    for i := 1; i <= 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            <-limiter // 获取令牌(缓冲区有数据立即返回,空则阻塞)
            fmt.Printf("请求 %d 执行 at %v\n", id, time.Now().Format("15:04:05"))
        }(i)
    }
    wg.Wait()
}

func main() {
    taskCoordination()
    rateLimiter()
}

5.对channel 进行读写数据的流程是怎样的?

分析:

考察对channel 底层结构以及chansend和chanrecv流程的掌握程度,下面回答不区分有缓冲channel 和 无缓冲channel,注意理解。

回答:

操作一个不为nil,并且未关闭的channel,读和写都有两种情况:

1.读操作:

成功读取:

**如果channel中有数据,**直接从channel里面读取,并且此时如果写等待队列里面有goroutine,还需要将队列头部goroutine数据放入到channel中,并唤醒这个goroutine。

channel没有数据,就尝试从 写等待队列 头部goroutine读取数据,并如果做对应的唤醒操作。

阻塞挂起:channel里面没有数据 并且 写等待队列为空,则将当前goroutine 加入 读等待队列中,并挂起,等待唤醒。

2.写操作

成功写入:

**如果当前读等待队列为空,**将数据写入到channel环形缓冲中。

如果channel 读等待队列不为空,则取 头部goroutine,将数据直接复制给这个头部goroutine,并将其唤醒,流程结束。

阻塞挂起:通道里面无法存放数据 并且 读等待队列为空,则当前goroutine 加入写等待队列中,并挂起,等待唤醒。

6、select的底层原理

分析:

**select 的核心目标是:**监听多个 channel 上的发送或接收操作,当其中一个可以立即执行时,就执行它。如果多个同时可执行,则随机选择一个,以保证公平性。如果都没有,则要么阻塞(无 default 时),要么执行 default 语句。

有default的分支是非阻塞性select,下面代码能够较好的展示:

Go 复制代码
package main

import "fmt"

func main() {
	ch := make(chan string)

	select {
	case msg := <-ch: // 尝试从ch接收
		fmt.Println("Received:", msg)
	default: // 如果ch没有立即可用的数据,就执行这里
		fmt.Println("No message received")
	}

	// 尝试向ch发送,但因为没有接收者,会失败
	select {
	case ch <- "hello": // 尝试向ch发送
		fmt.Println("Sent message")
	default:
		fmt.Println("No message sent")
	}
}

/*
No message received
No message sent
*/

select也被称为多路select,指的是一个goroutine 可以服务多个 channel的读或写操作,要清楚的知道 select分为两种,包含非阻塞型select(包含default分支的)阻塞型select(不包含default分支的),然后再回答其对应原理。

回答:

select的核心原理是,按照随机的顺序执行case,直到某个case完成操作,如果所有case的都没有完成操作,则看有没有defaut分支,如果有default分支,则直接走default,防止阻塞。

如果没有的话,需要将当前goroutine 加入到所有case对应channel的等待队列中,并挂起当前goroutine,等待唤醒。

如果当前goroutine被某一个case 上的channel操作唤醒后,还需要将当前goroutine从所有case对应channel的等待队列中剔除。

相关推荐
赵谨言3 小时前
基于python数据挖据的教学监控系统的设计与应用
开发语言·经验分享·python
润 下3 小时前
C语言——深入理解函数声明定义和调用访问
c语言·开发语言·经验分享·笔记·程序人生·其他
一只自律的鸡4 小时前
【python】从Hello World到数据类型
开发语言·python
寻星探路4 小时前
Java EE初阶启程记05---线程安全
java·开发语言·java-ee
xb11324 小时前
C#——方法的定义、调用与调试
开发语言·c#
froginwe114 小时前
MVC HTML 帮助器
开发语言
王嘉俊9255 小时前
ThinkPHP 入门:快速构建 PHP Web 应用的强大框架
开发语言·前端·后端·php·框架·thinkphp
syty20205 小时前
AST语法树应用于sql检查
java·开发语言·ast
倔强菜鸟5 小时前
2025.8.10-学习C++(一)
开发语言·c++·学习