GoでgRPCをササッと実装してみる 1

にあえん

November 17, 2024

こんにちは。ナナオです。

今回は触ろうと思って触ってなかったgRPCに入門してみようと思います。

gRPCとは?

gRPCとは、GoogleがRPCを実現するために作った技術のことです。

詳細については以下のサイトがかなり細かく説明してくれています。

作ってわかる! はじめてのgRPC

gRPCサーバーを動かす

まずはgRPCのリクエストとレスポンスを定義するためのprotoファイルを作成します。

適当なディレクトリを作成して、Goモジュールとして初期化しておきましょう。

mkdir go_grpc_playground && cd go_grpc_playground
go mod init go_grpc_playground

次に、protoファイルを作成します。

(参考にしてるサイトをパクってるだけですが。。)

// protoのバージョンの宣言
syntax = "proto3";

// protoファイルから自動生成させるGoのコードの置き先
option go_package = "pkg/grpc";

// packageの宣言
package myapp;

// サービスの定義
service GreetingService {
	// サービスが持つメソッドの定義
	rpc Hello (HelloRequest) returns (HelloResponse); 
}

// 型の定義
message HelloRequest {
	string name = 1;
}

message HelloResponse {
	string message = 1;
}

次にこのprotoファイルからgoファイルを作成するためのモジュールをインストールします。

brewがあれば以下のコマンドで一発インストールできます。

brew install protobuf

インストールするとprotocというコマンドが使用可能になります。

続けて、必要なモジュールを依存関係に追加しておきます。

go get -u google.golang.org/grpc
go get -u google.golang.org/grpc/cmd/protoc-gen-go-grpc

さて、今のディレクトリはこうなっています。

.
├── go.mod
├── go.sum
└── main.proto

この状態でprotocコマンドを使用してgoファイルを生成しようとすると…

❯ protoc main.proto
Missing output directives.

怒られてしまいました。

これはprotoファイルに定義したoption go_package = "pkg/grpc";pkg/grpcというディレクトリが存在しないからですね。

ディレクトリを作って再度挑戦してみます。

❯ mkdir pkg && mkdir pkg/grpc
❯ protoc main.proto
Missing output directives.

…どうやら関係なかったようです。

--go_outオプションを指定しないとダメっぽいですね。

protobufとgrpcのGoコード生成先ディレクトリの指定を、protocコマンドのオプションで行う

ということで、指定して再チャレンジ!

❯ protoc --go_out=./pkg/grpc main.proto;
protoc-gen-go: program not found or is not executable
Please specify a program using absolute path or make sure the program is available in your PATH system variable
--go_out: protoc-gen-go: Plugin failed with status code 1.

今度は違うエラーが出てしまいました。

protoc-gen-goというプログラムがインストールされていないからのようです。

これもbrewでインストールしておきましょう。

brew install protoc-gen-go

では改めて再チャレンジ!

protoc --go_out=./pkg/grpc main.proto

今度は何も出ないので成功したようです。

ディレクトリ構成を見てみましょう。

.
├── go.mod
├── go.sum
├── main.proto
└── pkg
    └── grpc
        └── pkg
            └── grpc
                └── main.pb.go

おお、できてる。

でもなんか変な感じで出来ちゃったので、一回消しましょう。

❯ rm -rf p
❯ protoc --go_out=. main.proto
❯ tree
.
├── go.mod
├── go.sum
├── main.proto
└── pkg
    └── grpc
        └── main.pb.go

うん、今度はいい感じ。

さて、このmain.pb.goの中身はどうなっているでしょう?

// protoのバージョンの宣言

// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// 	protoc-gen-go v1.35.2
// 	protoc        v5.28.3
// source: main.proto

// packageの宣言

package grpc

import (
	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
	reflect "reflect"
	sync "sync"
)

const (
	// Verify that this generated code is sufficiently up-to-date.
	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
	// Verify that runtime/protoimpl is sufficiently up-to-date.
	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)

// 型の定義
type HelloRequest struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}

func (x *HelloRequest) Reset() {
	*x = HelloRequest{}
	mi := &file_main_proto_msgTypes[0]
	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
	ms.StoreMessageInfo(mi)
}

func (x *HelloRequest) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*HelloRequest) ProtoMessage() {}

func (x *HelloRequest) ProtoReflect() protoreflect.Message {
	mi := &file_main_proto_msgTypes[0]
	if x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead.
func (*HelloRequest) Descriptor() ([]byte, []int) {
	return file_main_proto_rawDescGZIP(), []int{0}
}

func (x *HelloRequest) GetName() string {
	if x != nil {
		return x.Name
	}
	return ""
}

type HelloResponse struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}

func (x *HelloResponse) Reset() {
	*x = HelloResponse{}
	mi := &file_main_proto_msgTypes[1]
	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
	ms.StoreMessageInfo(mi)
}

func (x *HelloResponse) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*HelloResponse) ProtoMessage() {}

func (x *HelloResponse) ProtoReflect() protoreflect.Message {
	mi := &file_main_proto_msgTypes[1]
	if x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use HelloResponse.ProtoReflect.Descriptor instead.
func (*HelloResponse) Descriptor() ([]byte, []int) {
	return file_main_proto_rawDescGZIP(), []int{1}
}

func (x *HelloResponse) GetMessage() string {
	if x != nil {
		return x.Message
	}
	return ""
}

var File_main_proto protoreflect.FileDescriptor

var file_main_proto_rawDesc = []byte{
	0x0a, 0x0a, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x6d, 0x79,
	0x61, 0x70, 0x70, 0x22, 0x22, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75,
	0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
	0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x29, 0x0a, 0x0d, 0x48, 0x65, 0x6c, 0x6c, 0x6f,
	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73,
	0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61,
	0x67, 0x65, 0x32, 0x45, 0x0a, 0x0f, 0x47, 0x72, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x65,
	0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x13,
	0x2e, 0x6d, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75,
	0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x6d, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x48, 0x65, 0x6c, 0x6c,
	0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x0a, 0x5a, 0x08, 0x70, 0x6b, 0x67,
	0x2f, 0x67, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}

var (
	file_main_proto_rawDescOnce sync.Once
	file_main_proto_rawDescData = file_main_proto_rawDesc
)

func file_main_proto_rawDescGZIP() []byte {
	file_main_proto_rawDescOnce.Do(func() {
		file_main_proto_rawDescData = protoimpl.X.CompressGZIP(file_main_proto_rawDescData)
	})
	return file_main_proto_rawDescData
}

var file_main_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_main_proto_goTypes = []any{
	(*HelloRequest)(nil),  // 0: myapp.HelloRequest
	(*HelloResponse)(nil), // 1: myapp.HelloResponse
}
var file_main_proto_depIdxs = []int32{
	0, // 0: myapp.GreetingService.Hello:input_type -> myapp.HelloRequest
	1, // 1: myapp.GreetingService.Hello:output_type -> myapp.HelloResponse
	1, // [1:2] is the sub-list for method output_type
	0, // [0:1] is the sub-list for method input_type
	0, // [0:0] is the sub-list for extension type_name
	0, // [0:0] is the sub-list for extension extendee
	0, // [0:0] is the sub-list for field type_name
}

func init() { file_main_proto_init() }
func file_main_proto_init() {
	if File_main_proto != nil {
		return
	}
	type x struct{}
	out := protoimpl.TypeBuilder{
		File: protoimpl.DescBuilder{
			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
			RawDescriptor: file_main_proto_rawDesc,
			NumEnums:      0,
			NumMessages:   2,
			NumExtensions: 0,
			NumServices:   1,
		},
		GoTypes:           file_main_proto_goTypes,
		DependencyIndexes: file_main_proto_depIdxs,
		MessageInfos:      file_main_proto_msgTypes,
	}.Build()
	File_main_proto = out.File
	file_main_proto_rawDesc = nil
	file_main_proto_goTypes = nil
	file_main_proto_depIdxs = nil
}

…想像以上に複雑でしたね。

protoファイルに書かれていた内容を具現化するとこうなるということでしょう。

では次に、この定義からgRPCサーバーを起動させてみましょう。

package main

import (
	"fmt"
	"log"
	"net"
	"os"
	"os/signal"

	"google.golang.org/grpc"
)

func main() {
	// 1. 8080番portのListenerを作成
	port := 8080
	listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
	if err != nil {
		panic(err)
	}

	// 2. gRPCサーバーを作成
	s := grpc.NewServer()

	// 3. 作成したgRPCサーバーを、8080番ポートで稼働させる
	go func() {
		log.Printf("start gRPC server port: %v", port)
		s.Serve(listener)
	}()

	// 4.Ctrl+Cが入力されたらGraceful shutdownされるようにする
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, os.Interrupt)
	<-quit
	log.Println("stopping gRPC server...")
	s.GracefulStop()
}

これをmain.goで保存しておきます。

ただ、google.golang.org/grpcを依存関係に追加する際、Goのバージョンが>=1.22じゃないと怒られてしまったので、goenvでバージョンを固定しておきました。

goenv install 1.22.9
goenv local 1.22.9

また、go.modの定義も変更しておきます。

module go_grpc_playground

go 1.22 # 1.20 -> 1.22に変更

これでパッケージの追加がうまくいくはずです。

go add google.golang.org/grpc

ではサーバーを実行してみましょう。

❯ go run .
2024/11/18 22:37:56 start gRPC server port: 8080
^C2024/11/18 22:38:46 stopping gRPC server...

無事起動しました。

ただ今のままでは何もしないサーバーなので、ハンドラを追加します。

ここにきて気づいたのですが、サービスを実装した構造体が今のままだとないので追加します。

構造体を作成するために必要なパッケージをインストールします。

brew install protoc-gen-go-grpc

(私は一旦brewに統一していますが、go installでもできるっぽいです)

再度生成コマンドを実行します。その際にオプションとして--go-grpc_outをつけてあげないといけないみたいです。

protoc --go_out=. --go-grpc_out=. main.proto

これでサービスを実装した構造体が出来上がりました。

.
├── go.mod
├── go.sum
├── main.go
├── main.proto
└── pkg
    └── grpc
        ├── main.pb.go
        └── main_grpc.pb.go # このファイルが追加された

では動く形にしていきましょう。

package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"os"
	"os/signal"

	pb "go_grpc_playground/pkg/grpc"

	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
)

type myServer struct {
	pb.UnimplementedGreetingServiceServer
}

func NewMyServer() *myServer {
	return &myServer{}
}

func (s *myServer) Hello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
	// リクエストからnameフィールドを取り出して
	// "Hello, [名前]!"というレスポンスを返す
	return &pb.HelloResponse{
		Message: fmt.Sprintf("Hello, %s!", req.GetName()),
	}, nil
}

func main() {
	// 8080番portのListenerを作成
	port := 8080
	listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
	if err != nil {
		panic(err)
	}

	// gRPCサーバーを作成
	s := grpc.NewServer()

	// サービスを登録
	pb.RegisterGreetingServiceServer(s, NewMyServer())

	// gRPCurlのためにサーバーリフレクションを設定
	reflection.Register(s)

	go func() {
		log.Printf("start gRPC server port: %v", port)
		s.Serve(listener)
	}()

	// 4.Ctrl+Cが入力されたらGraceful shutdownされるようにする
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, os.Interrupt)
	<-quit
	log.Println("stopping gRPC server...")
	s.GracefulStop()
}

これで動く形になりました。

再度サーバーを実行します。

❯ go run .
2024/11/18 23:16:42 start gRPC server port: 8080

起動しました。

ではgRPCをcurlのように確認できるツールをインストールして、レスポンスを見てみます。

brew install grpcurl

では早速レスポンスが返却されるか見てみます。

❯ grpcurl -plaintext -d '{"name": "hsaki"}' localhost:8080 myapp.GreetingService.Hello
{
  "message": "Hello, hsaki!"
}

動いていますね!!!

感想

ということで、結構駆け足でしたが初めて動かしてみました。

次回はクライアントの作成をしてみるよ。お楽しみに~