Go標準ライブラリContect


Go標準ライブラリContect
Go httpパッケージのServerでは、それぞれの要求が対応するgoroutineで処理されます。要求処理関数は、通常、データベースやRPCサービスなどのバックエンドサービスにアクセスするために追加のgoroutineを起動する。要求を処理するためのgoroutineは、一般に、端末ユーザのアイデンティティ認証情報、認証に関するtoken、要求の締め切り時間など、特定のデータにアクセスする必要がある。一つの要求がキャンセルされたり、オーバータイムされたりした場合、要求を処理するためのすべてのgoroutineは速やかに終了し、その後、システムはこれらのgoroutineが占有するリソースを解放することができる。
なぜContectが必要ですか?
基本例
package main

import (
	"fmt"
	"sync"

	"time"
)

var wg sync.WaitGroup

//      

func worker() {
     
	for {
     
		fmt.Println("worker")
		time.Sleep(time.Second)
	}
	//             
	wg.Done()
}

func main() {
     
	wg.Add(1)
	go worker()
	//           goroutine
	wg.Wait()
	fmt.Println("over")
}
グローバル変数方式
package main

import (
	"fmt"
	"sync"

	"time"
)

var wg sync.WaitGroup
var exit bool

//            :
// 1.                  
// 2.   worker    goroutine,       。

func worker() {
     
	for {
     
		fmt.Println("worker")
		time.Sleep(time.Second)
		if exit {
     
			break
		}
	}
	wg.Done()
}

func main() {
     
	wg.Add(1)
	go worker()
	time.Sleep(time.Second * 3) // sleep3         
	exit = true                 //          goroutine   
	wg.Wait()
	fmt.Println("over")
}
チャンネルモード
package main

import (
	"fmt"
	"sync"

	"time"
)

var wg sync.WaitGroup

//          :
// 1.                       ,         channel

func worker(exitChan chan struct{
     }) {
     
LOOP:
	for {
     
		fmt.Println("worker")
		time.Sleep(time.Second)
		select {
     
		case exitChan: //         
			break LOOP
		default:
		}
	}
	wg.Done()
}

func main() {
     
	var exitChan = make(chan struct{
     })
	wg.Add(1)
	go worker(exitChan)
	time.Sleep(time.Second * 3) // sleep3         
	exitChan  struct{
     }{
     }      //   goroutine      
	close(exitChan)
	wg.Wait()
	fmt.Println("over")
}
公式版の案
package main

import (
	"context"
	"fmt"
	"sync"

	"time"
)

var wg sync.WaitGroup

func worker(ctx context.Context) {
     
LOOP:
	for {
     
		fmt.Println("worker")
		time.Sleep(time.Second)
		select {
     
		case ctx.Done(): //       
			break LOOP
		default:
		}
	}
	wg.Done()
}

func main() {
     
	ctx, cancel := context.WithCancel(context.Background())
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 3)
	cancel() //    goroutine  
	wg.Wait()
	fmt.Println("over")
}
子goroutineがまた他のgoroutineを開く時、ctxを入れてもいいです。
package main

import (
	"context"
	"fmt"
	"sync"

	"time"
)

var wg sync.WaitGroup

func worker(ctx context.Context) {
     
	go worker2(ctx)
LOOP:
	for {
     
		fmt.Println("worker")
		time.Sleep(time.Second)
		select {
     
		case ctx.Done(): //       
			break LOOP
		default:
		}
	}
	wg.Done()
}

func worker2(ctx context.Context) {
     
LOOP:
	for {
     
		fmt.Println("worker2")
		time.Sleep(time.Second)
		select {
     
		case ctx.Done(): //       
			break LOOP
		default:
		}
	}
}
func main() {
     
	ctx, cancel := context.WithCancel(context.Background())
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 3)
	cancel() //    goroutine  
	wg.Wait()
	fmt.Println("over")
}
Cotext初見
Go 1.7は、単一の要求を処理するための複数のgoroutineと要求ドメインのデータ、キャンセル信号、締切時間などの関連動作を単純化するためのcontextタイプを定義する新しい標準ライブラリContextを追加している。これらの動作は複数のAPI呼び出しに関連する可能性がある。
サーバからの要求はコンテキストを作成し、サーバへの呼び出しはコンテキストを受け入れるべきです。これらの間の関数コールチェーンは、コンテキストを伝達しなければならない、またはWithCancelWithDeadlineWithTimeout、またはWithValueによって作成された派生コンテキストを使用してもよい。コンテキストがキャンセルされると、その派生したすべてのコンテキストもキャンセルされます。
コネクタcontext.Contextは、4つの実装が必要な方法を定義するインターフェースである。具体的な署名は以下の通りです。
type Context interface {
     
    Deadline() (deadline time.Time, ok bool)
    Done() chan struct{
     }
    Err() error
    Value(key interface{
     }) interface{
     }
}
その中:
  • Deadline方法は、現在のContextがキャンセルされた時間、すなわち、仕事の終了時間を返す必要がある。
  • Done方法はChannelに戻る必要があります。このChanelは現在の作業が完了した後、またはコンテキストがキャンセルされた後にオフになります。何度も呼び出します。Done方法は同じChanelに戻ります。
  • ErrErr       Context     ,    Done   Channel            ;
    
  • 現在ContextがキャンセルされたらCanceledエラーに戻ります。
  • 現在ContextがタイムアウトしたらDeadlineExceededエラーに戻ります。
  • Value方法は、Contextからキーに対応する値を返します。同じ文脈では、Valueを何度も呼び出して同じKeyに入ってきたら、同じ結果を返します。
  • Background()とTODO()
    Goは、2つの関数を内蔵する。Background()TODO()と、それぞれContextインターフェースを実装したbackgroundtodoとを返す。私たちのコードの最初は、この2つの内蔵されたコンテキストオブジェクトを最上位のpartent contextとして、より多くのサブコンテキストオブジェクトを生み出しています。Background()は主にmain関数、初期化およびテストコードの中でContectというツリー構造の最上位のContectであり、つまり根Contectである。TODO()、まだ具体的な使用シーンが分かりません。もし私達が何を使うべきか分からないなら、これを使ってもいいです。backgroundおよびtodoは、本質的にはemptyCtx構造体タイプであり、キャンセル不可能であり、締切時間が設定されておらず、いかなる値も持たないContectである。
    Withシリーズ関数
    さらに、contextパケットには4つのWithシリーズ関数が定義されている。
    WithCacelWithCancelの関数署名は以下の通りである。
    func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
    
    WithCancelは、新しいDoneチャネルを有する親ノードのコピーを返す。戻ってきたcancel関数を呼び出したり、親コンテキストのDuneチャネルをオフにすると、コンテキストに戻るDuneチャネルが閉じられます。
    このコンテキストをキャンセルすると、関連するリソースが解放されますので、コードはこのコンテキストで実行されるべき操作が完了したら、すぐにキャンセルを呼び出すべきです。
    func gen(ctx context.Context) chan int {
         
    		dst := make(chan int)
    		n := 1
    		go func() {
         
    			for {
         
    				select {
         
    				case ctx.Done():
    					return // return   goroutine,    
    				case dst  n:
    					n++
    				}
    			}
    		}()
    		return dst
    	}
    func main() {
         
    	ctx, cancel := context.WithCancel(context.Background())
    	defer cancel() //              cancel
    
    	for n := range gen(ctx) {
         
    		fmt.Println(n)
    		if n == 5 {
         
    			break
    		}
    	}
    }
    
    上記の例示的なコードでは、gen関数は、個々のgoroutineにおいて整数を生成し、それらを返すチャネルに送信する。genの使用者は、生成された整数を使用した後、コンテキストをキャンセルする必要があり、genが起動している内部goroutineが漏洩しないようにする。
    WithDeadlineWithDeadlineの関数署名は以下の通りである。
    func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
    
    親コンテキストのコピーを返し、deadlineをdに遅くないように調整します。父の文脈のdeadlineがすでにdより早い場合、WithDeadlineは語義的には父の文脈に等しい。締め切りが切れた時、リターンのcancel関数を呼び出した時、または親コンテキストのDuneチャネルがオフになった時、コンテキストに戻るDuneチャネルは閉じられます。最初に発生した場合に準じます。
    このコンテキストをキャンセルすると、関連するリソースが解放されますので、コードはこのコンテキストで実行されるべき操作が完了したら、すぐにキャンセルを呼び出すべきです。
    func main() {
         
    	d := time.Now().Add(50 * time.Millisecond)
    	ctx, cancel := context.WithDeadline(context.Background(), d)
    
    	//   ctx   ,           cancel         。
    	//       ,                       。
    	defer cancel()
    
    	select {
         
    	case time.After(1 * time.Second):
    		fmt.Println("overslept")
    	case ctx.Done():
    		fmt.Println(ctx.Err())
    	}
    }
    
    上記のコードでは、50ミリ秒後に期限が切れるdeadlineを定義し、context.WithDeadline(context.Background(), d)を呼び出してコンテキスト(ctx)とキャンセル関数(cancel)を得てから、selectを使ってメインプログラムを待機させます。1秒後にプリントoversleptが終了するか、またはctxが期限が切れるまで待って終了します。ctx 50秒後に期限が切れますので、ctx.Done()は先に値を受け取ります。上のコードはctx.Err()を印刷します。
    WithTimeoutWithTimeoutの関数署名は以下の通りである。
    func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
    
    WithTimeoutWithDeadline(parent, time.Now().Add(timeout))を返します。
    このコンテキストをキャンセルすると、関連するリソースが解放されますので、コードはこのコンテキストで実行されるべき操作が完了したら、すぐにキャンセルを呼び出します。通常はデータベースまたはネットワーク接続のタイムアウト制御に使用されます。具体例は以下の通りです。
    package main
    
    import (
    	"context"
    	"fmt"
    	"sync"
    
    	"time"
    )
    
    // context.WithTimeout
    
    var wg sync.WaitGroup
    
    func worker(ctx context.Context) {
         
    LOOP:
    	for {
         
    		fmt.Println("db connecting ...")
    		time.Sleep(time.Millisecond * 10) //            10  
    		select {
         
    		case ctx.Done(): // 50       
    			break LOOP
    		default:
    		}
    	}
    	fmt.Println("worker done!")
    	wg.Done()
    }
    
    func main() {
         
    	//     50     
    	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
    	wg.Add(1)
    	go worker(ctx)
    	time.Sleep(time.Second * 5)
    	cancel() //    goroutine  
    	wg.Wait()
    	fmt.Println("over")
    }
    
    WithValue関数は、要求されたスコープのデータをContectオブジェクトと関係付けることができる。以下のように声明します
    func WithValue(parent Context, key, val interface{
         }) Context
    
    WithValueは、keyに関連する値がvalである親ノードのコピーを返す。
    APIとプロセス間の要求領域を伝達するデータのみに、コンテキスト値を使用して、オプションのパラメータを関数に伝達するために使用されるのではない。
    提供されたキーは比較可能であり、コンテキストを使用してパケット間で衝突が起こることを避けるために、stringタイプまたは他の任意の内蔵タイプであるべきではない。WithValueのユーザは、キーのために自分のタイプを定義しなければならない。interface{}に割り当てられたときに割り当てられることを避けるために、コンテキストキーは通常、特定のタイプstruct{}を有する。あるいは、導出されたコンテキストキー変数の静的なタイプは、ポインタまたはインターフェースであるべきである。
    package main
    
    import (
    	"context"
    	"fmt"
    	"sync"
    
    	"time"
    )
    
    // context.WithValue
    
    type TraceCode string
    
    var wg sync.WaitGroup
    
    func worker(ctx context.Context) {
         
    	key := TraceCode("TRACE_CODE")
    	traceCode, ok := ctx.Value(key).(string) //   goroutine   trace code
    	if !ok {
         
    		fmt.Println("invalid trace code")
    	}
    LOOP:
    	for {
         
    		fmt.Printf("worker, trace code:%s
    "
    , traceCode) time.Sleep(time.Millisecond * 10) // 10 select { case ctx.Done(): // 50 break LOOP default: } } fmt.Println("worker done!") wg.Done() } func main() { // 50 ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50) // trace code goroutine ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12512312234") wg.Add(1) go worker(ctx) time.Sleep(time.Second * 5) cancel() // goroutine wg.Wait() fmt.Println("over") }
    Contextを使用する注意事項
  • 転送Contact
  • をパラメータで表示することを推奨します。
  • は、Contectをパラメータの関数とする方法であり、Contectを最初のパラメータとするべきである。
  • 関数メソッドにContectを渡す時は、nilを伝えないでください。何を伝えたらいいのか分からない時は、context.TODO()
  • を使います。
  • ContectのValue関連方法は、要求ドメインの必要なデータを伝達し、オプションパラメータ
  • を転送するために使用されるべきではない。
  • Contectはスレッドの安全性であり、複数のgoroutineの中で安心して
  • を伝えることができます。
    クライアントタイムアウトキャンセル例
    // context_timeout/server/main.go
    package main
    
    import (
    	"fmt"
    	"math/rand"
    	"net/http"
    
    	"time"
    )
    
    // server ,       
    
    func indexHandler(w http.ResponseWriter, r *http.Request) {
         
    	number := rand.Intn(2)
    	if number == 0 {
         
    		time.Sleep(time.Second * 10) //   10     
    		fmt.Fprintf(w, "slow response")
    		return
    	}
    	fmt.Fprint(w, "quick response")
    }
    
    func main() {
         
    	http.HandleFunc("/", indexHandler)
    	err := http.ListenAndServe(":8000", nil)
    	if err != nil {
         
    		panic(err)
    	}
    }
    
    client端
    // context_timeout/client/main.go
    package main
    
    import (
    	"context"
    	"fmt"
    	"io/ioutil"
    	"net/http"
    	"sync"
    	"time"
    )
    
    //    
    
    type respData struct {
         
    	resp *http.Response
    	err  error
    }
    
    func doCall(ctx context.Context) {
         
    	transport := http.Transport{
         
    	   //           client        
    	   //           
    	   DisableKeepAlives: true, 	}
    	client := http.Client{
         
    		Transport: &transport,
    	}
    
    	respChan := make(chan *respData, 1)
    	req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil)
    	if err != nil {
         
    		fmt.Printf("new requestg failed, err:%v
    "
    , err) return } req = req.WithContext(ctx) // ctx client request var wg sync.WaitGroup wg.Add(1) defer wg.Wait() go func() { resp, err := client.Do(req) fmt.Printf("client.do resp:%v, err:%v
    "
    , resp, err) rd := &respData{ resp: resp, err: err, } respChan rd wg.Done() }() select { case ctx.Done(): //transport.CancelRequest(req) fmt.Println("call api timeout") case result := respChan: fmt.Println("call server api success") if result.err != nil { fmt.Printf("call server api failed, err:%v
    "
    , result.err) return } defer result.resp.Body.Close() data, _ := ioutil.ReadAll(result.resp.Body) fmt.Printf("resp:%v
    "
    , string(data)) } } func main() { // 100 ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) defer cancel() // cancel goroutine doCall(ctx) }