Golang context包必备

目录

应用场景

Context原理

遵循规则

Context包

应用场景

rpc调用

超时处理

API之间的数据传输

 

 



应用场景

在Go http包中的Server中,每个请求会有一个对应的goroutine里面处理,请求处理函数通常会启动额外的goroutine去访问后端服务,比如RPC服务和数据库,用来处理一个请求的goroutine通常需要访问一些与请求特定的数据,如,终端用户身份认证信息,验证token,请求截至时间.当一个请求被取消或超时,所有用来处理该请求的goroutine都应该快速退出。然后系统才能释放这些goroutine占用的资源。

Context原理

Context的调用是链式的,通过WithCancel,WithTimeout,WithDeadline或WithValue派送出新的Context,当父Context被取消时,其他派生的所有Context都会被取消。

通过context.WithXXX都能返回新的context和CancelFunc.调用CancelFunc将取消子代,移除父代对子代的引用,并停止所有定时器。未能调用CancelFunc将泄漏子代,直到父代被取消或定时器触发。go vet工具检查所有流程控制路径上使用的CancelFunc。

遵循规则

遵循以下规则,以保持包之间的接口一致。并启动静态分析工具检查上下文传播。

1.不要将Context放入结构体,相反context应该作为第一个参数传递,命名ctx,func DoSomething(ctx context.Context,arg Arg)error { // ... use ctx ... }

2.即使函数允许,不要传nil的Context.如果不知道用哪种Context。可以用context.TODO().

3.使用context的Value相关方法应该用于在程序和接口中传递或请求相关的元数据,不要用它来传递一些可选参数。

4.相同的Context可以传递在不同的goroutine。Context是并发安全的。

Context包

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

Done(),返回一个channel。当time out或调用cancel方法时。将close掉。

Err(),返回context被取消的原因。

Dealine.返回截止时间和Ok.

Value(),返回值

/*
    TODO返回一个非空,空的上下文
    在目前还不清楚要使用的上下文或尚不可用时
*/
context.TODO()
/*
    Background返回一个非空,空的上下文。
    这是没有取消,没有值,并且没有期限。
    它通常用于由主功能,初始化和测试,并作为输入的顶层上下文
*/
context.Background()
 

Background()和TODO()都是返回空Context的实例。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key interface{}, val interface{}) Context
 

WithCancel对应的时cancelCtx,其中返回一个cancelCtx,同时返回一个CancelFunc,调用CancelFunc时,关闭对应的ctx.Done,也就是让他的后代goroutinue退出。

WithDeadline和WithTimeout对应的是timeCtx,两者是类似的,WithDeadline是设置具体的deadline时间,到底deadline的时候,后代goroutine退出。而WithTimeout简单粗暴,返回一个WithDeadline(parent,time.Now().Add(timeout))

WithValue返回与键关联的值val的副本。

使用上下文值仅在过度进程和API的请求范围的数据。而不能作为可选参数传给函数

提供的键必须是可比性和应该不是字符串类型或任何其他内置的类型以避免包使用的上下文之间的碰撞。WithValue 用户应该定义自己的键的类型。为了避免分配分配给接口 {} 时,上下文键经常有具体类型结构 {}。另外,导出的上下文关键变量静态类型应该是一个指针或接口。

应用场景

rpc调用

在主goroutine上有4个RPC,RPC2/3/4是并行请求的,我们这里希望在RPC2请求失败之后,直接返回错误,并且让RPC3/4停止继续计算。这个时候,就使用的到Context

package main
 
import (
    "context"
    "sync"
    "github.com/pkg/errors"
)
 
func Rpc(ctx context.Context, url string) error {
    result := make(chan int)
    err := make(chan error)
 
    go func() {
        // 进行RPC调用,并且返回是否成功,成功通过result传递成功信息,错误通过error传递错误信息
        isSuccess := true
        if isSuccess {
            result <- 1
        } else {
            err <- errors.New("some error happen")
        }
    }()
 
    select {
        case <- ctx.Done():
            // 其他RPC调用调用失败
            return ctx.Err()
        case e := <- err:
            // 本RPC调用失败,返回错误信息
            return e
        case <- result:
            // 本RPC调用成功,不返回错误信息
            return nil
    }
}
 
 
func main() {
    ctx, cancel := context.WithCancel(context.Background())
 
    // RPC1调用
    err := Rpc(ctx, "http://rpc_1_url")
    if err != nil {
        return
    }
 
    wg := sync.WaitGroup{}
 
    // RPC2调用
    wg.Add(1)
    go func(){
        defer wg.Done()
        err := Rpc(ctx, "http://rpc_2_url")
        if err != nil {
            cancel()
        }
    }()
 
    // RPC3调用
    wg.Add(1)
    go func(){
        defer wg.Done()
        err := Rpc(ctx, "http://rpc_3_url")
        if err != nil {
            cancel()
        }
    }()
 
    // RPC4调用
    wg.Add(1)
    go func(){
        defer wg.Done()
        err := Rpc(ctx, "http://rpc_4_url")
        if err != nil {
            cancel()
        }
    }()
 
    wg.Wait()
}

当然我这里使用了waitGroup来保证main函数在所有RPC调用完成之后才退出。

在Rpc函数中,第一个参数是一个CancelContext, 这个Context形象的说,就是一个传话筒,在创建CancelContext的时候,返回了一个听声器(ctx)和话筒(cancel函数)。所有的goroutine都拿着这个听声器(ctx),当主goroutine想要告诉所有goroutine要结束的时候,通过cancel函数把结束的信息告诉给所有的goroutine。当然所有的goroutine都需要内置处理这个听声器结束信号的逻辑(ctx->Done())。我们可以看Rpc函数内部,通过一个select来判断ctx的done和当前的rpc调用哪个先结束。

这个waitGroup和其中一个RPC调用就通知所有RPC的逻辑,其实有一个包已经帮我们做好了。errorGroup。具体这个errorGroup包的使用可以看这个包的test例子。

有人可能会担心我们这里的cancel()会被多次调用,context包的cancel调用是幂等的。可以放心多次调用。

我们这里不妨品一下,这里的Rpc函数,实际上我们的这个例子里面是一个“阻塞式”的请求,这个请求如果是使用http.Get或者http.Post来实现,实际上Rpc函数的Goroutine结束了,内部的那个实际的http.Get却没有结束。所以,需要理解下,这里的函数最好是“非阻塞”的,比如是http.Do,然后可以通过某种方式进行中断。比如像这篇文章Cancel http.Request using Context中的这个例子:
 

func HttpRequest(
	ctx context.Context,
	req *http.Request,
	client *http.Client,
	respCh chan []byte,
	errCh chan error,
) {
	req = req.WithContext(ctx)
	tr := &http.Transport{}
	client.Transport = tr

	go func() {
		resp, err := client.Do(req)
		if err != nil {
			log.Println("http.Client.Do failure, err:", err)
			errCh <- err
		}
		if resp != nil {
			defer resp.Body.Close()
			respData, err := ioutil.ReadAll(resp.Body)
			if err != nil {
				log.Println("resp read all failure.err:", err)
				errCh <- err
			}
			respCh <- respData
		} else {
			errCh <- errors.New("http request failure")
		}
	}()
	for {
		select {
		case <-ctx.Done():
			tr.CancelRequest(req)
			errCh <- errors.New("http request Canceled")
			return
		case <-errCh:
			fmt.Println("into cancel request")
			tr.CancelRequest(req)
			return
		}
	}
}

func main() {
	client := &http.Client{}
	req, err := http.NewRequest("GET", "https://127.0.0.1:8080/get/user", nil)
	if err != nil {
		log.Println("http new request failue ,err:", err)
	}
	ctx, _ := context.WithCancel(context.Background())
	respCh := make(chan []byte)
	errCh := make(chan error)
	go func() {
		select {
		case <-errCh:
			fmt.Println("into there")
			//cancel()
		case resp := <-respCh:
			fmt.Println("resp:", string(resp))
		}
	}()
	HttpRequest(ctx, req, client, respCh, errCh)
}

超时处理

官方例子

package main
 
import (
    "context"
    "fmt"
    "time"
)
 
func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()
 
    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err()) // prints "context deadline exceeded"
    }
}

http客户端超时


func HttpClientReqTimeout() {
	req, err := http.NewRequest("GET", "https://www.baidu.com/", nil)
	if err != nil {
		log.Println("http new request timeout")
	}
	ctx, _ := context.WithTimeout(context.Background(), 3*time.Millisecond)
	req.WithContext(ctx)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Println("http request failure ,err:", err)
		return
	}
	defer resp.Body.Close()
	respData, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("err:", err)
	}
	fmt.Println("respData,", string(respData))
}

http服务端超时

package main
 
import (
    "net/http"
    "time"
)
 
func test(w http.ResponseWriter, r *http.Request) {
    time.Sleep(20 * time.Second)
    w.Write([]byte("test"))
}
 
 
func main() {
    http.HandleFunc("/", test)
    timeoutHandler := http.TimeoutHandler(http.DefaultServeMux, 5 * time.Second, "timeout")
    http.ListenAndServe(":8080", timeoutHandler)
}

API之间的数据传输

package main
 
import (
    "net/http"
    "context"
)
 
type FooKey string
 
var UserName = FooKey("user-name")
var UserId = FooKey("user-id")
 
func foo(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ctx := context.WithValue(r.Context(), UserId, "1")
        ctx2 := context.WithValue(ctx, UserName, "yejianfeng")
        next(w, r.WithContext(ctx2))
    }
}
 
func GetUserName(context context.Context) string {
    if ret, ok := context.Value(UserName).(string); ok {
        return ret
    }
    return ""
}
 
func GetUserId(context context.Context) string {
    if ret, ok := context.Value(UserId).(string); ok {
        return ret
    }
    return ""
}
 
func test(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("welcome: "))
    w.Write([]byte(GetUserId(r.Context())))
    w.Write([]byte(" "))
    w.Write([]byte(GetUserName(r.Context())))
}
 
func main() {
    http.Handle("/", foo(test))
    http.ListenAndServe(":8080", nil)
}

 

 

 

 

 

参考文章:

https://studygolang.com/articles/23247?fr=sidebarhttps://studygolang.com/articles/23247?fr=sidebarhttps://blog.csdn.net/qq_42015552/article/details/90207288https://blog.csdn.net/qq_42015552/article/details/90207288

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值