Go的defer和Context

defer 延迟执行
Go 中提供 defer 关键字来延迟执行函数,被 defer 延迟执行的函数会在 return 返回前执行,所以一般用来进行资源释放等清理工作、还可以用来恢复。
多个被 defer 的函数会按照先进后出的顺序被调用,类似栈数据结构的使用。
defer func1(){}()
defer func2() 先执行func2 再执行func1
传递给 defer 执行的延迟函数的参数会被立即解析,而非等待到正式执行时才被解析。
A defer 执行的延迟函数的参数会被立即解析
func main() {
var i = 1
defer func(i int) { fmt.Println(i)}(i) // 最后这里输出1
i = 0
fmt.Println(i) //先输出这个i是0
}
B defer的执行时机是return之前
输出结果是 0 1
func main() {
var i = 1
defer func() { fmt.Println(i)}() // 最后这里输出0
i = 0
fmt.Println(i) //先输出这个i是0
}
输出结果是 0 0
C 函数拥有具名返回值,会被初始化成一个局部变量,函数内部可以像使用局部变量一样使用该返回值,如果defer操作该返回值,可能会改变返回结果
func foo() (ret int) {
defer func() {
ret++
}()
return 0
}
返回值是1 执行步骤 ret = 0 ret++ return ret
D 确定的参数值是取地址的情况下
func printArray(array *[3]int) {
for i := range array {
fmt.Println(array[i])
}
}
func deferFuncParameter() {
var aArray = [3]int{1, 2, 3}
defer printArray(&aArray)
//这个参数值是数组的地址 延迟函数执行是在return之前 所以输出 10 2 3
aArray[0] = 10
return
}
E return 不是原子性的,它先把返回值result 的值设置为i,defer语句又把result增加1,最终返回2
func deferFuncReturn() (result int) {
i := 1
defer func() {
result++
}()
return i //2
}
  • defer定义的延迟函数参数在defer语句定义时就已经确定下来了;
  • defer定义顺序与实际执行顺序相反;
  • return不是原子操作,执行过程是:保存返回值(若有)-->执行defer(若有)-->执行ret跳转
  • 申请资源后立即使用defer关闭资源是好习惯
Context 上下文
当需要在多个 goroutine 中传递上下文信息时,可以使用 Context 实现。Context 除了用来传递上下文信息,还可以用于传递终结执行子任务的相关信号,中止多个执行子任务的 goroutine。Context 中提供以下接口:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Deadline 方法,返回 Context 被取消的时间,也就是完成工作的截止日期;
Done,返回一个 channel,这个 channel 会在当前工作完成或者上下文被取消之后关闭,多次调用 Done 方法会返回同一个 channel;
Err 方法,返回 Context 结束的原因,它只会在 Done 返回的 channel 被关闭时才会返回非空的值,如果 Context 被取消,会返回 Canceled 错误;如果 Context 超时,会返回 DeadlineExceeded 错误。
Value 方法,可用于从 Context 中获取传递的键值信息。
在 Web 请求的处理过程中,一个请求可能启动多个 goroutine 协同工作,这些 goroutine 之间可能需要共享请求的信息,且当请求被取消或者执行超时时,该请求对应的所有 goroutine 都需要快速结束,释放资源。Context 就是为了解决上述场景而开发的,我们通过下面一个例子来演示:
package main
import (
"context"
"fmt"
"time"
)
const DB_ADDRESS = "db_address"
const CALCULATE_VALUE = "calculate_value"
func readDB(ctx context.Context, cost time.Duration) {
fmt.Println("db address is", ctx.Value(DB_ADDRESS))
select {
case <- time.After(cost): // 模拟数据库读取
fmt.Println("read data from db")
case <-ctx.Done():
fmt.Println(ctx.Err()) // 任务取消的原因
// 一些清理工作
}
}
func calculate(ctx context.Context, cost time.Duration) {
fmt.Println("calculate value is", ctx.Value(CALCULATE_VALUE))
select {
case <- time.After(cost): // 模拟数据计算
fmt.Println("calculate finish")
case <-ctx.Done():
fmt.Println(ctx.Err()) // 任务取消的原因
// 一些清理工作
}
}
func main() {
ctx := context.Background(); // 创建一个空的上下文
// 添加上下文信息
ctx = context.WithValue(ctx, DB_ADDRESS, "localhost:10086")
ctx = context.WithValue(ctx, CALCULATE_VALUE, 1234)
// 设定子 Context 2s 后执行超时返回
ctx, cancel := context.WithTimeout(ctx, time.Second * 2)
defer cancel()
// 设定执行时间为 4 s
go readDB(ctx, time.Second * 4)
go calculate(ctx, time.Second * 4)
// 充分执行
time.Sleep(time.Second * 5)
}
在上述例子中,我模拟了一个请求中同时进行数据库访问和逻辑计算的操作,在请求执行超时时,及时关闭尚未执行结束 goroutine。
我们首先通过 context.WithValue 方法为 context 添加上下文信息,Context 在多个 goroutine 中是并发安全的,可以安全地在多个 goroutine 中对 Context 中的上下文数据进行读取。接着使用 context.WithTimeout 方法设定了 Context 的超时时间为 2s,并传递给 readDB 和 calculate 两个 goroutine 执行子任务。在 readDB 和 calculate 方法中,使用 select 语句对 Context 的 Done 通道进行监控。由于我们设定了子 Context 将在 2s 之后超时,所以它将在 2s 之后关闭 Done 通道;然而预设的子任务执行时间为 4s,对应的 case 语句尚未返回,执行被取消,进入到清理工作的 case 语句中,结束掉当前的 goroutine 所执行的任务。预期的输出结果如下:
calculate value is 1234
db address is localhost:10086
context deadline exceeded
context deadline exceeded
使用 Context,能够有效地在一组 goroutine 中传递共享值、取消信号、deadline 等信息,及时关闭不需要的 goroutine。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注