MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Go context API函数的灵活运用

2024-06-167.2k 阅读

Go context API 基础概念

在 Go 语言的并发编程中,context 包提供了一种强大的机制,用于在多个 goroutine 之间传递截止日期、取消信号和其他请求范围的值。这在处理复杂的并发任务时至关重要,因为它允许我们更优雅地管理资源和控制 goroutine 的生命周期。

context.Context 是一个接口,定义了四个方法:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
  • Deadline 方法:返回 context 的截止时间。oktrue 时,表示设置了截止时间,deadline 为截止时间点。这个方法常用于控制操作的最长执行时间。
func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    deadline, ok := ctx.Deadline()
    if ok {
        fmt.Printf("截止时间: %v\n", deadline)
    }
}

在上述代码中,context.WithTimeout 创建了一个带有超时的 context,通过 Deadline 方法可以获取到设置的截止时间。

  • Done 方法:返回一个只读的 channel,当 context 被取消或者超时时,这个 channel 会被关闭。在 goroutine 中监听这个 channel 可以得知是否需要提前结束任务。
func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("任务被取消")
            return
        default:
            fmt.Println("正在工作...")
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go worker(ctx)

    time.Sleep(2 * time.Second)
    cancel()
    time.Sleep(1 * time.Second)
}

这里 worker 函数通过 select 监听 ctx.Done(),当 cancel 函数被调用时,ctx.Done() 通道关闭,worker 函数可以感知到并结束工作。

  • Err 方法:返回 context 被取消或超时的原因。如果 Done 通道未关闭,Err 会返回 nil。当 Done 通道关闭后,Err 会返回具体的错误原因,比如 context.Canceledcontext.DeadlineExceeded
func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    select {
    case <-time.After(2 * time.Second):
        fmt.Println("操作超时,但未通过 context 感知")
    case <-ctx.Done():
        err := ctx.Err()
        if err == context.DeadlineExceeded {
            fmt.Println("操作超时,通过 context 感知")
        }
    }
}

上述代码展示了如何通过 Err 方法判断操作超时是由于 context 设置的截止时间到了。

  • Value 方法:用于在 context 中传递特定的值。这个值通常是与请求相关的数据,比如用户认证信息等。key 必须是可比较的类型,并且建议使用 context.key 类型来避免命名冲突。
type userKey struct{}

func main() {
    ctx := context.WithValue(context.Background(), userKey{}, "John Doe")

    value := ctx.Value(userKey{})
    if user, ok := value.(string); ok {
        fmt.Printf("用户: %s\n", user)
    }
}

这里定义了一个 userKey 类型作为 context.Value 的键,通过 context.WithValue 设置值,并通过 ctx.Value 获取值。

常用的 context 创建函数

  1. context.Background:这是所有 context 的根,通常作为其他 context 创建的基础。它永不取消,没有截止时间,也没有值。在程序的主函数或者初始化阶段,常以 context.Background 作为起点创建其他 context。
func main() {
    ctx := context.Background()
    // 以 ctx 为基础创建其他 context
    ctxWithValue := context.WithValue(ctx, "key", "value")
}
  1. context.TODO:当你不确定当前应该使用什么 context 时,可以暂时使用 context.TODO。它也是永不取消,没有截止时间,没有值的。但是,使用 context.TODO 应该是临时的,最终要替换为合适的 context。
func someFunction() {
    ctx := context.TODO()
    // 后续需要替换为合适的 context
}
  1. context.WithCancel:创建一个可取消的 context。返回的 cancel 函数用于取消这个 context,当 cancel 函数被调用时,所有基于这个 context 创建的子 context 都会被取消。
func main() {
    ctx, cancel := context.WithCancel(context.Background())

    go func(ctx context.Context) {
        for {
            select {
            case <-ctx.Done():
                fmt.Println("子 goroutine 被取消")
                return
            default:
                fmt.Println("子 goroutine 正在运行")
                time.Sleep(500 * time.Millisecond)
            }
        }
    }(ctx)

    time.Sleep(2 * time.Second)
    cancel()
    time.Sleep(1 * time.Second)
}

在这个例子中,主 goroutine 创建了一个可取消的 context,并启动了一个子 goroutine。2 秒后,主 goroutine 调用 cancel 函数,子 goroutine 可以感知到并结束运行。

  1. context.WithTimeout:创建一个带有超时的 context。timeout 参数指定了超时时间,当超过这个时间后,context 会自动取消。这在控制操作的最长执行时间时非常有用,比如网络请求、数据库查询等。
func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go func(ctx context.Context) {
        select {
        case <-ctx.Done():
            fmt.Println("操作超时,被取消")
            return
        case <-time.After(5 * time.Second):
            fmt.Println("操作正常结束(模拟)")
        }
    }(ctx)

    time.Sleep(4 * time.Second)
}

这里创建了一个 3 秒超时的 context,子 goroutine 模拟一个操作。由于操作时间超过了 3 秒,context 会自动取消,子 goroutine 感知到并打印超时信息。

  1. context.WithDeadline:与 context.WithTimeout 类似,不过它是基于绝对时间来设置截止时间。deadline 参数是一个 time.Time 类型,表示截止时间点。
func main() {
    deadline := time.Now().Add(2 * time.Second)
    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()

    go func(ctx context.Context) {
        select {
        case <-ctx.Done():
            fmt.Println("操作因截止时间到被取消")
            return
        case <-time.After(3 * time.Second):
            fmt.Println("操作正常结束(模拟)")
        }
    }(ctx)

    time.Sleep(3 * time.Second)
}

在这个例子中,通过 time.Now().Add(2 * time.Second) 计算出截止时间,创建了一个基于此截止时间的 context。子 goroutine 同样模拟一个操作,由于超过了截止时间,context 取消,子 goroutine 感知并打印相应信息。

context 在函数调用链中的传递

在实际应用中,context 通常会在函数调用链中传递,以便各级函数都能感知到取消信号或截止时间等信息。

func innerFunction(ctx context.Context) {
    select {
    case <-ctx.Done():
        fmt.Println("innerFunction 被取消")
        return
    default:
        fmt.Println("innerFunction 正在执行")
        time.Sleep(1 * time.Second)
    }
}

func middleFunction(ctx context.Context) {
    innerFunction(ctx)
}

func outerFunction(ctx context.Context) {
    middleFunction(ctx)
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go outerFunction(ctx)

    time.Sleep(2 * time.Second)
    cancel()
    time.Sleep(1 * time.Second)
}

在这个代码示例中,outerFunction 调用 middleFunctionmiddleFunction 又调用 innerFunction。通过将 context 一路传递下去,当 cancel 函数在主 goroutine 中被调用时,innerFunction 可以感知到并做出相应处理。

context 与 HTTP 服务器

在 HTTP 服务器编程中,context 发挥着重要作用。Go 的 net/http 包在处理请求时,会为每个请求创建一个 context。这个 context 可以传递给处理请求的各个函数,用于控制请求的生命周期。

package main

import (
    "context"
    "fmt"
    "net/http"
    "time"
)

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    select {
    case <-ctx.Done():
        err := ctx.Err()
        fmt.Fprintf(w, "请求被取消: %v\n", err)
        return
    case <-time.After(5 * time.Second):
        fmt.Fprintf(w, "请求处理完成\n")
    }
}

func main() {
    http.HandleFunc("/", handler)
    server := &http.Server{
        Addr:    ":8080",
        Handler: nil,
    }

    go func() {
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            fmt.Printf("服务器启动错误: %v\n", err)
        }
    }()

    time.Sleep(3 * time.Second)

    ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        fmt.Printf("服务器关闭错误: %v\n", err)
    }
}

在上述代码中,handler 函数通过 r.Context() 获取请求的 context。当服务器接收到请求时,handler 函数会模拟一个 5 秒的处理过程。在主函数中,3 秒后尝试关闭服务器,并设置了 2 秒的超时。如果在 2 秒内服务器没有正常关闭,会打印关闭错误。同时,如果在处理请求过程中服务器被关闭,handler 函数可以通过 context 感知到并做出相应处理。

context 与数据库操作

在数据库操作中,context 也非常有用。例如,在执行一个数据库查询时,可以使用 context 来设置超时时间,避免长时间等待数据库响应。

package main

import (
    "context"
    "fmt"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "time"
)

type User struct {
    ID   uint
    Name string
}

func main() {
    dsn := "user:password@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("数据库连接错误: " + err.Error())
    }

    ctx, cancel := context.WithTimeout(context.Background(), 3 * time.Second)
    defer cancel()

    var user User
    err = db.WithContext(ctx).First(&user).Error
    if err != nil {
        if err == context.DeadlineExceeded {
            fmt.Println("数据库查询超时")
        } else {
            fmt.Printf("数据库查询错误: %v\n", err)
        }
        return
    }

    fmt.Printf("查询到的用户: %v\n", user)
}

在这个例子中,使用 gorm 进行数据库查询。通过 db.WithContext(ctx) 将 context 传递给数据库查询操作,设置了 3 秒的超时时间。如果查询时间超过 3 秒,会返回 context.DeadlineExceeded 错误,程序可以根据这个错误做出相应处理。

context 的注意事项

  1. 避免在全局变量中使用 context:context 应该与特定的请求或任务相关联,在全局变量中使用 context 会导致难以理解和维护代码,并且可能会引发竞态条件等问题。
  2. 及时取消 context:在使用完 context 后,尤其是在创建了可取消的 context 时,要及时调用 cancel 函数,以释放资源并通知相关的 goroutine 结束任务。否则可能会导致 goroutine 泄漏。
  3. 小心传递 context:在函数调用链中传递 context 时,要确保每个函数都正确处理 context 的取消信号或截止时间。如果某个函数忽略了 context,可能会导致整个任务无法按预期取消或超时。
  4. 不要在 context 中传递大量数据context.Value 主要用于传递一些与请求相关的小数据,如认证信息等。传递大量数据不仅会增加内存开销,还可能影响性能。

context 的高级应用场景

  1. 分布式系统中的上下文传递:在分布式系统中,一个请求可能会经过多个服务节点。通过在请求中传递 context,可以在整个调用链中传递截止时间、跟踪信息等。例如,使用分布式跟踪系统(如 Jaeger)时,context 可以携带跟踪 ID,以便在各个服务之间关联请求,实现全链路跟踪。
  2. 资源池管理:在管理资源池(如数据库连接池、线程池等)时,context 可以用于控制资源的获取和释放。当 context 被取消时,可以及时归还资源,避免资源泄漏。
  3. 并发任务的依赖管理:在处理多个并发任务时,有些任务可能依赖于其他任务的结果。通过 context,可以在任务之间传递依赖关系和取消信号。例如,任务 A 依赖于任务 B 的结果,当任务 B 因 context 取消而失败时,任务 A 也可以及时感知并取消。

示例:并发任务依赖管理

package main

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

func taskB(ctx context.Context, wg *sync.WaitGroup) {
    defer wg.Done()

    select {
    case <-ctx.Done():
        fmt.Println("taskB 被取消")
        return
    case <-time.After(2 * time.Second):
        fmt.Println("taskB 完成")
    }
}

func taskA(ctx context.Context, wg *sync.WaitGroup) {
    defer wg.Done()

    ctxB, cancel := context.WithCancel(ctx)
    var wgB sync.WaitGroup
    wgB.Add(1)
    go taskB(ctxB, &wgB)

    select {
    case <-ctx.Done():
        cancel()
        fmt.Println("taskA 被取消,同时取消 taskB")
        return
    case <-time.After(3 * time.Second):
        fmt.Println("taskA 等待 taskB 完成后继续执行")
        wgB.Wait()
        fmt.Println("taskA 完成")
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 4 * time.Second)
    defer cancel()

    var wg sync.WaitGroup
    wg.Add(1)
    go taskA(ctx, &wg)

    wg.Wait()
}

在这个示例中,taskA 依赖于 taskB 的结果。taskA 创建了一个子 context ctxB 传递给 taskB。当 ctx 被取消时,taskA 会取消 ctxB,从而也取消 taskB。如果没有取消,taskA 会等待 taskB 完成后再继续执行。

通过深入理解和灵活运用 Go 的 context API,可以编写出更健壮、可维护的并发程序,尤其是在处理复杂的并发任务和分布式系统时。希望通过以上内容,你对 Go context API 的灵活运用有了更深入的认识。