文章目录
- [1、channel 是否线程安全?锁用在什么地方?](#1、channel 是否线程安全?锁用在什么地方?)
- [2、go channel 的底层实现原理(数据结构)](#2、go channel 的底层实现原理(数据结构))
- [3、nil、关闭的 channel、有数据的 channel,再进行读、写、关闭会怎么样?](#3、nil、关闭的 channel、有数据的 channel,再进行读、写、关闭会怎么样?)
- [4、向 channel 发送数据和从 channel 读数据的流程是什么样的?](#4、向 channel 发送数据和从 channel 读数据的流程是什么样的?)
- 5、哪些操作会使channel发生panic?
- 6、当一个channel关闭后,我们是否还能从channel读到数据?
- 7、channel是创建在堆上还是栈上
- 8、channel发送和接收元素的本质是什么?
- 9、有4个goroutine,编号为1、2、3、4。每秒钟会有一个goroutine打印出自己的编号,要求写一个程序,让输出的编号总是按照1、2、3、4、1、2、3、4...的顺序打印出来
- 10、用channel实现一个限流器
- 11、用channel实现一个互斥锁
1、channel 是否线程安全?锁用在什么地方?
是线程安全的,hchan的底层实现中,hchan结构体中采用runtime.mutex锁来保证数据读写安全。在对循环数组buf中的数据进行入队和出队操作时,必须先获取互斥锁,才能操作channel数据
2、go channel 的底层实现原理(数据结构)
channel的底层实现是一个hchan的结构
回顾这个图

go
type hchan struct {
qcount uint // channel 环形数组中元素的数量
dataqsiz uint // channel 环形数组的容量
buf unsafe.Pointer // 指向循环队列的指针
elemsize uint16 // 循环队列中的每个元素的大小
closed uint32 // 标记位,标记channel是否关闭
elemtype *type // 循环队列中元素类型
sendx uint // 下一次发送元素在循环队列中的索引位置
recvx uint // 下一次接收元素在循环队列中的索引位置
recvq waitq // 等待从channel接收消息的sudog队列
sendq waitq // 等待向channel写入消息的sudog队列
lock mutex // 互斥锁,对channel的数据读写操作加锁,保证并发安全
}
3、nil、关闭的 channel、有数据的 channel,再进行读、写、关闭会怎么样?

-
对nil的channel进行读和写 都会造成当前goroutine永久阻塞(如果当前goroutine是main goroutine,则会让整个程序直接报fatal error 退出,也就是报错deadlock),关闭则会发生panic
-
对已经关闭的channel进行写 和 再次关闭,都会导致panic,而读操作的话,会一直将channel中的数据读完,读完之后,每次读channel都会获得一个对应类型的零值
-
对一个正常的channel进行读写都有两种情况
a. 读:阻塞挂起或者成功接收
b. 写:阻塞挂起或者成功发送
c. 关闭:正常close
4、向 channel 发送数据和从 channel 读数据的流程是什么样的?
考察对channel 底层结构以及chansend和chanrecv流程的掌握程度,下面回答不区分有缓冲channel 和 无缓冲channel,注意理解
下面是对一个非nil,且未关闭的channel进行读写的流程
回答
操作一个不为nil,并且未关闭的channel,读和写都有两种情况
1、读操作:
a、成功读取:
-
如果channel中有数据,直接从channel里面读取,并且此时如果写等待队列里面有goroutine,还需要将队列头部goroutine数据放入到channel中,并唤醒这个goroutine
-
如果channel没有数据,就尝试从 写等待队列 头部goroutine读取数据,并做对应的唤醒操作
b、阻塞挂起:channel里面没有数据 并且 写等待队列为空,则将当前goroutine 加入 读等待队列中,并挂起,等待唤醒
2、写操作
a、成功写入:
-
如果channel 读等待队列不为空,则取 头部goroutine,将数据直接复制给这个头部goroutine,并将其唤醒,流程结束
-
否则就尝试将数据写入到channel 环形缓冲中
b、阻塞挂起:通道里面无法存放数据 并且 读等待队列为空,则当前goroutine 加入写等待队列中,并挂起,等待唤醒
5、哪些操作会使channel发生panic?
三种情况
情况1:往一个已经关闭的channel写数据
情况2:关闭一个nil的channel
情况3:关闭一个已经关闭的channel
6、当一个channel关闭后,我们是否还能从channel读到数据?
当一个 channel 关闭后,我们仍然能从 channel 读到数据------但读到的是两类情况:
- 先把缓冲区里剩余的数据读完(如果是带缓冲的 channel,或关闭前已经有数据进入缓冲)
- 读完后继续读:每次都会立刻返回该类型的零值
7、channel是创建在堆上还是栈上
堆上,新建一个 chan 后,内存在堆上分配,大概长这样:

首先,要实现channel这样的复杂功能,肯定不是几个字节可以搞定的,所以需要一个struct来实现;其次,这种被设计用来实现协程间通信的组件,其作用域和生命周期不可能仅限于某个函数内部,所以golang直接将其分配在堆上
8、channel发送和接收元素的本质是什么?
值的拷贝
例子1
go
package main
import (
"fmt"
"time"
)
func print(u <-chan int) {
time.Sleep(2 * time.Second)
fmt.Println("print int", <-u)
}
func main() {
c := make(chan int, 5)
a := 0
c <- a
fmt.Println(a)
// modify g
a = 1
go print(c)
time.Sleep(5 * time.Second)
fmt.Println(a)
}
go
root@GoLang:~/proj/goforjob# go run main.go
0
print int 0
1
例子2
go
package main
import (
"fmt"
"time"
)
type people struct {
name string
}
var u = people{name: "A"}
func printPeople(u <-chan *people) {
time.Sleep(2 * time.Second)
fmt.Println("printPeople", <-u)
}
func main() {
c := make(chan *people, 5)
var a = &u
c <- a
fmt.Println(a)
// modify g
a = &people{name: "B"}
go printPeople(c)
time.Sleep(5 * time.Second)
fmt.Println(a)
}
因为a = &people{name: "B"} 里的是新创建的 people 对象(name 是 "B")
go
root@GoLang:~/proj/goforjob# go run main.go
&{A}
printPeople &{A}
&{B}
例子3
go
package main
import (
"fmt"
"time"
)
type people struct {
name string
}
var u = people{name: "A"}
func printPeople(u <-chan *people) {
time.Sleep(2 * time.Second)
a := <-u
fmt.Printf("我和main &u这个指针一样%p\n", a)
fmt.Println("printPeople", a)
}
func main() {
c := make(chan *people, 5)
var a = &u
fmt.Printf("我和printPeople 里面的指针一样%p\n", a)
c <- a
fmt.Println(a)
// modify g
a = &people{name: "B"}
go printPeople(c)
time.Sleep(5 * time.Second)
fmt.Println(a)
fmt.Printf("我不一样,我是&people{name: \"B\"}%p\n", a)
}
go
root@GoLang:~/proj/goforjob# go run main.go
我和printPeople 里面的指针一样0x56aff0
&{A}
我和main &u这个指针一样0x56aff0
printPeople &{A}
&{B}
我不一样,我是&people{name: "B"}0xc00009a030
root@GoLang:~/proj/goforjob#
例子4
go
package main
import (
"fmt"
"time"
)
type student struct {
name string
}
func print2(u <-chan *student) {
time.Sleep(2 * time.Second)
fmt.Println("ok", <-u)
}
func main() {
fmt.Println("Hello, World!")
c2 := make(chan *student, 5)
a2 := student{"px"}
c2 <- &a2
a2 = student{"xp"}
go print2(c2)
time.Sleep(5 * time.Second)
}
go
root@GoLang:~/proj/goforjob# go run main.go
Hello, World!
ok &{xp}
9、有4个goroutine,编号为1、2、3、4。每秒钟会有一个goroutine打印出自己的编号,要求写一个程序,让输出的编号总是按照1、2、3、4、1、2、3、4...的顺序打印出来
方法1
go
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 1)
i := 1
recv := 0
for {
ch <- i
i++
if i == 5 {
i = 1
}
recv = <-ch
if recv == 1 {
go func() {
fmt.Println("1")
}()
}
if recv == 2 {
go func() {
fmt.Println(2)
}()
}
if recv == 3 {
go func() {
fmt.Println(3)
}()
}
if recv == 4 {
go func() {
fmt.Println(4)
}()
}
time.Sleep(time.Second)
}
}
go
root@GoLang:~/proj/goforjob# go run main.go
1
2
3
4
1
2
3
4
1
2
3
4
1
2
3
4
1
2
3
4
^Csignal: interrupt
root@GoLang:~/proj/goforjob#
方法2
go
package main
import (
"fmt"
"time"
)
func main() {
// 4个channel
chs := make([]chan int, 4)
for i := range chs {
chs[i] = make(chan int)
// 开4个协程
go func(i int) {
for {
// 获取当前channel值并打印
v := <-chs[i]
fmt.Println(v + 1)
time.Sleep(time.Second)
// 把下一个值写入下一个channel,等待下一次消费
chs[(i+1)%4] <- (v + 1) % 4
}
}(i)
}
// 往第一个塞入0
chs[0] <- 0
select {}
}
go
root@GoLang:~/proj/goforjob# go run main.go
1
2
3
4
1
2
3
4
1
2
3
4
1
2
3
4
1
2
3
4
1
2
3
^Csignal: interrupt

10、用channel实现一个限流器
go
package main
import (
"fmt"
"time"
)
func main() {
// 每次处理3个请求
chLimit := make(chan struct{}, 3)
for i := range 20 {
chLimit <- struct{}{}
go func(i int) {
fmt.Println("下游服务处理逻辑...", i)
time.Sleep(time.Second * 3)
<-chLimit
}(i)
}
time.Sleep(30 * time.Second)
}
go
root@GoLang:~/proj/goforjob# go run main.go
下游服务处理逻辑... 0
下游服务处理逻辑... 2
下游服务处理逻辑... 1
下游服务处理逻辑... 4
下游服务处理逻辑... 3
下游服务处理逻辑... 5
下游服务处理逻辑... 8
下游服务处理逻辑... 6
下游服务处理逻辑... 7
下游服务处理逻辑... 11
下游服务处理逻辑... 9
下游服务处理逻辑... 10
下游服务处理逻辑... 14
下游服务处理逻辑... 12
下游服务处理逻辑... 13
下游服务处理逻辑... 16
下游服务处理逻辑... 15
下游服务处理逻辑... 17
下游服务处理逻辑... 18
下游服务处理逻辑... 19
root@GoLang:~/proj/goforjob#
11、用channel实现一个互斥锁
思路:初始化一个缓冲区为1的 channel,放入元素代表一把锁,谁获取到这个元素就代表获取了这把锁,释放锁的时候再把这个元素放回 channel
go
package main
import (
"log"
"time"
)
type Mutex struct {
ch chan struct{}
}
// 初始化锁
func NewMutex() *Mutex {
mu := &Mutex{make(chan struct{}, 1)}
mu.ch <- struct{}{}
return mu
}
// 加锁,阻塞获取
func (m *Mutex) Lock() {
<-m.ch
}
// 释放锁
func (m *Mutex) Unlock() {
select {
// 成功写入channel代表释放成功
case m.ch <- struct{}{}:
default:
panic("unlock of unlocked mutex")
}
}
// 尝试获取锁
func (m *Mutex) TryLock() bool {
select {
case <-m.ch:
return true
default:
}
return false
}
func (m *Mutex) LockTimeout(timeout time.Duration) bool {
// 当过了 timeout 这段时间后,定时器会往 timer.C 里"发送一个时间值"
timer := time.NewTimer(timeout)
select {
case <-m.ch:
// 成功获取锁关闭定时器
timer.Stop()
return true
case <-timer.C:
// 获取锁超时
return false
}
}
// 是否上锁
func (m *Mutex) IsLocked() bool {
return len(m.ch) == 0
}
func main() {
m := NewMutex()
ok := m.TryLock()
log.Printf("locked v %v\n", ok)
ok = m.TryLock()
log.Printf("locked v %v\n", ok)
go func() {
time.Sleep(5 * time.Second)
m.Unlock()
}()
ok = m.LockTimeout(10 * time.Second)
log.Printf("LockTimeout v %v\n", ok)
}
go
root@GoLang:~/proj/goforjob# go run main.go
2026/01/17 22:56:40 locked v true
2026/01/17 22:56:40 locked v false
2026/01/17 22:56:45 LockTimeout v true
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!