Go 上下文 Context 详解

golang 的 context

在Go语言中,context(上下文)是一个标准库包context中定义的类型,用于在不同的 Goroutine(Go协程)之间传递请求范围的数据、取消信号和截止时间等信息。

context.Context类型是一个接口,它定义了一组方法,用于管理和传递请求范围的上下文数据。这些方法包括:

  • context.WithCancel(parent Context) (ctx Context, cancelFunc CancelFunc):创建一个带有取消信号的子上下文。当父上下文被取消时,子上下文也会被取消。
  • context.WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc):创建一个带有截止时间的子上下文。当截止时间到达时,子上下文会被取消。
  • context.WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc):创建一个带有超时时间的子上下文。超过指定的时间后,子上下文会被取消。
  • context.WithValue(parent Context, key interface{}, value interface{}) Context:创建一个带有键值对的子上下文。可以通过这个方法在上下文中传递请求范围的数据。

上下文对象可以通过函数调用链传递给需要访问请求范围数据的各个函数或方法。通过在上下文中传递取消信号,可以在某些情况下优雅地取消正在进行的操作,如停止长时间运行的任务或终止正在进行的网络请求。

使用上下文的一个常见场景是在并发处理中。例如,在处理HTTP请求时,可以在处理程序函数中创建一个上下文对象,并将其传递给执行具体业务逻辑的 Goroutine。在需要取消请求时,可以调用上下文的取消方法,通知相关的 Goroutine 停止处理,并释放相关资源。

下面是一个简单的示例,展示了如何使用上下文进行请求取消:

package main

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

func main() {
	http.HandleFunc("/hello", handleRequest)
	http.ListenAndServe(":8080", nil)
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
	// 创建一个父上下文
	parentCtx := r.Context()

	// 创建带有取消信号的子上下文,设置取消时间为2秒后
	ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
	defer cancel()

	// 模拟一个耗时操作
	go performTask(ctx)

	select {
	case <-ctx.Done():
		// 上下文被取消,请求超时
		fmt.Println("请求超时")
		w.WriteHeader(http.StatusRequestTimeout)
	case <-time.After(1 * time.Second):
		// 假设任务在1秒内完成
		fmt.Println("请求成功")
		w.Write([]byte("Hello, World!"))
	}
}

func performTask(ctx context.Context) {
	// 模拟一个耗时操作,需要2秒
	time.Sleep(2 * time.Second)

	// 检查上下文是否被取消
	if ctx.Err() != nil {
		fmt.Println("任务被取消")
		return
	}

	// 任务完成
	fmt.Println("任务完成")
}

在上述示例中,我们创建了一个HTTP服务器,处理/hello路径的请求。在处理函数中,我们创建了一个父上下文parentCtx,然后使用context.WithTimeout函数创建了一个带有2秒超时时间的子上下文ctx。然后,我们启动一个 Goroutine 来执行模拟的耗时任务performTask。在主函数中的select语句中,我们使用ctx.Done()通道来检测上下文是否被取消。如果超时时间到达,ctx.Done()会被关闭,从而触发相应的处理操作。

总结:context是Go语言中用于在不同 Goroutine 之间传递请求范围数据、取消信号和截止时间等信息的机制。通过使用上下文,可以优雅地取消请求、管理超时和传递数据。上下文对象可以通过函数调用链传递,并在需要时取消相关操作。

Go 上下文 Context

Go 语言中用来设置截止日期、同步信号,传递请求相关值的结构体。

Go 语言 1.7 版本中引入 context.Context 标准库的接口,该接口定义了四个需要实现的方法:

Deadline

返回 context.Context 被取消的时间,也就是完成工作的截止日期;

Done

返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,多次调用 Done 方法会返回同一个 Channel;

Err

返回 context.Context 结束的原因,它只会在 Done 方法对应的 Channel 关闭时返回非空的值;
1 如果 context.Context 被取消,会返回 Canceled 错误;
2 如果 context.Context 超时,会返回 DeadlineExceeded 错误;

Value

从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据;

context 包接口

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

context 包中提供:
context.Background、
context.TODO、
context.WithDeadline、
context.WithValue
函数会返回实现该接口的私有结构体,我们会在后面详细介绍它们的工作原理。

设计原理

在 Goroutine 构成的树形结构中对信号进行同步以减少计算资源的浪费是 context.Context 的最大作用。

Go 服务的每一个请求都是通过单独的 Goroutine 处理的,HTTP/RPC 请求的处理器会启动新的 Goroutine 访问数据库和其他服务。

我们可能会创建多个 Goroutine 来处理一次请求,而 context.Context 的作用是在不同 Goroutine 之间同步请求特定数据、取消信号以及处理请求的截止日期。

在这里插入图片描述

Context 与 Goroutine 树

每一个 context.Context 都会从最顶层的 Goroutine 一层一层传递到最下层。
context.Context 可以在上层 Goroutine 执行出现错误时,将信号及时同步给下层。

在这里插入图片描述

不使用 Context 同步信号

当最上层的 Goroutine 因为某些原因执行失败时,下层的 Goroutine 由于没有接收到这个信号所以会继续工作;

但是当我们正确地使用 context.Context 时,就可以在下层及时停掉无用的工作以减少额外资源的消耗:

在这里插入图片描述

使用 Context 同步信号

可以通过一个代码片段了解 context.Context 是如何对信号进行同步的。

在这段代码中,我们创建了一个过期时间为 1s 的上下文,并向上下文传入 handle 函数,该方法会使用 500ms 的时间处理传入的请求:

package main

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

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

	go handle(ctx, 500*time.Millisecond)
	select {
	case <-ctx.Done():
		fmt.Println("main", ctx.Err())
	}
}

func handle(ctx context.Context, duration time.Duration) {
	select {
	case <-ctx.Done():
		fmt.Println("handle", ctx.Err())
	case <-time.After(duration):
		fmt.Println("process request with", duration)
	}
}

因为过期时间大于处理时间,
所以我们有足够的时间处理该请求,
运行上述代码会打印出下面的内容:

$ go run context.go
process request with 500ms
main context deadline exceeded

handle 函数没有进入超时的 select 分支,但是 main 函数的 select 却会等待 context.Context 超时并打印出 main context deadline exceeded。

如果我们将处理请求时间增加至 1500ms,整个程序都会因为上下文的过期而被中止:

go handle(ctx, 1500*time.Millisecond)

$ go run context.go
main context deadline exceeded
handle context deadline exceeded

相信这两个例子能够帮助各位读者理解 context.Context 的使用方法和设计原理 — 多个 Goroutine 同时订阅 ctx.Done() 管道中的消息,一旦接收到取消信号就立刻停止当前正在执行的工作。

默认上下文

context 包中最常用的方法还是 context.Background、context.TODO,
这两个方法都会返回预先初始化好的私有变量 background 和 todo,它们会在同一个 Go 程序中被复用:

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}

这两个私有变量都是通过 new(emptyCtx) 语句初始化的,
它们是指向私有结构体 context.emptyCtx 的指针,
这是最简单、最常用的上下文类型:

type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
	return
}

func (*emptyCtx) Done() <-chan struct{} {
	return nil
}

func (*emptyCtx) Err() error {
	return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
	return nil
}

从上述代码中,我们不难发现 context.emptyCtx 通过空方法实现了 context.Context 接口中的所有方法,它没有任何功能。
在这里插入图片描述

Context 层级关系

从源代码来看,context.Background 和 context.TODO 也只是互为别名,没有太大的差别,只是在使用和语义上稍有不同:

  • context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生出来;
  • context.TODO 应该仅在不确定应该使用哪种上下文时使用;

在多数情况下,如果当前函数没有上下文作为入参,我们都会使用 context.Background 作为起始的上下文向下传递。

示例

context.WithValue 根据上下文添加和读取价值

package main

import (
	"context"
	"fmt"
)

func main() {
	ctx := context.Background()
	ctx = addValue(ctx)
	readValue(ctx)
}

func addValue(ctx context.Context) context.Context {
	return context.WithValue(ctx, "key", "test-value")
}

func readValue(ctx context.Context) {
	val := ctx.Value("key")
	fmt.Println(val)
}
test-value

HTTP 中间件将 guid 添加到请求上下文的中间件

package main

import (
	"context"
	"log"
	"net/http"

	"github.com/google/uuid"
	"github.com/gorilla/mux"
)

func main() {
	// 创建一个新的Gorilla Mux路由器
	router := mux.NewRouter()

	// 使用guidMiddleware中间件,该中间件会为每个请求生成一个唯一的UUID并将其存储在请求上下文中
	router.Use(guidMiddleware)

	// 定义一个路由处理函数,处理GET请求到"/ishealthy"路径
	router.HandleFunc("/ishealthy", handleIsHealthy).Methods(http.MethodGet)

	// 监听端口8080,并将请求交给Mux路由器处理
	http.ListenAndServe(":8080", router)
}

// handleIsHealthy是处理"/ishealthy"路径的路由处理函数
func handleIsHealthy(w http.ResponseWriter, r *http.Request) {
	// 设置HTTP响应状态码为200 - 健康状态
	w.WriteHeader(http.StatusOK)

	// 从请求上下文中获取之前生成的UUID
	uuid := r.Context().Value("uuid")

	// 在日志中记录响应以及相关UUID
	log.Printf("[%v] Returning 200 - Healthy", uuid)

	// 向客户端发送"Healthy"作为响应主体
	w.Write([]byte("Healthy"))
}

// guidMiddleware是一个中间件函数,用于生成唯一的UUID并将其存储在请求上下文中
func guidMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 生成一个新的UUID
		uuid := uuid.New()

		// 将UUID存储在请求上下文中,以便后续处理函数可以访问它
		r = r.WithContext(context.WithValue(r.Context(), "uuid", uuid))

		// 调用下一个处理程序(通常是路由处理函数)
		next.ServeHTTP(w, r)
	})
}

context.Background()、context.TODO() 生成和使用两者

package main

import (
	"context"
	"fmt"
)

func thingThatTakesContext(ctx context.Context) {
	fmt.Println("hello, world")
}

func main() {
	ctxBackground := context.Background()
	ctxTODO := context.TODO()

	thingThatTakesContext(ctxBackground)
	thingThatTakesContext(ctxTODO)
}
hello, world
hello, world

WithCancel 返回可取消的上下文

生成一个可取消的上下文,并通检查完成通道来检查它是否已被取消Done()

package main

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

func longRunningTask(ctx context.Context) {
	fmt.Println("后台长时间运行任务启动")

	// select {
	// case <-ctx.Done():
	// 	fmt.Println("由于上下文取消,长时间运行的任务被保释")
	// }
	// 更简洁的办法
	<-ctx.Done()
	fmt.Println("由于上下文取消,长时间运行的任务被保释")
}

func main() {
	/*
		当cancelFunc被调用时,这个函数将会退出
		WithCancel返回一个带有Done通道的parent副本。
		当返回的cancel函数被调用或父上下文的Done通道被关闭时,
		以先发生的为准关闭返回上下文的Done通道。
	*/
	ctx, cancelFunc := context.WithCancel(context.Background())
	go longRunningTask(ctx)
	time.Sleep(time.Second)

	fmt.Println("后台长时间运行的任务仍在运行")
	time.Sleep(time.Second)

	fmt.Println("要取消后台任务")
	cancelFunc()

	time.Sleep(time.Second)
	fmt.Println("取消后已经过了一段时间")
}

WithDeadline 返回带有截止日期的上下文

WithDeadline 返回一个带有截止日期调整的父上下文的副本

package main

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

func longRunningTask(ctx context.Context, timeToRun time.Duration) {
	select {
	case <-time.After(timeToRun):
		fmt.Println("在上下文截止日期之前完成")
	case <-ctx.Done():
		fmt.Println("因为上下文截止日期已过,放弃了")
	}
}

const duration = 5 * time.Second

func main() {
	ctx := context.Background()

	// 这将放弃,因为函数的运行时间超过了上下文的截止日期允许的时间。
	ctx1, _ := context.WithDeadline(ctx, time.Now().Add(duration))
	/*
		WithDeadline 返回一个带有截止日期调整的父上下文的副本,
		截止日期不会晚于 `d`。如果父上下文的截止日期已经早于 `d`,
		那么 `WithDeadline(parent, d)` 在语义上等同于 `parent`。
		当截止日期到期、调用返回的取消函数或父上下文的 Done 通道关闭时,
		返回的上下文的 Done 通道也会关闭,以先发生的事件为准。

		取消此上下文会释放与其关联的资源,
		因此代码应该在运行在此上下文中的操作完成后尽快调用 cancel。
	*/
	longRunningTask(ctx1, 10*time.Second)

	// 这将完成,因为函数在上下文的截止日期到来之前完成。
	ctx2, _ := context.WithDeadline(ctx, time.Now().Add(duration))
	longRunningTask(ctx2, 3*time.Second)
}
因为上下文截止日期已过,放弃了
在上下文截止日期之前完成

WithTimeout 超时上下文

package main

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

func longRunningTask(ctx context.Context, timeToRun time.Duration) {
	select {
	case <-time.After(timeToRun):
		fmt.Println("在上下文超时之前完成")
	case <-ctx.Done():
		fmt.Println("因为上下文超时而退出")
	}
}

const timeout = 5 * time.Second

func main() {
	ctx := context.Background()

	// 这将会因为函数运行时间超过上下文允许的时间而退出。
	ctx1, _ := context.WithTimeout(ctx, timeout)
	longRunningTask(ctx1, 10*time.Second)

	// 这将会完成,因为函数在上下文超时之前完成。
	ctx2, _ := context.WithTimeout(ctx, timeout)
	longRunningTask(ctx2, 1*time.Second)
}

context.WithValue 具有值的上下文

package main

import (
	"context"
	"fmt"
)

func tryAnotherKeyType(ctx context.Context, keyToConvert string) {
	type keyType2 string

	k := keyType2(keyToConvert)
	if v := ctx.Value(k); v != nil {
		fmt.Println("找到键类型2的值:", v)
	} else {
		fmt.Println("没有键类型值 2")
	}
}

func main() {
	keyString := "foo"

	type keyType1 string

	k := keyType1(keyString)
	ctx := context.WithValue(context.Background(), k, "bar")

	if v := ctx.Value(k); v != nil {
		fmt.Println("找到键类型的值 1:", v)
	} else {
		fmt.Println("没有键类型值 1")
	}

	tryAnotherKeyType(ctx, keyString)
}
找到键类型的值 1: bar
没有键类型值 2
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
SELinux(Security Enhanced Linux)是一种强制访问控制(MAC)的实现,它可以限制进程对系统资源的访问。在SELinux中,每个进程和文件都有一个安全上下文(Security Context),它是由三部分组成的:user、role和type,分别代表用户、角色和类型。 1. User User代表了进程或文件所属的用户,它是一个字符串,通常是一个数字。在Linux系统中,每个用户都有一个唯一的UID(User ID),SELinux使用UID来标识用户。 2. Role Role代表了进程或文件所属的角色,它也是一个字符串。在SELinux中,角色用于区分不同的进程或文件,以及它们对系统资源的访问权限。例如,Web服务器进程可能有一个webserver角色,而数据库服务器进程可能有一个database角色。 3. Type Type代表了进程或文件的类型,它是一个字符串。在SELinux中,类型用于定义进程或文件可以访问的系统资源。例如,Web服务器进程可能有一个httpd_t类型,而数据库服务器进程可能有一个mysqld_t类型。 SELinux的安全上下文可以通过命令行工具semanage和chcon来设置和修改。例如,可以使用如下命令将一个文件的安全上下文修改为httpd_sys_content_t类型: ``` chcon -t httpd_sys_content_t myfile.html ``` 总之,SELinux的安全上下文提供了一种强制访问控制的机制,它可以限制进程和文件对系统资源的访问,从而提高系统的安全性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知其黑、受其白

喝个咖啡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值