golang context用法詳解

背景

在go服務器中,對於每一個請求的request都是在單獨的goroutine中進行的,處理一個request也可能設計多個goroutine之間的交互, 使用context能夠使開發者方便的在這些goroutine裏傳遞request相關的數據、取消goroutine的signal或截止日期。golang

Context結構

// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}

Done 方法在Context被取消或超時時返回一個close的channel,close的channel能夠做爲廣播通知,告訴給context相關的函數要中止當前工做而後返回。安全

當一個父operation啓動一個goroutine用於子operation,這些子operation不可以取消父operation。下面描述的WithCancel函數提供一種方式能夠取消新建立的Context.服務器

Context能夠安全的被多個goroutine使用。開發者能夠把一個Context傳遞給任意多個goroutine而後cancel這個context的時候就可以通知到全部的goroutine。app

Err方法返回context爲何被取消。函數

Deadline返回context什麼時候會超時。測試

Value返回context相關的數據。this

繼承的Context

BackGround

// Background returns an empty Context. It is never canceled, has no deadline,
// and has no values. Background is typically used in main, init, and tests,
// and as the top-level Context for incoming requests.
func Background() Context

BackGound是全部Context的root,不可以被cancel。spa

WithCancel

WithCancel返回一個繼承的Context,這個Context在父Context的Done被關閉時關閉本身的Done通道,或者在本身被Cancel的時候關閉本身的Done。
WithCancel同時還返回一個取消函數cancel,這個cancel用於取消當前的Context。設計

package main

import (
    "context"
    "log"
    "os"
    "time"
)

var logg *log.Logger

func someHandler() {
    ctx, cancel := context.WithCancel(context.Background())
    go doStuff(ctx)

//10秒後取消doStuff
    time.Sleep(10 * time.Second)
    cancel()

}

//每1秒work一下,同時會判斷ctx是否被取消了,若是是就退出
func doStuff(ctx context.Context) {
    for {
        time.Sleep(1 * time.Second)
        select {
        case <-ctx.Done():
            logg.Printf("done")
            return
        default:
            logg.Printf("work")
        }
    }
}

func main() {
    logg = log.New(os.Stdout, "", log.Ltime)
    someHandler()
    logg.Printf("down")
}

 

返回:code

E:\wdy\goproject>go run context_learn.go
15:06:44 work
15:06:45 work
15:06:46 work
15:06:47 work
15:06:48 work
15:06:49 work
15:06:50 work
15:06:51 work
15:06:52 work
15:06:53 down

 

withDeadline withTimeout

WithTimeout func(parent Context, timeout time.Duration) (Context, CancelFunc)
WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).

WithTimeout 等價於 WithDeadline(parent, time.Now().Add(timeout)).

對上面的樣例代碼進行修改

func timeoutHandler() {
    // ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
    // go doTimeOutStuff(ctx)
    go doStuff(ctx)

    time.Sleep(10 * time.Second)

    cancel()

}

func main() {
    logg = log.New(os.Stdout, "", log.Ltime)
    timeoutHandler()
    logg.Printf("end")
}

返回:

15:59:22 work
15:59:24 work
15:59:25 work
15:59:26 work
15:59:27 done
15:59:31 end

能夠看到doStuff在context超時的時候被取消了,ctx.Done()被關閉。
將context.WithDeadline替換爲context.WithTimeout

func timeoutHandler() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    // ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
    // go doTimeOutStuff(ctx)
    go doStuff(ctx)

    time.Sleep(10 * time.Second)

    cancel()

}
16:02:47 work
16:02:49 work
16:02:50 work
16:02:51 work
16:02:52 done
16:02:56 end

doTimeOutStuff替換doStuff

func doTimeOutStuff(ctx context.Context) {
    for {
        time.Sleep(1 * time.Second)

        if deadline, ok := ctx.Deadline(); ok { //設置了deadl
            logg.Printf("deadline set")
            if time.Now().After(deadline) {
                logg.Printf(ctx.Err().Error())
                return
            }

        }

        select {
        case <-ctx.Done():
            logg.Printf("done")
            return
        default:
            logg.Printf("work")
        }
    }
}

func timeoutHandler() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    // ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
    go doTimeOutStuff(ctx)
    // go doStuff(ctx)

    time.Sleep(10 * time.Second)

    cancel()

}
16:03:55 deadline set
16:03:55 work
16:03:56 deadline set
16:03:56 work
16:03:57 deadline set
16:03:57 work
16:03:58 deadline set
16:03:58 work
16:03:59 deadline set
16:03:59 context deadline exceeded
16:04:04 end
WithTimeout
package main

import (
    "math/rand"
    "time"
    "sync"
    "fmt"
    "context"
)

func main()  {
    rand.Seed(time.Now().Unix())

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

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

    fmt.Println("生成幸運用戶成功")
}
func GenUsers(ctx context.Context,wg *sync.WaitGroup)  { //生成用戶ID
fmt.Println("開始生成幸運用戶")
   users:=make([]int,0)
   guser:for{
        select{
           case <- ctx.Done(): //表明父context發起 取消操做

             fmt.Println(users)
               wg.Done()
             break guser
             return
        default:
            users=append(users,getUserID(1000,100000))
        }
   }

}
func getUserID(min int ,max int) int  {
    return rand.Intn(max-min)+min
}

 

 

context deadline exceeded就是ctx超時的時候ctx.Err的錯誤消息。

搜索測試程序

完整代碼參見官方文檔Go Concurrency Patterns: Context,其中關鍵的地方在於函數httpDo

func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
    // Run the HTTP request in a goroutine and pass the response to f.
    tr := &http.Transport{}
    client := &http.Client{Transport: tr}
    c := make(chan error, 1)
    go func() { c <- f(client.Do(req)) }()
    select {
    case <-ctx.Done():
        tr.CancelRequest(req)
        <-c // Wait for f to return.
        return ctx.Err()
    case err := <-c:
        return err
    }
}

httpDo關鍵的地方在於

 select {
    case <-ctx.Done():
        tr.CancelRequest(req)
        <-c // Wait for f to return.
        return ctx.Err()
    case err := <-c:
        return err
    }

要麼ctx被取消,要麼request請求出錯。

 

httpserver中實現超時

package main

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

func CountData(c chan string) chan string {
    time.Sleep(time.Second*5)
    c<- "統計結果"
    return c
}

type IndexHandler struct {}
func(this *IndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)  {
    if r.URL.Query().Get("count")==""{
        w.Write([]byte("這是首頁"))
    }else {
        ctx,cancel:=context.WithTimeout(r.Context(),time.Second*3)
        defer cancel()
        c:=make(chan string)
        go CountData(c)
        select {
           case <-ctx.Done():
               w.Write([]byte("超時"))
            case ret:=<-c:
                w.Write([]byte(ret))
        }


    }


}

func main()  {
     mux:=http.NewServeMux()
     mux.Handle("/",new(IndexHandler))

     http.ListenAndServe(":8082",mux)
}

 

 

WithValue

func WithValue(parent Context, key interface{}, val interface{}) Context
// NewContext returns a new Context carrying userIP.
func NewContext(ctx context.Context, userIP net.IP) context.Context {
    return context.WithValue(ctx, userIPKey, userIP)
}

// FromContext extracts the user IP address from ctx, if present.
func FromContext(ctx context.Context) (net.IP, bool) {
    // ctx.Value returns nil if ctx has no value for the key;
    // the net.IP type assertion returns ok=false for nil.
    userIP, ok := ctx.Value(userIPKey).(net.IP)
    return userIP, ok
}
相關文章
相關標籤/搜索