🔖

connect-go(Getting Started)

2023/05/25に公開

connect-go

Getting started | Connect

新しいGoモジュールを作成し、いくつかのコード生成ツールをインストールする

shell
mkdir connect-go-example
cd connect-go-example
go mod init example
go install github.com/bufbuild/buf/cmd/buf@latest
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install github.com/bufbuild/connect-go/cmd/protoc-gen-connect-go@latest

Goのインストールディレクトリをパスに追加する:

shell
[ -n "$(go env GOBIN)" ] && export PATH="$(go env GOBIN):${PATH}"
[ -n "$(go env GOPATH)" ] && export PATH="$(go env GOPATH)/bin:${PATH}"

protoファイルを定義する

shell
mkdir -p greet/v1
touch greet/v1/greet.proto
greet.proto
syntax = "proto3";

package greet.v1;

option go_package = "example/gen/greet/v1;greetv1";

message GreetRequest {
  string name = 1;
}

message GreetResponse {
  string greeting = 1;
}

service GreetService {
  rpc Greet(GreetRequest) returns (GreetResponse) {}
}

このprotoファイルは、greet.v1パッケージ内にある、GreetServiceという名前のサービスを定義しています。proto3構文を使用しています。

まず、syntax = "proto3";という行は、このprotoファイルがproto3構文を使用していることを示しています。

次に、package greet.v1;という行は、このprotoファイルがgreet.v1パッケージに所属していることを示しています。

option go_package = "example/gen/greet/v1;greetv1";という行は、生成されるGoコードのパッケージ名とインポートパスを指定しています。具体的には、生成されるGoコードのパッケージはexample/gen/greet/v1となり、インポートパスにはgreetv1が使用されます。

message GreetRequestという行から始まるブロックは、GreetRequestというメッセージ型を定義しています。このメッセージ型は1つのフィールドを持ち、そのフィールドはstring型のnameで、タグ番号は1です。

同様に、message GreetResponseという行から始まるブロックは、GreetResponseというメッセージ型を定義しています。このメッセージ型も1つのフィールドを持ち、そのフィールドはstring型のgreetingで、タグ番号は1です。

最後に、service GreetServiceという行から始まるブロックは、GreetServiceという名前のサービスを定義しています。このサービスは1つのメソッドを持ち、そのメソッドはGreetRequestを受け取り、GreetResponseを返します。メソッド名はGreetです。

以上がこのprotoファイルの内容です。このファイルは、gRPCなどのプロトコルバッファコンパイラを使用して、各言語に対応したコードを生成するために使用されることがあります。

buf.yamlを作成する

shell
buf mod init

buf.yamlが生成される

次に、buf.gen.yamlを次のように作成する

buf.gen.yaml
version: v1
plugins:
  - plugin: go
    out: gen
    opt: paths=source_relative
  - plugin: connect-go
    out: gen
    opt: paths=source_relative

buf lint,buf generate コマンドは、protoファイルを変更するたびに実行します

shell
buf lint

buf lint コマンドを実行すると、Protocol Buffersのソースコードに対して様々なLintルールが適用され、問題がある場合にはそれらが報告されます。

shell
buf generate

buf generate コマンドを実行すると、指定された定義ファイルから生成されたコードが出力されます。生成されたコードは、各言語のベストプラクティスに従って設計されており、Protocol Buffersを使用するアプリケーション開発を簡素化します。

サーバーを実装する

server/main.go
package main

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

    "github.com/bufbuild/connect-go"
    "golang.org/x/net/http2"
    "golang.org/x/net/http2/h2c"

    greetv1 "example/gen/greet/v1" // generated by protoc-gen-go
    "example/gen/greet/v1/greetv1connect" // generated by protoc-gen-connect-go
)

type GreetServer struct{}

func (s *GreetServer) Greet(
    ctx context.Context,
    req *connect.Request[greetv1.GreetRequest],
) (*connect.Response[greetv1.GreetResponse], error) {
    log.Println("Request headers: ", req.Header())
    res := connect.NewResponse(&greetv1.GreetResponse{
        Greeting: fmt.Sprintf("Hello, %s!", req.Msg.Name),
    })
    res.Header().Set("Greet-Version", "v1")
    return res, nil
}

func main() {
    greeter := &GreetServer{}
    mux := http.NewServeMux()
    path, handler := greetv1connect.NewGreetServiceHandler(greeter)
    mux.Handle(path, handler)
    http.ListenAndServe(
        "localhost:8080",
        // Use h2c so we can serve HTTP/2 without TLS.
        h2c.NewHandler(mux, &http2.Server{}),
    )
}

このmain.goファイルは、Protocol Buffersのgreet.protoファイルで定義されたGreetServiceの実装を含む、Go言語のサーバーアプリケーションのコードです。

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

    "github.com/bufbuild/connect-go"
    "golang.org/x/net/http2"
    "golang.org/x/net/http2/h2c"

    greetv1 "example/gen/greet/v1" // generated by protoc-gen-go
    "example/gen/greet/v1/greetv1connect" // generated by protoc-gen-connect-go
)

必要なパッケージをインポートしています。contextはコンテキスト操作のため、fmtはフォーマット処理のため、logはログ出力のためのパッケージです。net/httpはHTTPサーバーを作成するために使用され、http2h2cはHTTP/2サポートを提供します。

github.com/bufbuild/connect-goパッケージは、Protocol Buffersのメッセージを扱うためのヘルパー関数と機能を提供します。また、example/gen/greet/v1は、protoc-gen-goによって生成されたGreetServiceのGo言語バインディングを示しています。同様に、example/gen/greet/v1/greetv1connectは、protoc-gen-connect-goによって生成されたGreetServiceのハンドラを示しています。

type GreetServer struct{}

GreetServerという構造体型を定義しています。この構造体はGreetServiceの実装を担当します。

func (s *GreetServer) Greet(
    ctx context.Context,
    req *connect.Request[greetv1.GreetRequest],
) (*connect.Response[greetv1.GreetResponse], error) {
    log.Println("Request headers: ", req.Header())
    res := connect.NewResponse(&greetv1.GreetResponse{
        Greeting: fmt.Sprintf("Hello, %s!", req.Msg.Name),
    })
    res.Header().Set("Greet-Version", "v1")
    return res, nil
}

GreetメソッドがGreetServer構造体に定義されています。このメソッドはGreetRequestを受け取り、GreetResponseを返します。メソッド内では、リクエストヘッダーをログ出力し、GreetResponseを作成して返します。具体的には、リクエストメッセージのnameフィールドを使用して応答メッセージを生成し、ヘッダー

にカスタムフィールドGreet-Versionを設定します。

func main() {
    greeter := &GreetServer{}
    mux := http.NewServeMux()
    path, handler := greetv1connect.NewGreetServiceHandler(greeter)
    mux.Handle(path, handler)
    http.ListenAndServe(
        "localhost:8080",
        // Use h2c so we can serve HTTP/2 without TLS.
        h2c.NewHandler(mux, &http2.Server{}),
    )
}

メイン関数が定義されています。ここでは、GreetServerのインスタンスを作成し、HTTPサーバーを設定します。http.NewServeMux()で新しいルーターを作成し、greetv1connect.NewGreetServiceHandler(greeter)を使用してGreetServiceのハンドラを取得し、ルーターに登録します。最後に、http.ListenAndServe()を使用してサーバーを開始します。h2c.NewHandler()は、HTTP/2を使用するためにハンドラをラップするために使用されています。

これにより、Protocol Buffersで定義されたGreetServiceの実装が行われたGo言語のサーバーアプリケーションが作成されます。このアプリケーションは、localhost:8080でHTTP/2プロトコルを使用してリクエストを受け付け、Greetメソッドの処理を行い、応答を返します。

リクエストを作成する

shell(Request)
curl \
    --header "Content-Type: application/json" \
    --data '{"name": "Jane"}' \
    http://localhost:8080/greet.v1.GreetService/Greet
shell(Response)
{"greeting": "Hello, Jane!"}

クライアントを実装する

client/main.go
package main

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

    greetv1 "example/gen/greet/v1"
    "example/gen/greet/v1/greetv1connect"

    "github.com/bufbuild/connect-go"
)

func main() {
    client := greetv1connect.NewGreetServiceClient(
        http.DefaultClient,
        "http://localhost:8080",
    )
    res, err := client.Greet(
        context.Background(),
        connect.NewRequest(&greetv1.GreetRequest{Name: "Jane"}),
    )
    if err != nil {
        log.Println(err)
        return
    }
    log.Println(res.Msg.Greeting)
}

このmain.goファイルは、Protocol Buffersのgreet.protoファイルで定義されたGreetServiceのクライアントアプリケーションのコードです。以下でコードの各部分を解説します。

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

    greetv1 "example/gen/greet/v1"
    "example/gen/greet/v1/greetv1connect"

    "github.com/bufbuild/connect-go"
)

必要なパッケージをインポートしています。contextはコンテキスト操作のため、logはログ出力のため、net/httpはHTTPクライアントを作成するためのパッケージです。

example/gen/greet/v1パッケージは、protoc-gen-goによって生成されたGreetServiceのGo言語バインディングを示しています。同様に、example/gen/greet/v1/greetv1connectパッケージは、protoc-gen-connect-goによって生成されたGreetServiceのクライアントを示しています。

func main() {
    client := greetv1connect.NewGreetServiceClient(
        http.DefaultClient,
        "<http://localhost:8080>",
    )
    res, err := client.Greet(
        context.Background(),
        connect.NewRequest(&greetv1.GreetRequest{Name: "Jane"}),
    )
    if err != nil {
        log.Println(err)
        return
    }
    log.Println(res.Msg.Greeting)
}

メイン関数が定義されています。ここでは、greetv1connect.NewGreetServiceClient()を使用してGreetServiceのクライアントを作成し、指定されたエンドポイント("http://localhost:8080")に接続します。

client.Greet()メソッドを呼び出してGreetリクエストを送信し、レスポンスを受け取ります。リクエストはcontext.Background()を使用した空のコンテキストと、connect.NewRequest()を使用して作成されたGreetRequestメッセージを含んでいます(ここでは名前を"Jane"としています)。

エラーチェックを行い、エラーが発生した場合はログに出力します。エラーがない場合は、レスポンスメッセージのGreetingフィールドをログに出力します。

これにより、Protocol Buffersで定義されたGreetServiceのクライアントが作成され、指定されたエンドポイントにGreetリクエストが送信され、レスポンスが受け取られます。

WithGRPCオプションを使用する

client/main.go
client := greetv1connect.NewGreetServiceClient(
  http.DefaultClient,
  "http://localhost:8080",
  connect.WithGRPC(),
)

**connect.WithGRPC()**は、gRPCを使用して通信するためのオプションを設定するために使用されます。

通常、Protocol Buffersで生成されたクライアントは、HTTPを介してサーバーと通信しますが、このオプションを指定することで、gRPCを使用して通信するように設定されます。gRPCは、高性能で効率的なRPC(Remote Procedure Call)フレームワークであり、Protocol Buffersと統合されています。

したがって、**WithGRPC()**オプションを使用することで、クライアントはgRPCを介してサーバーと通信し、gRPCプロトコルを使用してデータをやり取りします。

このように、オプションを使用することで、GreetServiceのクライアントの動作をカスタマイズすることができます。**WithGRPC()**オプションを指定することで、gRPCを使用して通信する設定が行われます。

Routing

server/main.go
package main

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

    "github.com/bufbuild/connect-go"

    greetv1 "example/gen/greet/v1" // generated by protoc-gen-go
    "example/gen/greet/v1/greetv1connect" // generated by protoc-gen-connect-go
)

type GreetServer struct{}

func (s *GreetServer) Greet(
    ctx context.Context,
    req *connect.Request[greetv1.GreetRequest],
) (*connect.Response[greetv1.GreetResponse], error) {
    log.Println("Request headers: ", req.Header())
    res := connect.NewResponse(&greetv1.GreetResponse{
        Greeting: fmt.Sprintf("Hello, %s!", req.Msg.Name),
    })
    res.Header().Set("Greet-Version", "v1")
    return res, nil
}

func main() {
    api := http.NewServeMux()
    api.Handle(greetv1connect.NewGreetServiceHandler(&GreetServer{}))

    mux := http.NewServeMux()
    mux.Handle("/grpc/", http.StripPrefix("/grpc", api))
    http.ListenAndServe(":http", mux)
}

Discussion