go context 用法

314 阅读2分钟

context接口定义

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

context特性描述

1、进入的请求应该创建一个上下文,输出应该接收一个上下文
2、功能调用链应该传播上下文
3、调用CancelFunc,
    会取消子上下文和它的派生上下文,
    删除父对子的引用
    停止相关的定时器
3、调用 CancelFunc 失败会泄漏孩子及其孩子,直到父母被取消或计时器触发
4go vet 可以检查所有的控制流路径是否使用了CancelFunc

./c.go:85:6: the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak

5、不要将上下文存储在结构类型中,而是将上下文显式传递给需要它的每个函数
6、上下文应该为第一个参数,通常命名为ctx
7、不要传递 nil 上下文,即使函数允许传递 context.TODO 如果您不确定要使用哪个上下文。

context.TODO()
8、仅将上下文值用于传输流程和API边界的请求范围数据,而不用于将可选参数传递给函数。
9、同一个上下文在多个不同的协程中使用是合理的,而且是安全的

context接口方法说明

Deadline() (deadline time.Time, ok bool)

返回上下文应该被取消的时间

c := context.Background()
fmt.Println(time.Now())
c, cancel := context.WithTimeout(c, time.Second*2)
t, ok := c.Deadline()
fmt.Println(t, ok)
fmt.Println(<-c.Done()) // 2s到之前,一直被阻塞
fmt.Println(time.Now())

Output:
2022-06-08 08:28:38.617696 +0800 CST m=+0.000180371
2022-06-08 08:28:40.617885 +0800 CST m=+2.000369547 true
2022-06-08 08:28:40.618983 +0800 CST m=+2.001474187

Done() <-chan struct{}

返回一个被关闭的通道,代表上下文被取消了 如果上下文永远不会被取消,返回 nil

c := context.Background()
fmt.Println(c.Done()) // <nil>

Err() error

Done未关闭,返回nil Done已关闭,返回错误原因

方式1:
c := context.Background()
c, cancel := context.WithTimeout(c, time.Second*2)
fmt.Println(c.Err()) // nil
con(cancel)
fmt.Println(c.Err()) // context canceled
fmt.Println(<-c.Done()) // {}
fmt.Println(c.Err()) // context canceled

方式2:
c := context.Background()
c, cancel := context.WithTimeout(c, time.Second*2)
// c, cancel := context.WithDeadline(c, time.Now().Add(time.Second*2)) // 和WithTimeout效果相同
defer cancel()
fmt.Println(<-c.Done()) // {}
fmt.Println(c.Err()) // context deadline exceeded

func con(ctx context.CancelFunc) {
	defer func() {
		ctx()
	}()
}

Value(key any) any

仅将上下文值用于传输流程和API边界的请求范围数据,而不用于将可选参数传递给函数。

key标识上下文中的特定值。 希望在 Context 中存储值的函数通常会在全局变量中分配一个键,然后将该键用作 context 的参数。 WithValue 和 Context.Value 。 键可以是任何支持相等的类型; 包应将键定义为未导出的类型以避免冲突

定义 Context 键的包应该为使用该键存储的值提供类型安全的访问器

package user

import "context"

// User 是存储在上下文中的值的类型。
type User struct {...}

// key 是此包中定义的键的未导出类型。 这可以防止与其他包中定义的键发生冲突。
type key int

// userKey 是 Contexts 中 user.User 值的键。 未导出; 客户端使用 user.NewContext 和
// user.FromContext 而不是直接使用这个键。
var userKey key

// NewContext 返回一个带有值 u 的新 Context
func NewContext(ctx context.Context, u *User) context.Context {
    return context.WithValue(ctx, userKey, u)
}

// FromContext 返回存储在 ctx 中的用户值(如果有)
func FromContext(ctx context.Context) (*User, bool) {
    u, ok := ctx.Value(userKey).(*User)
    return u, ok
}

context使用最佳实践 (官方userip的例子)并发安全的上下文在多个包中传递的最佳实践方式

package google

import (
   "context"
   "ctx/userip"
   "encoding/json"
   "errors"
   "fmt"
   "net/http"
   "strconv"
)

type Results []Result

type Result struct {
   Title, URL string
}

func Search(ctx context.Context, query string) (Results, error) {
   req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil)
   if err != nil {
      return nil, err
   }
   q := req.URL.Query()
   q.Set("q", query)

   if userIP, ok := userip.FromContext(ctx); ok {
      q.Set("userip", userIP.String())
   }
   req.URL.RawQuery = q.Encode()

   var results Results
   err = httpDo(ctx, req, func(resp *http.Response, err error) error {
      if err != nil {
         return err
      }
      defer resp.Body.Close()

      var data struct {
         ResponseData struct {
            Results []struct {
               TitleNoFormatting, URL string
            }
         }
      }

      if 200 != resp.StatusCode {
         return errors.New("Deal Fail, HttpCode:" + strconv.FormatInt(int64(resp.StatusCode), 10))
      }

      if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
         fmt.Println("some error:", err.Error())
         return err
      }
      for _, res := range data.ResponseData.Results {
         results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL})
      }
      return nil
   })
   return results, err
}

func httpDo(ctx context.Context, r *http.Request, f func(*http.Response, error) error) error {
   c := make(chan error, 1)
   r = r.WithContext(ctx)
   go func() {
      c <- f(http.DefaultClient.Do(r))
   }()
   select {
   case <-ctx.Done():
      <-c
      return ctx.Err()
   case err := <-c:
      return err
   }
}
package userip

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

func FromRequest(req *http.Request) (net.IP, error) {
   ip, _, err := net.SplitHostPort(req.RemoteAddr)
   if err != nil {
      return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
   }

   userIP := net.ParseIP(ip)
   if userIP == nil {
      return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
   }
   return userIP, nil
}

// 不导出,防止和其他包的类型名冲突
type key int

const userIPKey key = 0

// 包向context中添加值的官方推荐方式,不是操作上下文,而是派生新的上下文
func NewContext(ctx context.Context, UserIP net.IP) context.Context {
   return context.WithValue(ctx, userIPKey, UserIP)
}

// 包从context中查询值的推荐方式,不是直接操作,而是封装方法来获取
func FromContext(ctx context.Context) (net.IP, bool) {
   userIP, ok := ctx.Value(userIPKey).(net.IP)
   return userIP, ok
}
package main

import (
   "context"
   "ctx/google"
   "ctx/userip"
   "html/template"
   "log"
   "net/http"
   "time"
)

var resultsTemplate = template.Must(template.New("results").Parse(`
<html>
<head/>
<body>
  <ol>
  {{range .Results}}
    <li>{{.Title}} - <a href="{{.URL}}">{{.URL}}</a></li>
  {{end}}
  </ol>
  <p>{{len .Results}} results in {{.Elapsed}}; timeout {{.Timeout}}</p>
</body>
</html>
`))

func main() {
   http.HandleFunc("/search", handleSearch)
   log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}

func handleSearch(w http.ResponseWriter, req *http.Request) {
   var (
      ctx    context.Context
      cancel context.CancelFunc
   )
   timeout, err := time.ParseDuration(req.FormValue("timeout"))
   if err == nil {
      ctx, cancel = context.WithTimeout(context.Background(), timeout)
   } else {
      ctx, cancel = context.WithCancel(context.Background())
   }
   defer cancel()

   query := req.FormValue("q")
   if query == "" {
      http.Error(w, "no query", http.StatusBadRequest)
      return
   }

   userIP, err := userip.FromRequest(req)
   if err != nil {
      http.Error(w, err.Error(), http.StatusBadRequest)
      return
   }
   ctx = userip.NewContext(ctx, userIP)

   start := time.Now()
   results, err := google.Search(ctx, query)
   elapsed := time.Since(start)
   if err != nil {
      http.Error(w, err.Error(), http.StatusInternalServerError)
      return
   }
   if err := resultsTemplate.Execute(w, struct {
      Results          google.Results
      Timeout, Elapsed time.Duration
   }{
      Results: results,
      Timeout: timeout,
      Elapsed: elapsed,
   }); err != nil {
      log.Print(err)
      return
   }
}