Go 并发编程基础:什么是上下文(下)

简介: 在开发过程中,也有这个上下文(Context)的概念,而且上下文也必不可少,缺少上下文,就不能获取完整的程序信息。那么什么是程序中的上下文呢?简单来说,就是在 API 之间或者函数调用之间,除了业务参数信息之外的额外信息。比如,服务器接收到客户端的 HTTP 请求之后,可以把客户端的 IP 地址和端口、客户端的身份信息、请求接收的时间、Trace ID 等信息放入到上下文中,这个上下文可以在后端的方法调用中传递。

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

此函数从其父级返回派生上下文,返回一个 parent 的副本。


当期限超过或调用取消函数时,该派生上下文将被取消。例如,您可以创建一个在未来某个时间自动取消的上下文,并将其传递给子函数。当该上下文由于截止日期用完而被取消时,所有获得该上下文的函数都会收到通知停止工作并返回。如果 parent 的截止日期已经早于 d,则上下文的 Done 通道已经关闭。


下面是我们正在读取一个大文件的示例,该文件的截止时间为当前时间 2 毫秒。我们将获得 2 毫秒的输出,然后将关闭上下文并退出程序。

package main
import (
    "bufio"
    "context"
    "fmt"
    "log"
    "os"
    "time"
)
func main() {
    // context with deadline after 2 millisecond
    ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2*time.Millisecond))
    defer cancel()
    lineRead := make(chan string)
    var fileName = "sample-file.txt"
    file, err := os.Open(fileName)
    if err != nil {
        log.Fatalf("failed opening file: %s", err)
    }
    scanner := bufio.NewScanner(file)
    scanner.Split(bufio.ScanLines)
    // goroutine to read file line by line and passing to channel to print
    go func() {
        for scanner.Scan() {
            lineRead <- scanner.Text()
        }
        close(lineRead)
        file.Close()
    }()
outer:
    for {
        // printing file line by line until deadline is reached
        select {
        case <-ctx.Done():
            fmt.Println("process stopped. reason: ", ctx.Err())
            break outer
        case line := <-lineRead:
            fmt.Println(line)
        }
    }
}

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

这个函数类似于 context.WithDeadline。不同之处在于它将持续时间作为输入而不是时间对象。此函数返回一个派生上下文,如果调用取消函数或超过超时持续时间,该上下文将被取消。


WithTimeout 的实现是:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    // 当前时间+timeout就是deadline
    return WithDeadline(parent, time.Now().Add(timeout))
}


WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))

package main
import (
    "bufio"
    "context"
    "fmt"
    "log"
    "os"
    "time"
)
func main() {
    // context with deadline after 2 millisecond
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
    defer cancel()
    lineRead := make(chan string)
    var fileName = "sample-file.txt"
    file, err := os.Open(fileName)
    if err != nil {
        log.Fatalf("failed opening file: %s", err)
    }
    scanner := bufio.NewScanner(file)
    scanner.Split(bufio.ScanLines)
    // goroutine to read file line by line and passing to channel to print
    go func() {
        for scanner.Scan() {
            lineRead <- scanner.Text()
        }
        close(lineRead)
        file.Close()
    }()
outer:
    for {
        // printing file line by line until deadline is reached
        select {
        case <-ctx.Done():
            fmt.Println("process stopped. reason: ", ctx.Err())
            break outer
        case line := <-lineRead:
            fmt.Println(line)
        }
    }
}


如果父上下文的 Done 通道关闭,它最终将关闭所有派生的 Done 通道(所有后代),如:

package main
import (
    "context"
    "fmt"
    "time"
)
func main() {
    c := make(chan string)
    go func() {
        time.Sleep(1 * time.Second)
        c <- "one"
    }()
    ctx1 := context.Context(context.Background())
    ctx2, cancel2 := context.WithTimeout(ctx1, 2*time.Second)
    ctx3, cancel3 := context.WithTimeout(ctx2, 10*time.Second) // derives from ctx2
    ctx4, cancel4 := context.WithTimeout(ctx2, 3*time.Second)  // derives from ctx2
    ctx5, cancel5 := context.WithTimeout(ctx4, 5*time.Second)  // derives from ctx4
    cancel2()
    defer cancel3()
    defer cancel4()
    defer cancel5()
    select {
    case <-ctx3.Done():
        fmt.Println("ctx3 closed! error: ", ctx3.Err())
    case <-ctx4.Done():
        fmt.Println("ctx4 closed! error: ", ctx4.Err())
    case <-ctx5.Done():
        fmt.Println("ctx5 closed! error: ", ctx5.Err())
    case msg := <-c:
        fmt.Println("received", msg)
    }
}


在这里,由于我们在创建其他派生上下文后立即关闭 ctx2,因此所有其他上下文也会立即关闭,随机打印 ctx3、ctx4 和 ctx5 关闭消息。 ctx5 是从 ctx4 派生的,由于 ctx2 关闭的级联效应,它正在关闭。尝试多次运行,您会看到不同的结果。


使用 Background 或 TODO 方法创建的上下文没有取消、值或截止日期。

package main
import (
    "context"
    "fmt"
)
func main() {
    ctx := context.Background()
    _, ok := ctx.Deadline()
    if !ok {
        fmt.Println("no dealine is set")
    }
    done := ctx.Done()
    if done == nil {
        fmt.Println("channel is nil")
    }
}

4.2 Context 使用规范

  • 不要将上下文存储在结构类型中;相反,将 Context 显式传递给需要它的每个函数。 Context 应该是第一个参数,通常命名为 ctx。
func DoSomething(ctx context.Context, arg Arg) error {
    // ... use ctx ...
}


  • 不要传递 nil 上下文,即使函数允许。如果不确定要使用哪个 Context,请传递 context.TODO 或使用 context.Background() 创建一个空的上下文对象。
  • 仅使用上下文传递请求范围的数据。不要传递应该使用函数参数传递的数据。
  • 始终寻找 goroutine 泄漏并有效地使用上下文来避免这种情况。
  • 如果父上下文的 Done 通道关闭,它最终将关闭所有派生的 Done 通道(所有后代)
  • 上下文只是临时做函数之间的上下文传透,不能持久化上下文
  • key 的类型不应该是字符串类型或者其它内建类型,否则容易在包之间使用 Context 时候产生冲突。使用 WithValue 时,key 的类型应该是自己定义的类型。

4.3 Context 使用场景

  • 上下文信息传递 (request-scoped),比如处理 http 请求、在请求处理链路上传递信息;
  • 控制子 goroutine 的运行;
  • 超时控制的方法调用;
  • 可以取消的方法调用。

5 总结

Context 是在 Go 中进行并发编程时最重要的工具之一。上下文的主要作用是在多个 Goroutine 或者模块之间同步取消信号或者截止日期,用于减少对资源的消耗和长时间占用,避免资源浪费。标准库中的 database/sql、os/exec、net、net/http 等包中都使用到了 Context。


相关文章
|
3月前
|
Go 调度 开发者
Go语言中的并发编程:深入理解与实践###
探索Go语言在并发编程中的独特优势,揭秘其高效实现的底层机制。本文通过实例和分析,引导读者从基础到进阶,掌握Goroutines、Channels等核心概念,提升并发处理能力。 ###
|
1月前
|
并行计算 安全 Go
Go语言中的并发编程:掌握goroutines和channels####
本文深入探讨了Go语言中并发编程的核心概念——goroutine和channel。不同于传统的线程模型,Go通过轻量级的goroutine和通信机制channel,实现了高效的并发处理。我们将从基础概念开始,逐步深入到实际应用案例,揭示如何在Go语言中优雅地实现并发控制和数据同步。 ####
|
2月前
|
存储 Go 开发者
Go语言中的并发编程与通道(Channel)的深度探索
本文旨在深入探讨Go语言中并发编程的核心概念和实践,特别是通道(Channel)的使用。通过分析Goroutines和Channels的基本工作原理,我们将了解如何在Go语言中高效地实现并行任务处理。本文不仅介绍了基础语法和用法,还深入讨论了高级特性如缓冲通道、选择性接收以及超时控制等,旨在为读者提供一个全面的并发编程视角。
100 50
|
2月前
|
安全 Serverless Go
Go语言中的并发编程:深入理解与实践####
本文旨在为读者提供一个关于Go语言并发编程的全面指南。我们将从并发的基本概念讲起,逐步深入到Go语言特有的goroutine和channel机制,探讨它们如何简化多线程编程的复杂性。通过实例演示和代码分析,本文将揭示Go语言在处理并发任务时的优势,以及如何在实际项目中高效利用这些特性来提升性能和响应速度。无论你是Go语言的初学者还是有一定经验的开发者,本文都将为你提供有价值的见解和实用的技巧。 ####
|
2月前
|
算法 安全 程序员
Go语言的并发编程:深入理解与实践####
本文旨在探讨Go语言在并发编程方面的独特优势及其实现机制,通过实例解析关键概念如goroutine和channel,帮助开发者更高效地利用Go进行高性能软件开发。不同于传统的摘要概述,本文将以一个简短的故事开头,引出并发编程的重要性,随后详细阐述Go语言如何简化复杂并发任务的处理,最后通过实际案例展示其强大功能。 --- ###
|
2月前
|
Go 调度 开发者
Go语言中的并发编程:深入理解goroutines和channels####
本文旨在探讨Go语言中并发编程的核心概念——goroutines和channels。通过分析它们的工作原理、使用场景以及最佳实践,帮助开发者更好地理解和运用这两种强大的工具来构建高效、可扩展的应用程序。文章还将涵盖一些常见的陷阱和解决方案,以确保在实际应用中能够避免潜在的问题。 ####
|
2月前
|
安全 Go 数据处理
Go语言中的并发编程:掌握goroutine和channel的艺术####
本文深入探讨了Go语言在并发编程领域的核心概念——goroutine与channel。不同于传统的单线程执行模式,Go通过轻量级的goroutine实现了高效的并发处理,而channel作为goroutines之间通信的桥梁,确保了数据传递的安全性与高效性。文章首先简述了goroutine的基本特性及其创建方法,随后详细解析了channel的类型、操作以及它们如何协同工作以构建健壮的并发应用。此外,还介绍了select语句在多路复用中的应用,以及如何利用WaitGroup等待一组goroutine完成。最后,通过一个实际案例展示了如何在Go中设计并实现一个简单的并发程序,旨在帮助读者理解并掌
|
2月前
|
安全 Java Go
Go语言中的并发编程:掌握goroutine与通道的艺术####
本文深入探讨了Go语言中的核心特性——并发编程,通过实例解析goroutine和通道的高效使用技巧,旨在帮助开发者提升多线程程序的性能与可靠性。 ####
|
2月前
|
Go 开发者
Go语言中的并发编程:掌握goroutines和channels####
本文深入探讨了Go语言中并发编程的核心概念,重点介绍了goroutines和channels的工作原理及其在实际开发中的应用。文章通过实例演示如何有效地利用这些工具来编写高效、可维护的并发程序,旨在帮助读者理解并掌握Go语言在处理并发任务时的强大能力。 ####
|
2月前
|
存储 安全 Go
Go 语言以其高效的并发编程能力著称,主要依赖于 goroutines 和 channels 两大核心机制
Go 语言以其高效的并发编程能力著称,主要依赖于 goroutines 和 channels 两大核心机制。本文介绍了这两者的概念、用法及如何结合使用,实现任务的高效并发执行与数据的安全传递,强调了并发编程中的注意事项,旨在帮助开发者更好地掌握 Go 语言的并发编程技巧。
40 2