Golang原理剖析(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,再进行读、写、关闭会怎么样?

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

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

  3. 对一个正常的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 读到数据------但读到的是两类情况:

  1. 先把缓冲区里剩余的数据读完(如果是带缓冲的 channel,或关闭前已经有数据进入缓冲)
  2. 读完后继续读:每次都会立刻返回该类型的零值

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

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

相关推荐
H Corey2 小时前
Java--面向对象之继承与多态
java·开发语言·windows·学习·算法·intellij-idea
Gofarlic_OMS2 小时前
如何将MATLAB网络并发许可证闲置率降至10%以下
大数据·运维·服务器·开发语言·人工智能·matlab·制造
ejinxian2 小时前
2026 年 Java 开发计划-Oracle公布
java·开发语言·java 开发计划
Sylvia-girl2 小时前
Java之日志框架
java·开发语言
小程同学>o<2 小时前
嵌入式之ARM体系与架构面试题(一)硬件基础篇
arm开发·笔记·学习·面试·架构
oioihoii2 小时前
QT跨平台一次编写,处处编译
开发语言·qt
edisao2 小时前
四。SpaceX、网络化与未来的跨越:低成本、高频次的真正威胁
大数据·开发语言·人工智能·科技·php
qq_336313932 小时前
java基础-多线程练习
java·开发语言·算法
wjs20242 小时前
《jEasyUI 树形网格添加分页》
开发语言