当前位置: 首页 > article >正文

GO 高级特性篇

1. context

1.1 什么是context?

上下文,这里指协程上下文,go在1.7引入的,用于在goroutine之间传递上下文信息和控制信号,包括跟踪,取消信号和超时等信息,这些信息可以被多个goroutine共享和使用,实现协作式的并发处理.


随着context包的引入,标准库中很多接口因此加上了context参数,比如database/sql包,context几乎成为了并发控制和超时控制的标准做法.

1.2 context的结构?

type Context interface {


// 当 context 被取消或者到了 deadline,返回一个被关闭的 channel
//它是一个只读的Channel,也就是说在整个生命周期都不会有写入操作,只有当这个channel被关闭时,才会读取到这个channel对应类型的零值,否则是无法读取到任何值的。

正是因为这个机制,当子协程从这个channel中读取到零值后可以做一些收尾工作,让子协程尽快退出。
Done() <-chan struct{}

// 在 channel Done 关闭后,返回 context 取消原因,这里只有两种原因,取消或者超时。
Err() error

// 返回 context 是否会被取消以及自动取消时间(即 deadline)
//通过这个时间我们可以判断是否有必要进行接下来的操作,如果剩余时间太短则可以选择不继续执行一些任务,可以节省系统资源。
Deadline() (deadline time.Time, ok bool)

// 获取 key 对应的 value
Value(key interface{}) interface{}
}

1.3 context的使用场景?

多个goroutine之间传递请求作用域的上下文的情况,有以下三种

信息传递
跨越多个 Goroutine 处理一个请求时,需要将请求上下文传递到每个Goroutine 中,比如我们可以将请求的 ID ,用户身份等信息传递给处理这个请求的多个Goroutine中.

取消任务
当某个 Goroutine 需要取消请求时,需要通知其他 Goroutine 停止处理。
比如当上层任务取消或超时时,可以通知下层任务及时退出,避免资源浪费或泄露。

超时控制
当某个 Goroutine 超时时,需要通知其他 Goroutine 停止处理。
比如可以为每个任务设定一个截止时间,一旦达到截止时间之后,任务会被自动取消。

2. switch的细节

case表达式可以是任意类型,多个值;
switch语句除了可以是常量变量外,还可以是表达式或者函数调用;
default分支可以放在其他case前面,后面或者中间;

常量case分支表达式不能重复,但布尔常量case可以重复(即便重复也是执行一个跳出);
switch中缺省默认值为bool类型且值为true;
switch和case分支的数据类型必须一致;
case分支只会命中一个,需要穿透使用fallthrough;
case中变量作用域为case代码块内;

3. defer底层数据结构是什么?

defer: 使用defer关键字的代码段可以在函数返回前执行,主要用于资源释放和异常捕获.

本质是一个用链表实现的栈的结构,在defer底层结构中有一个link指针,该指针会指向链表头部,每次声明defer时会头插一次,在获取时是从头部获取的,所以最后声明的defer最先执行.

defer栈与函数绑定,不同函数之间的defer栈是独立的.

4. 多个defer的执行顺序?

执行顺序先进后出,典型使用场景:
关闭文件或网络连接,关闭数据库连接,释放锁,捕获异常,取消任务,记录程序耗时等

5. 打开十万个文件,如何使用defer关闭资源?

当一次性打开大量文件时,就不能将关闭文件句柄操作全部放到defer中,
而是将文件处理封装到一个函数里,每个文件处理完后就关闭资源,
将defer放在文件处理的函数中在返回前执行.

func processFiles1() error {
	for i := 0; i < 100000; i++ {
		processFile("file" + strconv.Itoa(i) + ".txt")
	}
	return nil
}

func processFile(filePath string) error {
	file, err := os.Open(filePath)
	if err != nil {
		return err
	}
	defer file.Close()

	// 处理文件内容
	// ...
	return nil
}

6. defer容易踩的坑?

6.1 defer在return前执行,但return后面如果是表达式的话会先计算值保存在寄存器里,然后执行defer.

func main(){
	fmt.Println(f(2))
}
func f(x int) (r int) {
	defer func() {
		r += x // 修改返回值
	}()
	return x + x // <=> r = x + x; return
}
最终r=6

6.2 在执行 defer 语句时,先计算表达式的值,然后将其保存在一个新的栈帧中,并将该栈帧推入 defer 栈中。当函数执行完毕时,defer 栈中的所有栈帧会按照后进先出的顺序被执行,每个栈帧中保存的表达式也会被依次执行。

也就是说,defer虽然最后执行,但它是按执行顺序先将值压入栈的,并不是最后赋值. 

6.3 避免在for循环中使用defer,避免在defer中引用大对象或者闭包变量,容易导致不能及时被垃圾回收器回收.

避免这种内存泄漏的方法是在defer执行后显式地将对象或变量置为nil.

7. 容易被忽略的panic和recover的一些细节

理解panic: go在运行时出现的一个异常情况,若异常未被捕获,go程序执行会被终止.

如何触发panic: 两种来源,一种是来自go运行时(比如发生空指针或越界等),另一种是调用panic函数主动触发的

panicking的执行过程: 一旦 panic 被触发,后续 Go 程序的执行过程都是一样的,这个过程被 Go 语言称为 panicking。
当函数f调用panic函数时,函数f的执行将停止,但是f中已进行求值的defer函数会得到正常执行,执行完后会把控制权返还给调用者.
该panicking过程会继续在栈上进行,直到当前goroutine中所有函数都返回为止,最后go程序崩溃退出.

recover 必须被放在一个 defer 函数中才能生效。
如果 recover 捕捉到 panic,它会返回以 panic 的具体内容为错误上下文信息的错误值。
没有 panic 发生,那么 recover 将返回 nil。
如果 panic 被recover 捕捉到,panic 引发的 panicking 过程就会停止。

细节

recover必须在defer声明的匿名函数中才能捕获到异常.

当recover与函数调用方不在同一个协程时,也无法捕获到异常.

当前gorutine中的panic如果被一个defer中的panic覆盖,则在当前gorutine中只能捕获到最后一个panic.

注意多个defer语句中panic的执行顺序,当前协程中有多个defer语句中出现panic时,会先执行defer语句中panic之前的代码,再依次执行panic

多个调用链中捕获panic时,会就近优先被当前协程中的recover所捕获

如何正确使用panic&recover?

是否应该使用recover来避免panic主要取决于程序对panic的忍受度.

8. channel底层数据结构是什么?

channel是用于goroutine之间通信的机制,可以帮助实现并发编程中的数据传递和同步操作.
go语言设计者对并发编程的建议是不要通过共享内存来通信,而是通过通讯来共享内存.

结构: channel底层数据结构是由一个双向链表和一个锁组成的,先进先出.
goroutine向channel发送数据,它会获取锁,将数据添加到链表的末尾,并释放锁。
goroutine从channel接收数据,它也会获取锁,从链表的头部取出数据,并释放锁。

每个channel都有一个缓冲区,存储传递的数据。
goroutine向channel发数据时,数据会被放入缓冲区中,如果缓冲区已满,那么该goroutine会被阻塞,直到有足够的空间可以存储数据为止。
goroutine从channel接收数据时,数据会从缓冲区中取出,如果缓冲区为空,那么该goroutine会被阻塞,直到有数据可供接收为止。

hchan结构体: 是 channel 的头部结构体,它包含了 channel 的元素类型、缓冲区大小、队列头部和尾部指针等信息。

channel缓冲区: 是一个连续的内存空间,用于存储 channel 中的元素。缓冲区的大小由 dataqsiz 字段指定。

等待队列: 是一个包含多个 sudog 结构体的链表,用于存储正在等待发送或接收数据的 goroutine。当有数据可用时,等待队列中的 goroutine 会被唤醒并继续执行。

sudog 结构体: 是channel最核心的数据结构。sudog 代表了一个在等待队列中的 goroutine,它包含了等待的 goroutine 的信息,如等待的 channel、等待的元素值、等待的方向(发送或接收)等。

g 结构体: 是 Golang 中的 goroutine 结构体,这个结构体比较复杂,它包含了 goroutine 的运行状态、栈信息、等待的信号等信息。

type hchan struct {
    //当前队列中元素的个数。当我们向channel发送数据时,qcount会增加1;当我们从channel接收数据时,qcount会减少1
    qcount   uint
    
    //如果我们在创建channel时指定了缓冲区的大小,那么dataqsiz就等于指定的大小;否则,dataqsiz为0,表示该channel没有缓冲区。
    dataqsiz uint
    
    //buf字段是一个unsafe.Pointer类型的指针,指向缓冲区的起始地址。如果该channel没有缓冲区,则buf为nil。
    buf      unsafe.Pointer 
    
   //表示缓冲区中每个元素的大小。当我们创建channel时,Golang会根据元素的类型计算出elemsize的值。
    elemsize uint16
    
    // channel 是否已经关闭,当我们通过close函数关闭一个channel时,Golang会将closed字段设置为true。
    closed   uint32        
    
    //表示下一次接收元素的位置.当我们从channel接收数据时,Golang会从缓冲区中recvx索引的位置读取数据,并将recvx加1
    recvx    uint           
    
     //表示下一次发送元素的位置。在channel的发送操作中,如果缓冲区未满,则会将数据写入到sendx指向的位置,并将sendx加1。如果缓冲区已满,则发送操作会被阻塞,直到有足够的空间可用。
    sendx    uint          
    
     // 等待接收数据的 goroutine 队列,用于存储等待从channel中读取数据的goroutine。当channel中没有数据可读时,接收者goroutine会进入recvq等待队列中等待数据的到来。当发送者goroutine写入数据后,会将recvq等待队列中的接收者goroutine唤醒,并进行读取操作。在进行读取操作时,会先检查recvq等待队列是否为空,如果不为空,则会将队列中的第一个goroutine唤醒进行读取操作。同时,由于recvq等待队列是一个FIFO队列,因此等待时间最长的goroutine会排在队列的最前面,最先被唤醒进行读取操作。
    recvq    waitq         
    
    // 等待发送数据的 goroutine 队列。sendq 字段是一个指向 waitq 结构体的指针,waitq 是一个用于等待队列的结构体。waitq 中包含了一个指向等待队列中第一个协程的指针和一个指向等待队列中最后一个协程的指针。当一个协程向一个 channel 中发送数据时,如果该 channel 中没有足够的缓冲区来存储数据,那么发送操作将会被阻塞,直到有另一个协程来接收数据或者 channel 中有足够的缓冲区来存储数据。当一个协程被阻塞在发送操作时,它将会被加入到 sendq 队列中,等待另一个协程来接收数据或者 channel 中有足够的缓冲区来存储数据。
    sendq    waitq          
    
    //channel的读写锁,确保多个gorutine同时访问时的并发安全,保证读写操作的原子性和互斥性。当一个goroutine想要对channel进行读写操作时,首先需要获取lock锁。如果当前lock锁已经被其他goroutine占用,则该goroutine会被阻塞,直到lock锁被释放。一旦该goroutine获取到lock锁,就可以进行读写操作,并且在操作完成后释放lock锁,以便其他goroutine可以访问channel底层数据结构。
    lock     mutex 
}

9. 有缓冲的channel和无缓冲的channel有何区别?

channel按读写分类,单向只读,单向只写,双向读写,
按有无缓冲分类:每个channel有个容量属性,容量为0的称为非缓冲通道,容量不为0的称为缓冲通道.
channel类型零值可以用nil表示,非零值必须通过make函数创建.

带缓冲区的 channel,定义了缓冲区大小,可以存储多个数据;
不带缓冲区的 channel,只能存一个数据,并且只有当该数据被取出才能存下一个数据.

无缓冲的channel也叫同步channel,需要两个goroutine同时操作,发送和接收是阻塞的.

有缓冲的channel也叫异步channel,特点是发送和接收操作都是非阻塞的.

死锁场景: 只发不收,只收不发

10. nil的channel发送和接收数据会发生什么?

什么情况下channel的值为nil?
未初始化的channel是nil的,channel的零值是nil.
而关闭后的channel并不为nil.

向nil的channel发送和读取数据会因为阻塞导致死锁导致panic.

nil的channel不能执行关闭,也会导致panic.

11. 向关闭的channel发送和接收数据会发生什么?

发送会panic,
读取是允许的,如果channel类型为int,可以一直读取0出来,也就是读取类型的默认零值.

.

怎么判断一个channel是否关闭?

读取channel时,若已经关闭,会返回一个零值和标识,可以通过标识判断是否关闭.

func f6() {
	ch := make(chan int)
	go func() {
		for i := 0; i < 5; i++ {
			ch <- i
		}
		close(ch)
	}()

	for {
		select {
		case x, ok := <-ch:
			if !ok {
				fmt.Println("channel closed")
				return
			}
			fmt.Println(x)
		}
	}
}

细节

多次关闭channel会触发运行时错误,应该使用defer延迟关闭channel,保证只释放一次.

不能使用channel传输大的数据,channel最大能传输的尺寸为64kb,否则会出错.

注意通道和协程的垃圾回收

一个channel会被发送协程和接收协程引用着,如果channel的发送或接收队列不为空则channel不会被垃圾回收.
对协程而言,如果处于channel的某个协程队列里,也不会被垃圾回收,只有当协程退出后才能被垃圾回收.

12. 如何通过interface实现鸭子类型?

鸭子类型: 关注对象的行为,而非对象的类型,如果某个对象具有与鸭子相似的行为,就可以被看做是个鸭子.

go允许面向对象编程,通过使用接口实现鸭子类型.

只要一个对象实现了接口中的所有方法,那么它就可以被看作是实现了该接口。

13. go支持重载吗?如何实现重写?

重载是指方法名相同,参数列表不同,go不支持重载.

重写是指子类重写父类方法,go里继承是通过结构体内嵌,重写的优点是实现多态,提高代码可扩展性和可维护性.

package main

import "fmt"

type Animal struct {
}

func (a *Animal) eat() {
	fmt.Println("Animal is eating")
}

// Cat继承Animal
type Cat struct {
	Animal
}

// Cat子类也可以有eat方法,且实现可以跟父类Animal不同
func (c *Cat) eat() {
	fmt.Println("Cat is eating fish")
}

func main() {
	a := &Animal{}
	c := &Cat{}
	a.eat()
	c.eat()
}

14. go如何实现继承?

通过结构体内嵌实现继承的效果,实现代码的复用和扩展.一个结构体允许内嵌多个结构体.

15. go中如何实现多态?

多态通过接口实现,接口定义了一组方法,任何实现了该接口的类型都可以被认为是这个接口类型.


http://www.kler.cn/a/524258.html

相关文章:

  • 生成模型:扩散模型(DDPM, DDIM, 条件生成)
  • A7. Jenkins Pipeline自动化构建过程,可灵活配置多项目、多模块服务实战
  • 软考信安27~Windows操作系统安全相关
  • ARM64平台Flutter环境搭建
  • 智能调度体系与自动驾驶技术优化运输配送效率的研究——兼论开源AI智能名片2+1链动模式S2B2C商城小程序的应用潜力
  • 【深度之眼cs231n第七期】笔记(三十一)
  • 常见端口的攻击思路
  • 爱书爱考平台说明
  • C#操作GIF图片(上)
  • python+playwright自动化测试(八):iframe切换、多窗口切换
  • Go Fx 框架使用指南:深入理解 Provide 和 Invoke 的区别
  • 单片机基础模块学习——AT24C02芯片
  • open-webui本地AI人工智能问答知识库搭建
  • 云计算与虚拟化技术讲解视频分享
  • 【赵渝强老师】K8s中Pod探针的ExecAction
  • Java基础知识总结(二十四)--Collections
  • 想品客老师的第五天:Map与WeakMap类型
  • 文本左右对齐
  • Linux shell脚本笔记-One
  • Promise.race
  • 在win11下搭建ios开发环境
  • javaweb复习总结
  • 算法随笔_27:最大宽度坡
  • AI学习(vscode+cline+deepseek)
  • 【MQ】如何保证消息队列的高性能?
  • DeepSeek LLM解读