コンテンツにスキップ

Go@gRPCクライアントパッケージ

はじめに

本サイトにつきまして、以下をご認識のほど宜しくお願いいたします。


01. セットアップ

各種ツール

▼ Protocol Bufferコンパイラー

protoファイルをコンパイルするために、Protocol Bufferコンパイラーをインストールする。

マイクロサービス別にリポジトリがある場合、各リポジトリで同じバージョンのprotocコマンドを使用できるように、パッケージ管理ツールを使用した方がよい。

$ asdf plugin list all | grep protoc

$ asdf plugin add protoc https://github.com/paxosglobal/asdf-protoc.git

$ asdf install protoc

▼ Protocol BufferコンパイラーGoプラグイン

サービス定義ファイル (protoファイル) からpb.goファイルをコンパイルするために、Protocol Bufferコンパイラーのプラグインをインストールする。

ただし、執筆時点 (2024/07/15) では、 protoc-gen-goモジュールに移行するべきである。

$ go install google.golang.org/protobuf/cmd/protoc-gen-go@HEAD

$ protoc-gen-go --version

protoc-gen-go <バージョン>

▼ Protocol BufferコンパイラーGo-gRPCプラグイン

サービス定義ファイル (protoファイル) からgRPC対応のpb.goファイルをコンパイルするために、Protocol Bufferコンパイラーのプラグインをインストールする。

protoc-gen-goの移行先のモジュールである。

$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@HEAD

$ protoc-gen-go-grpc --version

protoc-gen-go-grpc <バージョン>

▼ 上記ツールを持つ専用コンテナ

各種ツールをGoアプリにダウンロードしても良いが、専用コンテナとして切り分けるとよい。

サービス定義ファイル (protoファイル) からpb.goファイルを作成したくなったら、このコンテナを実行する。

docker-compose.ymlファイルは以下の通りである。

services:
  grpc_compile:
    image: protocol_buffer_compiler
    build:
      context: .
    container_name: protocol_buffer_compiler
    volumes:
      - .:/

Dockerfileファイルは以下の通りである。

FROM golang:<Goアプリと同じバージョン>

RUN apt update -y \
  && apt install -y protobuf-compiler \
  && export GO111MODULE=on \
  && go install google.golang.org/protobuf/cmd/protoc-gen-go@<バージョン> \
  && go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@<バージョン>

COPY . /
CMD ["/protocol_buffer_compiler.sh"]

protocol_buffer_compiler.shファイルは以下の通りである。

バックアップも兼ねて、pb.goファイルを作業日付ごとに作成する。

#!/bin/sh

# 日付ごとにディレクトリを作成する
DATE=`date '+%Y%m%d%H%M%S'`
mkdir -p ${DATE}

protoc \
  -I=${DATE} \
  --go_out=${DATE} \
  --go_opt=paths=source_relative \
  --go-grpc_out=${DATE} \
  --go-grpc_opt=paths=source_relative,require_unimplemented_servers=false \
  *.proto


gRPCクライアントとgRPCサーバーの両方

protoファイル (サービス定義ファイル) とは

gRPCクライアントとgRPCサーバーの両方で、サービス定義ファイル (protoファイル) を作成する。

例えば、messageはJSONに代わるスキーマを表し、serviceはgRPCにおけるAPI仕様を表す。

サービス定義ファイルにインターフェースとメッセージ構造を実装し、このファイルからpb.goファイルをコンパイルする。

pb.goファイルとは

gRPCクライアントとgRPCサーバーの両方で、protoファイルからpb.goファイルをコンパイルする。

protocコマンドを使用して、pb.goファイルを作成する。

このファイルには、gRPCクライアントとgRPCサーバーの両方が参照するための実装が定義されており、開発者はそのまま使用すれば良い。

# foo.protoファイルから、gRPCに対応するfoo.pb.goファイルをコンパイルする。
$ protoc -I=. --go_out=. --go-grpc_out=. foo.proto

# ワイルドカードで指定できる。
$ protoc -I=. --go_out=. --go-grpc_out=. *.proto

▼ RPC-API仕様書

gRPCにおけるAPI仕様書である。

仕様の実装であるprotoファイルを使用して、RPC-API仕様書を作成できる。

$ protoc --doc_out=. --doc_opt=html,index.html *.proto


サーバー側のみ

▼ gRPCサーバー

リモートプロシージャーコールを受け付けるサーバーを定義する。

サーバーをgRPCサーバーとして登録する必要がある。


gRPCクライアント側のみ

▼ gRPCクライアントパッケージ

記入中...

▼ gRPCクライアント

GoのgRPCサーバーをリモートプロシージャーコールする。


02. クライアント側のインターセプター

クライアント側のインターセプターとは

gRPCでは、ミドルウェア処理として、インターセプターをリクエスト処理の前後に挿入する。


インターセプターの種類

▼ メトリクス系

package main

import (
    "google.golang.org/grpc"

    grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
)

func main() {

    ...

    // gRPCサーバーとのコネクションを作成する
    conn, err := grpc.DialContext(
        ctx,
        ":7777",
        grpc.WithChainUnaryInterceptor(grpc_prometheus.UnaryClientInterceptor),
    )

    ...
}

▼ 分散トレース系

package main

import (
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "google.golang.org/grpc"
)

func main() {

    ...

    // gRPCサーバーとのコネクションを作成する
    conn, err := grpc.DialContext(
        ctx,
        ":7777",
        grpc.WithChainUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
    )

    ...
}


02-02. インターセプターの設定方法

単項RPCの場合

▼ 既製のインターセプター (UnaryClientInterceptor)

gRPCでは、単項RPCを送信するクライアント側のミドルウェア処理はUnaryClientInterceptorという名前で定義されている。

type UnaryClientInterceptor func(
    ctx context.Context,
    method string,
    req,
    reply interface{},
    cc *ClientConn,
    invoker UnaryInvoker,
    opts ...CallOption
    ) error

この関数は、リクエストでエラーが起こった場合に、内部的にSetStatus関数を実行する。

エラー時に、Spanステータスとエラーメッセージをスパンに設定してくれる。

これをgRPCサーバーとのコネクション作成時に、WithChainUnaryInterceptor関数に渡す。

package main

import (
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "google.golang.org/grpc"

    // pb.goファイルを読み込む。
    pb "github.com/hiroki-hasegawa/foo/foo"
)

func main() {

    ...

    // gRPCサーバーとのコネクションを作成する
    conn, err := grpc.DialContext(
        ctx,
        ":7777",
        grpc.WithChainUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
    )

    defer conn.Close()

    // gRPCクライアントを作成する
    client := pb.NewFooServiceClient(conn)

    // goサーバーをリモートプロシージャーコールする。
    response, err := client.SayHello(
        context.Background(),
        &pb.Message{Body: "Hello From Client!"},
    )

    ...
}

▼ 自前のインターセプター

関数に定められた引数を定義すれば、インターセプターとして使用できる。

package intercetor

import (
    "google.golang.org/grpc"
)

func UnaryClientInterceptor(
    ctx context.Context,
    method string,
    req,
    reply interface{},
    cc *grpc.ClientConn,
    invoker grpc.UnaryInvoker,
    callOpts ...grpc.CallOption,
    ) error {

    // リクエスト送信の事前処理

    err := invoker(ctx, method, req, reply, cc, callOpts...) // 単項RPC処理

    // リクエスト送信の事後処理

    return err
}

ユーザー定義のオプションを渡したい場合は、grpc.UnaryClientInterceptor型を返却する関数とする。

package intercetor

import (
    "google.golang.org/grpc"
)

func UnaryClientInterceptor(opts ...fooOption) grpc.UnaryClientInterceptor {

    // オプションを使用してパラメーターを作成する

    return func(
        ctx context.Context,
        method string,
        req,
        reply interface{},
        cc *grpc.ClientConn,
        invoker grpc.UnaryInvoker,
        callOpts ...grpc.CallOption,
    ) error {

        // リクエスト送信の事前処理

        err := invoker(ctx, method, req, reply, cc, callOpts...) // 単項RPC処理

        // リクエスト送信の事後処理

        return err
    }
}


ストリーミングRPCの場合

▼ 既製のインターセプター (StreamClientInterceptor)

gRPCでは、ストリーミングRPCを送信するクライアント側のミドルウェア処理は、StreamClientInterceptorという名前にすることが定められている。

type StreamClientInterceptor func(
    ctx context.Context,
    desc *StreamDesc,
    cc *ClientConn,
    method string,
    streamer Streamer,
    opts ...CallOption,
) (ClientStream, error)

この関数は、リクエストでエラーが起こった場合に、内部的にSetStatus関数を実行する。

エラー時に、Spanステータスとエラーメッセージをスパンに設定してくれる。

これをgRPCサーバーとのコネクション作成時に、WithChainStreamInterceptor関数に渡す。

package main

import (
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "google.golang.org/grpc"

    // pb.goファイルを読み込む。
    pb "github.com/hiroki-hasegawa/foo/foo"
)

func main() {

    ...

    // gRPCサーバーとのコネクションを作成する
    conn, err := grpc.DialContext(
        ctx,
        ":7777",
        grpc.WithChainStreamInterceptor(otelgrpc.StreamClientInterceptor()),
    )

    defer conn.Close()

    // gRPCクライアントを作成する
    client := pb.NewFooServiceClient(conn)

    // goサーバーをリモートプロシージャーコールする。
    response, err := client.SayHello(
        context.Background(),
        &pb.Message{Body: "Hello From Client!"},
    )

    ...
}

▼ 自前のインターセプター

関数に定められた引数を定義すれば、インターセプターとして使用できる。

package intercetor

import (
    "google.golang.org/grpc"
)

func StreamClientInterceptor(
    ctx context.Context,
    desc *StreamDesc,
    cc *ClientConn,
    method string,
    streamer Streamer,
    opts ...CallOption,
    ) (grpc.ClientStream, error) {

        // 事前処理

        streamer, err := streamer(ctx, desc, cc, method, callOpts...) // ストリーミングRPC処理

        // 事後処理

        return &wrapClientStream{streamer}, err
}

type wrapClientStream struct {
    grpc.ClientStream
}

ユーザー定義のオプションを渡したい場合は、grpc.StreamClientInterceptor型を返却する関数とする。

package intercetor

import (
    "google.golang.org/grpc"
)

func StreamClientInterceptor(opts ...fooOption) grpc.StreamServerInterceptor {

    // オプションを使用してパラメーターを作成する

    return func(
        ctx context.Context,
        desc *StreamDesc,
        cc *ClientConn,
        method string,
        streamer Streamer,
        opts ...CallOption,
    ) (grpc.ClientStream, error) {

        // 事前処理

        streamer, err := streamer(ctx, desc, cc, method, callOpts...) // ストリーミングRPC処理

        // 事後処理

        return &wrapClientStream{streamer}, err
    }
}

type wrapClientStream struct {
    grpc.ClientStream
}


03. サーバー側のインターセプター

サーバー側のインターセプターとは

gRPCでは、ミドルウェア処理として、インターセプターをレスポンス処理の前後に挿入する。


インターセプターの種類

▼ 認証系

記入中...

▼ メトリクス系

記入中...

▼ 分散トレース系

記入中...

▼ リカバー系

gRPCの処理で起こったパニックを、Internal Server Errorとして処理する。

package main

import (

    grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/interceptors/recovery"

    "google.golang.org/grpc"

    ...

)

func main() {

    // gRPCサーバーを作成する。
    grpcServer := grpc.NewServer(
        // 単項RPCのサーバーインターセプター処理
        grpc.ChainUnaryInterceptor(
            // リカバー処理
            grpc_recovery.UnaryServerInterceptor(...),
        ),
    )

    ...

}

▼ フィルター系

分散トレースの作成を無視する場合を設定する。

package main

import (

    grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/interceptors/recovery"

    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/filters"
    "google.golang.org/grpc"

    ...

)

func main() {

    // gRPCサーバーを作成する。
    grpcServer := grpc.NewServer(
        // 単項RPCのサーバーインターセプター処理
        grpc.ChainUnaryInterceptor(
            // ヘルスチェックへのリクエストを無視する
            grpc_recovery.UnaryServerInterceptor(otelgrpc.WithInterceptorFilter(filters.HealthCheck())),
        ),
    )

    ...

}


03-02. インターセプターの設定方法

単項RPCの場合

▼ 既製のインターセプター (UnaryServerInterceptor)

gRPCでは、単項RPCを受信するサーバー側のミドルウェア処理は、UnaryServerInterceptorという名前にすることが定められている。

type UnaryServerInterceptor func(
    ctx context.Context,
    req interface{},
    info *UnaryServerInfo,
    handler UnaryHandler,
    ) (resp interface{}, err error)

▼ 自前のインターセプター

関数に定められた引数を定義すれば、インターセプターとして使用できる。

package interceptor

import (
    "context"

    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "google.golang.org/grpc"
)

func UnaryServerInterceptor(
    ctx context.Context,
    req interface{},
    info *UnaryServerInfo,
    handler UnaryHandler,
    ) (resp interface{}, err error) {

    // 事前処理

    resp, err := handler(ctx, req) // 単項RPC処理

    // 事後処理

    return err
}

ユーザー定義のオプションを渡したい場合は、grpc.UnaryServerInterceptor型を返却する関数とする。

package interceptor

import (
    "context"

    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "google.golang.org/grpc"
)

func UnaryServerInterceptor(opts ...fooOption) grpc.UnaryServerInterceptor {

    // オプションを使用してパラメーターを作成する

    return func(
        ctx context.Context,
        req interface{},
        info *grpc.UnaryServerInfo,
        handler grpc.UnaryHandler,
        ) (resp interface{}, err error) {

        // 事前処理

        resp, err := handler(ctx, req) // 単項RPC処理

        // 事後処理

        return err
    }
}


ストリーミングRPCの場合

▼ 既製のインターセプター (StreamServerInterceptor)

gRPCでは、ストリーミングRPCを受信するサーバー側のミドルウェア処理はStreamServerInterceptorという名前にすることが定められている。

type StreamServerInterceptor func(
    srv interface{},
    ss ServerStream,
    info *StreamServerInfo,
    handler StreamHandler,
    ) error

▼ 自前のインターセプター

関数に定められた引数を定義すれば、インターセプターとして使用できる。

package interceptor

import (
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "google.golang.org/grpc"
)

func StreamServerInterceptor(
    srv interface{},
    ss ServerStream,
    info *StreamServerInfo,
    handler StreamHandler,
    ) error {

    // 事前処理

    err := handler(srv, wrapServerStream(ctx, ss, cfg)) // ストリーミングRPC処理

    // 事後処理

    return err
}

ユーザー定義のオプションを渡したい場合は、grpc.StreamServerInterceptor型を返却する関数とする。

package interceptor

import (
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "google.golang.org/grpc"
)

func StreamServerInterceptor(opts ...fooOption) grpc.StreamServerInterceptor {

    // オプションを使用してパラメーターを作成する

    return func(
        srv interface{},
        ss ServerStream,
        info *StreamServerInfo,
        handler StreamHandler,
    ) error {

        // 事前処理

        err := handler(srv, wrapServerStream(ctx, ss, cfg)) // ストリーミングRPC処理

        // 事後処理

        return err
    }
}


04. サーバー側の実装例

gRPCサーバー (インターセプターがない場合)

gRPCサーバーを実装する。

package main

import (
    "context"
    "fmt"
    "log"
    "net"

    // pb.goファイルを読み込む。
    pb "github.com/hiroki-hasegawa/foo/foo"

    "google.golang.org/grpc"
)

// goサーバー
type Server struct {
}

// Helloを返信する関数
func (s *Server) SayHello (ctx context.Context, in *pb.Message) (*Message, error) {
    log.Printf("Received message body from client: %v", in.Body)
    return &pb.Message{Body: "Hello From the Server!"}, nil
}

func main() {

    // gRPCサーバーを作成する。
    grpcServer := grpc.NewServer()

    // pb.goファイルでコンパイルされた関数を使用して、goサーバーをgRPCサーバーとして登録する。
    // goサーバーがリモートプロシージャーコールを受信できるようになる。
    pb.RegisterFooServiceServer(grpcServer, &Server{})

    // goサーバーで待ち受けるポート番号を設定する。
    listenPort, _ := net.Listen("tcp", fmt.Sprintf(":%d", 9000))

    if err != nil {
        log.Fatalf("Failed to listen: %v", err)
    }

    // gRPCサーバーとして、goサーバーで通信を受信する。
    if err := grpcServer.Serve(listenPort); err != nil {
        log.Fatalf("Failed to serve: %v", err)
    }

    ...
}


gRPCサーバー (インターセプターを使用する場合)

▼ インターセプターを使用する場合について

gRPCサーバーでは、リクエスト/レスポンスの送受信前のミドルウェア処理として、インターセプターを実行できる。

Chain関数であれば単一のインターセプター、一方でChain関数であれば複数のインターセプターを渡せる。

執筆時点 (202309/16) で、パッケージのv1は非推奨で、v2が推奨である。

package main

import (
    "fmt"
    "log"
    "net"

    // pb.goファイルを読み込む。
    pb "github.com/hiroki-hasegawa/foo/foo"

    grpc_prometheus "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus"
    grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/interceptors/auth"
    grpc_logging "github.com/grpc-ecosystem/go-grpc-middleware/interceptors/logging"
    grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/interceptors/recovery"
    grpc_selector "github.com/grpc-ecosystem/go-grpc-middleware/interceptors/selector"

    "github.com/grpc-ecosystem/go-grpc-middleware/interceptors"
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "google.golang.org/grpc"
)

func main() {

    // gRPCサーバーを作成する。
    grpcServer := grpc.NewServer(
        // 単項RPCのサーバーインターセプター処理
        grpc.ChainUnaryInterceptor(
            // 認証処理
            grpc_selector.UnaryServerInterceptor(...),
            // メトリクス処理
            grpc_prometheus.UnaryServerInterceptor(...),
            // ロギング処理
            grpc_logging.UnaryServerInterceptor(...),
            // 分散トレースのスパン作成処理
            otelgrpc.UnaryServerInterceptor(...),
            // リカバー処理
            grpc_recovery.UnaryServerInterceptor(...),
        ),
        // ストリーミングRPCのサーバーインターセプター処理
        grpc.ChainStreamInterceptor(
            otelgrpc.StreamServerInterceptor(...),
            recovery.StreamServerInterceptor(...),
        ),
    )

    ...


    // goサーバーで待ち受けるポート番号を設定する。
    listenPort, _ := net.Listen("tcp", fmt.Sprintf(":%d", 9000))

    // gRPCサーバーとして、goサーバーで通信を受信する。
    if err := grpcServer.Serve(listenPort); err != nil {
        log.Fatalf("Failed to serve: %v", err)
    }

    ...
}


ヘルスチェックサーバー

grpc_health_v1パッケージのRegisterHealthServer関数を使用して、gRPCサーバーをヘルスチェックサーバーとして登録する。

package main

import (
    "log"
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/health"

    grpc_health_v1 "google.golang.org/grpc/health/grpc_health_v1"
)

func main() {

    ...

    // gRPCサーバーを作成する。
    grpcServer := grpc.NewServer(
        ...
    )

    ...

    healthCheckServer := health.NewServer()

    grpc_health_v1.RegisterHealthServer(grpcServer, healthCheckServer)

    // goサーバーで待ち受けるポート番号を設定する。
    listenPort, _ := net.Listen("tcp", fmt.Sprintf(":%d", 9000))

    // gRPCサーバーとして、goサーバーで通信を受信する。
    if err := grpcServer.Serve(listenPort); err != nil {
        log.Fatalf("Failed to serve: %v", err)
    }

    ...
}


05. gRPCクライアント側の実装例

gRPCクライアントパッケージ

記入中...


gRPCクライアント

gRPCクライアント側では、gRPCサーバーとのコネクションを作成する必要がある。

package main

import (
    "log"

    "golang.org/x/net/context"
    "google.golang.org/grpc"

    // pb.goファイルを読み込む。
    pb "github.com/hiroki-hasegawa/foo/foo"
)

func main() {

    // gRPCサーバーとのコネクションを作成する
    conn, err := grpc.DialContext(
        ctx,
        ":7777",
        grpc.WithInsecure(),
    )

    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }

    defer conn.Close()

    // gRPCクライアントを作成する
    client := pb.NewFooServiceClient(conn)

    // goサーバーをリモートプロシージャーコールする。
    response, err := client.SayHello(
        context.Background(),
        &pb.Message{Body: "Hello From Client!"},
    )

    if err != nil {
        log.Fatalf("Error when calling SayHello: %v", err)
    }

    // goサーバーからの返却を確認する。
    log.Printf("Response from server: %v", response.Body)
}


gRPCクライアント (インターセプターを使用する場合)

▼ インターセプターを使用する場合について

gRPCクライアントでは、リクエスト/レスポンスの送受信前のミドルウェアとして、インターセプターを実行できる。

Chain関数であれば単一のインターセプター、一方でChain関数であれば複数のインターセプターを渡せる。

▼ ストリーミングRPCの場合

package main

import (
    "google.golang.org/grpc"
)

func main() {

    ...

    // gRPCサーバーとのコネクションを作成する
    conn, err := grpc.DialContext(
        ctx,
        ":7777",
        // ストリーミングRPCのインターセプター処理
        grpc.WithChainStreamInterceptor(
            myStreamClientInteceptor1,
            myStreamClientInteceptor2,
        ),
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithBlock(),
    )

    ...
}


06. gRPCサーバーとクライアントの両方の実装例

protoファイル

クライアントからのコールで返信する構造体や関数を定義する。

// protoファイルの構文のバージョンを設定する。
syntax = "proto3";

import "google/api/annotations.proto";

// pb.goファイルでコンパイルされる時のパッケージ名
package foo;

// gRPCクライアント側からのリモートプロシージャーコール時に渡す引数を定義する。
// フィールドのタグを1としている。メッセージ内でユニークにする必要があり、フィールドが増えれば別の数字を割り当てる。
message Message {
  string body = 1;
}

// 単項RPC
// gRPCクライアント側からリモートプロシージャーコールされる関数を定義する。
service FooService {
  rpc SayHello(Message) returns (Message) {
    // エンドポイント
    option (google.api.http).get = "/foo";
  }
}


pb.goファイル

事前に用意したprotoファイルを使用して、pb.goファイルをコンパイルする。

pb.goファイルには、gRPCクライアントとgRPCサーバーの両方が参照するための構造体や関数が定義されており、ユーザーはこのファイルをそのまま使用すれば良い。

protoコマンドを実行し、以下のようなpb.goファイルをコンパイルできる。

# foo.protoファイルから、gRPCに対応するfoo.pb.goファイルをコンパイルする。
$ protoc -I=. --go_out=. --go-grpc_out=. foo.proto
// コメントアウトに元になった.protoファイルの情報が記載されている
//
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc             v3.21.12
// source: foo.proto

// 〜 中略 〜

func RegisterFooServiceServer(s *grpc.Server, srv FooServiceServer) {
    s.RegisterService(&_FooService_serviceDesc, srv)
}

// 〜 中略 〜

補足として、pb.goファイルには、gRPCサーバーとして登録するためのRegister<ファイル名>ServiceServer関数が定義される。


07. メタデータ

メタデータの操作

▼ Append

送信/受信するgRPCリクエストのコンテキストにメタデータを設定する。

コンテキストにメタデータがすでにある場合は追加し、もしなければメタデータを新しく作成する。

package main

import (
    "google.golang.org/grpc/metadata"
)

func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {

    ...

    // メタデータを作成する
    md := metadata.New(map[string]string{
        "Foo": "foo",
        "Bar": "bar",
    })

    // メタデータにキーを設定する
    md.Append("Baz", "baz")

    ...

}

▼ AppendToOutgoingContext

リクエスト送信用のコンテキストにメタデータとしてキーバリューを設定する。

コンテキストにメタデータがすでにある場合は追加し、もしなければメタデータを新しく作成する。

package main

import (
    "google.golang.org/grpc/metadata"
)

func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {

    ...

    // メタデータをコンテキストに設定する
    ctx = metadata.AppendToOutgoingContext(ctx, "Foo", "foo")

    ...

}

▼ FromIncomingContext

受信したgRPCリクエストのコンテキストからメタデータを取得する。

package main

import (
    "google.golang.org/grpc/metadata"
)

func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {

    ...

    // コンテキストにメタデータがあれば取得する
    md, ok := metadata.FromIncomingContext(ctx)

    // メタデータがなければ作成する
    if !ok {
        md = metadata.MD{}
    }

    log.Print(md)

    ...

}

内部的にはmdIncomingKeyというコンテキストキー名を指定している。

このキー名は、NewIncomingContext関数がメタデータ付きのコンテキスト作成時に設定する。

func ValueFromIncomingContext(ctx context.Context, key string) []string {

    ...

    md, ok := ctx.Value(mdIncomingKey{}).(MD)

    ...

}

func NewIncomingContext(ctx context.Context, md MD) context.Context {
    return context.WithValue(ctx, mdIncomingKey{}, md)
}

▼ FromOutgoingContext

送信するgRPCリクエストのコンテキストからメタデータを取得する。

package main

import (
    "google.golang.org/grpc/metadata"
)

func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {

    ...

    // コンテキストにメタデータがあれば取得する
    md, ok := metadata.FromOutgoingContext(ctx)

    // メタデータがなければ作成する
    if !ok {
        md = metadata.MD{}
    }

    ...
}

内部的にはmdIncomingKeyというコンテキストキー名を指定している。

このキー名は、NewOutgoingContext関数がメタデータ付きのコンテキスト作成時に設定する。

func ValueFromIncomingContext(ctx context.Context, key string) []string {

    ...

    md, ok := ctx.Value(mdIncomingKey{}).(MD)

    ...

}

func NewOutgoingContext(ctx context.Context, md MD) context.Context {
    return context.WithValue(ctx, mdOutgoingKey{}, rawMD{md: md})
}

▼ Get

送信/受信するgRPCリクエストのコンテキストからメタデータを取得する。

package main

import (
    "google.golang.org/grpc/metadata"
)

func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {

    ...

    // メタデータを作成する
    md := metadata.New(map[string]string{
        "Foo": "foo",
        "Bar": "bar",
    })

    // メタデータから値を取得する
    val = md.Get("foo")

    ...

}

▼ New

メタデータを作成する。

grpc-』から始まるキー名はgRPCで予約されている。

package main

import (
    "google.golang.org/grpc/metadata"
)

func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {

    ...

    // メタデータを作成する
    md := metadata.New(map[string]string{
        "Foo": "foo",
        "Bar": "bar",
    })

    ...

}

空のメタデータを作成する場合は、MD構造体を初期化しても良い。

package main

import (
    "google.golang.org/grpc/metadata"
)

func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {

    ...

    // メタデータを作成する
    md := metadata.MD{}

    ...

}

▼ NewIncomingContext

リクエスト受信用のコンテキストにメタデータを設定する。

コンテキストにメタデータがすでにある場合は置換するため、もしメタデータにキーを追加したい場合はAppendToOutgoingContext関数を使用する。

package main

import (
    "google.golang.org/grpc/metadata"
)

func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {

    ...

    // メタデータを作成する
    md := metadata.New(map[string]string{
        "Foo": "foo",
        "Bar": "bar",
    })

    // メタデータをコンテキストに設定する
    ctx = metadata.NewIncomingContext(ctx, md)

    ...

}

▼ NewOutgoingContext

リクエスト送信用のコンテキストにメタデータを設定する。

コンテキストにメタデータがすでにある場合は置換するため、もしメタデータにキーを追加したい場合はAppendToOutgoingContext関数を使用する。

package main

import (
    "google.golang.org/grpc/metadata"
)

func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {

    ...

    // メタデータを作成する
    md := metadata.New(map[string]string{
        "Foo": "foo",
        "Bar": "bar",
    })

    // メタデータをコンテキストに設定する
    ctx = metadata.NewOutgoingContext(ctx, md)

    ...

}

▼ Set

送信/受信するgRPCリクエストのコンテキストにメタデータを設定する。

package main

import (
    "google.golang.org/grpc/metadata"
)

func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {

    ...

    // メタデータを作成する
    md := metadata.New(map[string]string{
        "Foo": "foo",
        "Bar": "bar",
    })

    // メタデータにキーを設定する
    md.Set("Baz", "baz")

    ...

}


クライアントからサーバーに単項RPCを送信する場合

▼ クライアント側

クライアント側では、メタデータを設定する。

package main

import (
    "google.golang.org/grpc/metadata"
)

func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {

    // メタデータを作成する
    md := metadata.New(map[string]string{
        "Foo": "foo",
        "Bar": "bar",
    })

    // メタデータをコンテキストに設定する
    ctx = metadata.NewOutgoingContext(ctx, md)

    ...

}


▼ サーバー側

サーバー側では、メタデータを取得する。

package main

import (
    "google.golang.org/grpc/metadata"
)

func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {

    // コンテキストにメタデータがあれば取得する
    md, ok := metadata.FromIncomingContext(ctx)

    // メタデータがなければ作成する
    if !ok {
        md = metadata.MD{}
    }

    log.Print(md)

    ...

}


サーバーからクライアントに単項RPCを送信する場合

▼ サーバー側

package main

import (
    "google.golang.org/grpc/metadata"
)

func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {

    // メタデータを作成する
    headMd := metadata.New(map[string]string{
        "Foo": "foo",
        "Bar": "bar",
    })

    // メタデータをヘッダーに設定する
    if err := grpc.SetHeader(ctx, headerMD); err != nil {
        return nil, err
    }

    // メタデータをトレーラーに設定する
    if err := grpc.SetTrailer(ctx, trailerMD); err != nil {
        return nil, err
    }

    ...

}

▼ クライアント側

package main

import (
    "google.golang.org/grpc/metadata"
)

func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {

    var header, trailer metadata.MD

    // ヘッダーやトレーラーからメタデータを取得する
    res, err := client.Hello(
        ctx,
        req,
        grpc.Header(&header),
        grpc.Trailer(&trailer),
    )

    if err != nil {
        md = metadata.MD{}
    }

    log.Print(md)

    ...

}