GOコンテキストから始める


並列プログラミングのキャンセルとデータ共有のような信頼できるアプリケーションの非常に重要な側面を制御して、管理するためのgolang使用文脈のアプリケーション.これは些細なことに聞こえるかもしれませんが、実際にはそうではありません.Golangのコンテキストのエントリポイントはコンテキストパッケージです.これは非常に有用であり、おそらく1つの言語の中で最も汎用性のパッケージです.あなたがまだ文脈に対処している何かに遭遇していないならば、あなたは多分非常にすぐに(あるいは、多分、あなたはそれにあまり注意を払わなかったでしょう).コンテキストの使用方法は、他の複数のパッケージがそれに依存しており、同じことをすると仮定しています.それは確かにゴランの生態系の重要なコンポーネントです.

コンテキストパッケージhttps://golang.org/pkg/context/の公式ドキュメントです.それは本当に素晴らしいと実用的な例でいっぱいです.それらを拡張する試みでは、実際のアプリケーションで遭遇したことを見てみましょう.

値のコンテキスト


文脈の最も一般的な用途の1つはデータを共有するか、またはリクエストスコープ値を使用することです.複数の関数を持ち、それらの間でデータを共有したい場合は、コンテキストを使用して行うことができます.もっとも簡単な方法は、関数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)
}
コンテクストパッケージの背後にあるデザインの重要な側面は、すべて242479142の構造体を返すことです.これは、操作から返された値で動作し、古いコンテキストを新しいものでオーバーライドすることを覚えておかなければならないことを意味します.これは不変性のキーデザインです.
この手法を使用すると、context.Contextに沿って並行した関数に渡すことができますし、適切にコンテキストを管理する限り、これらの同時実行関数間のスコープ値を共有するのに良い方法です(各コンテキストは、スコープ内で独自の値を維持することを意味します).HTTPリクエストを処理する際には、context.Contextパッケージが正確に処理されます.詳しくは次の例を見てみましょう.

ミドルウェア


リクエストスコープ値の大きな例とユースケースは、Webリクエストハンドラーのミドルウェアで動作します.タイプnet/httpはHTTPパイプラインを通してスコープ値を運ぶことができるコンテキストを含んでいます.ミドルウェアがHTTPパイプラインに加えられ、ミドルウェアの結果がhttp.Requestコンテキストに追加されるコードを見ることは非常に一般的です.これは非常に有用なテクニックとして、あなたはすでに後段にあなたのパイプラインで起こった知っているものに依存することができます.これにより、HTTPリクエストを処理するためにジェネリックコードを使用できますが、データを共有するスコープ(たとえばグローバル変数のデータを共有する代わりに)を尊重します.リクエストコンテキストを利用するミドルウェアの例です.
package main

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

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

func main() {
    router := mux.NewRouter()
    router.Use(guidMiddleware)
    router.HandleFunc("/ishealthy", handleIsHealthy).Methods(http.MethodGet)
    http.ListenAndServe(":8080", router)
}

func handleIsHealthy(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    uuid := r.Context().Value("uuid")
    log.Printf("[%v] Returning 200 - Healthy", uuid)
    w.Write([]byte("Healthy"))
}

func guidMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        uuid := uuid.New()
        r = r.WithContext(context.WithValue(r.Context(), "uuid", uuid))
        next.ServeHTTP(w, r)
    })
}

文脈解除


ゴランの文脈のもう一つの非常に役に立つ特徴は、関係しているものをキャンセルしています.これは非常にあなたのキャンセルを伝播したいときに重要です.あなたが1を受け取るとき、キャンセル信号を伝播することは良い習慣です.あなたが何十ものGoroutinesを始める機能があると言いましょう.その主な関数は、すべてのGoroutinesを終了するか、または処理する前にキャンセル信号を待ちます.あなたがキャンセル信号を受け取るならば、あなたはあなたのすべてのGoroutinesにそれを伝播させたいかもしれません.あなたがすべてのGoroutinesの間で同じ文脈を共有するならば、あなたはそれを簡単にすることができます.
キャンセルを伴う文脈を作成するために、あなたの文脈をパラメタとして渡す機能http.Requestだけを呼ぶ必要があります.これは新しいコンテキストとキャンセル関数を返します.このコンテキストをキャンセルするには、cancel関数を呼び出す必要があります.
package main

import (
    "context"
    "fmt"
    "io/ioutil"
    "net/http"
    neturl "net/url"
    "time"
)

func queryWithHedgedRequestsWithContext(urls []string) string {
    ch := make(chan string, len(urls))
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    for _, url := range urls {
        go func(u string, c chan string) {
            c <- executeQueryWithContext(u, ctx)
        }(url, ch)

        select {
        case r := <-ch:
            cancel()
            return r
        case <-time.After(21 * time.Millisecond):
        }
    }

    return <-ch
}

func executeQueryWithContext(url string, ctx context.Context) string {
    start := time.Now()
    parsedURL, _ := neturl.Parse(url)
    req := &http.Request{URL: parsedURL}
    req = req.WithContext(ctx)

    response, err := http.DefaultClient.Do(req)

    if err != nil {
        fmt.Println(err.Error())
        return err.Error()
    }

    defer response.Body.Close()
    body, _ := ioutil.ReadAll(response.Body)
    fmt.Printf("Request time: %d ms from url%s\n", time.Since(start).Nanoseconds()/time.Millisecond.Nanoseconds(), url)
    return fmt.Sprintf("%s from %s", body, url)
}
それぞれの要求は別々のゴロウーンで発火される.すべてのリクエストに対してコンテキストが渡されます.コンテキストで行われている唯一のことは、HTTPクライアントに伝播することです.これにより、キャンセル関数が呼び出されたときのリクエストと基本的な接続の優雅なキャンセルが可能になります.これはcontext.WithCancel(ctx)を引数として受け入れる関数の非常に一般的なパタ-ンですが、それらはどちらかというと文脈において積極的に動作します(キャンセルされたかどうかをチェックするかのように)、あるいはそれを扱う基礎的な関数に渡します(この場合、context.Contextを通して文脈を受け取るDO関数).

コンテキストタイムアウト


タイムアウトは、外部の要求を行うための本当に一般的なパターンです.データベースのクエリや、HTTPまたはGRPC(両方のサポートコンテキスト)を通じて、別のサービスからデータを取得するような.これらのシナリオの処理は、コンテキストパッケージを使用して簡単です.あなたがしなければならないすべては、あなたの前後関係と実際のタイムアウトを渡す機能http.Requestです.このように:
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
手動でトリガしたい場合は、まだキャンセル機能を受け取ります.これは、通常のコンテキストキャンセルと同じように動作します.
この場合の動作はとても簡単です.タイムアウトに達した場合、コンテクストはキャンセルされます.HTTPコールの場合、基本的には上記の例と同じです.

GRPC


コンテキストはGolangでのGRPC実装の基本的な部分です.これは、ストリームやリクエストをキャンセルするようなデータ(いわゆるメタデータ)を共有し、フローを制御するために使用されます.
// Server implementation receiving metadata
func (*server) Sum(ctx context.Context, req *calculatorpb.SumRequest) (*calculatorpb.SumResponse, error) {
    log.Printf("Sum rpc invoked with req: %v\n", req)
    md, _ := metadata.FromIncomingContext(ctx)
    log.Printf("Metadata received: %v", md)
    return &calculatorpb.SumResponse{
        Result: req.NumA + req.NumB,
    }, nil
}

// Client implementation sending metadata
func sum(c calculatorpb.CalculatorServiceClient) {
    req := &calculatorpb.SumRequest{
        NumA: 3,
        NumB: 10,
    }
    ctx := metadata.AppendToOutgoingContext(context.Background(), "user", "test")
    res, err := c.Sum(ctx, req)
    if err != nil {
        log.Fatalf("Error calling Sum RPC: %v", err)
    }
    log.Printf("Response: %d\n", res.Result)
}

// Server implementation handling context cancellation
func (*server) Greet(ctx context.Context, req *greetpb.GreetRequest) (*greetpb.GreetResponse, error) {
    log.Println("Greet rpc invoked!")

    time.Sleep(500 * time.Millisecond)

    if ctx.Err() == context.Canceled {
        return nil, status.Error(codes.Canceled, "Client cancelled the request")
    }

    first := req.Greeting.FirstName
    return &greetpb.GreetResponse{
        Result: fmt.Sprintf("Hello %s", first),
    }, nil
}

// Client implementation using timeout context cancellation
func greetWithTimeout(c greetpb.GreetServiceClient) {
    req := &greetpb.GreetRequest{
        Greeting: &greetpb.Greeting{
            FirstName: "Ricardo",
            LastName:  "Linck",
        },
    }
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
    defer cancel()
    res, err := c.Greet(ctx, req)
    if err != nil {
        grpcErr, ok := status.FromError(err)

        if ok {
            if grpcErr.Code() == codes.DeadlineExceeded {
                log.Fatal("Deadline Exceeded")
            }
        }

        log.Fatalf("Error calling Greet RPC: %v", err)
    }
    log.Printf("Response: %s\n", res.Result)
}

オプトメントメトリー


OpenTextRemoteはまた、コンテキストのコンテキストには、コンテキストの伝播と呼ばれるに依存します.それは、異なるシステムで起こっている要求を結びつける方法です.それを実装する方法は、あなたが使用しているプロトコルの一部(例えばhttpやgrpc)として送信するコンテキストにスパン情報を注入することです.他のサービスでは、コンテキストからスパン情報を抽出する必要があります.