こんにちは。KOUKIです。
タイトル通り、Go言語とgRPCとDockerを使って、Todoアプリを作ってみようと思います。
今回は、フロントエンド部分は作成しません。いずれはVueJSで作りたいと思いますが、本記事ではバックエンドのみ作成します。
なお、データベースはMongoDBを使用します。
Go言語からMongoへのアクセスは、「mongo」パッケージを使用します。
<目次>
gRPCのあれこれ
gRPCについては、以下の記事を参考にしてください。
開発に必要な環境構築についても触れています。
事前準備
本記事で作成するアプリケーションは、Mac上で動作確認をします。
また、以下のモジュールをインストールしてください。
1 2 3 4 5 6 |
go version go version go1.14 darwin/amd64 # モジュール go get -u google.golang.org/grpc go get -u github.com/golang/protobuf/protoc-gen-go |
加えて、protocをインストールする必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# how to install protoc? # ---- mac ---- brew install protobuf # ---- Linux ---- https://github.com/google/protobuf/releases # 最新バージョンを選択すること curl -OL https://github.com/google/protobuf/releases/download/v3.5.1/protoc-3.5.1-linux-x86_64.zip # Unzip unzip protoc-3.5.1-linux-x86_64.zip -d protoc3 # Move protoc to /usr/local/bin/ sudo mv protoc3/bin/* # ---- Windows ---- # ここからダウンロード https://github.com/google/protobuf/releases 例) https://github.com/google/protobuf/releases/download/v3.5.1/protoc-3.5.1-win32.zip # 以下にインストールされる C:\proto3 # 以下ができてるか確かめる C:\proto3\bin C:\proto3\include # 環境変数に以下のパスを追加 C:\proto3\bin 例) C:\Program Files; C:\Winnt; ...... ; C:\proto3\bin |
protocはあとで使うので、その時何をするか見てください。
プロジェクトの準備
まずは、プロジェクトを作成しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
mkdir todo-app cd todo-app/ touch Dockerfile touch docker-compose.yml mkdir api mkdir api/todopb touch api/todopb/todo.proto mkdir api/client touch api/client/client.go mkdir api/server touch api/server/server.go touch generate.sh touch Makefile |
たくさん作成しましたね。。ファイルの説明は、後ほど行います。
Dockerの設定
Dockerの設定を行いましょう。まずは、メインとなるGo言語の環境をDockerfileに記述しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Dockerfile FROM golang:latest ENV SRC_DIR=/go/src/github.com/<user>/todo-app/api WORKDIR $SRC_DIR ADD ./api $SRC_DIR RUN go get -u google.golang.org/grpc \ && go get -u github.com/golang/protobuf/protoc-gen-go \ && go get go.mongodb.org/mongo-driver/mongo |
「<user>」の部分は適宜書き換えてください。
このDockerfileから以下のコマンドで、docker imageを作成します。
1 |
docker build -t todo:v1 . |
続いて、docker-compose.ymlファイルを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
version: "3" services: server: image: todo:v1 ports: - "50051:50051" container_name: server command: bash -c "go run server/server.go" depends_on: - mongo volumes: - ./api:/go/src/github.com/user/todo-app/api client: image: todo:v1 container_name: client depends_on: - server volumes: - ./api:/go/src/github.com/user/todo-app/api mongo: image: mongo container_name: mongo |
DBには、MongoDBを使いました。
こちらのvolumesの「user」も適宜変えてください。
また、今回はフロントエンドの実装を行わない為、データのやり取りは二つのコンテナを立てて行います。それが、clientコンテナとserverコンテナです。
起動に必要なファイルを作成していないので、あとでdockerコンテナが立ち上がるか確認します。
Todoアプリの作成
準備が長くなりましたが、いよいよTodoアプリの作成に取り掛かります!
Todoサーバーの構築
さて、最初にTodoサーバーの構築を行いましょう。
todo.protoファイルに以下の実装をしてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// api/todopb/todo.proto syntax = "proto3"; package api; option go_package = "api/todopb"; // MongoDBのデータベースと合わせる message Todo { string id = 1; string author_id = 2; string title = 3; string content = 4; } // TodoServieを定義 service TodoService {} |
続いて、generate.shに以下の設定を行います。
1 |
protoc api/todopb/todo.proto --go_out=plugins=grpc:. |
ここでprotocが出てきました。このコマンドは、protoファイルからGo言語用のgRPCコードを自動生成してくれます。
terminal上で、以下のコマンドを実行しましょう。
1 2 3 4 5 6 7 |
chmod 777 generate.sh ./generate.sh $ tree api/todopb/ api/todopb/ ├── todo.pb.go // 新規に作成される └── todo.proto |
続いて、Server側のコードを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
ackage main import ( "fmt" "log" "net" "os" "os/signal" "github.com/user/todo-app/api/todopb" "google.golang.org/grpc" ) func checkErr(message string, err error) { if err != nil { log.Fatalf(message, err) } } func init() { log.SetFlags(log.LstdFlags | log.Lshortfile) } type server struct{} func main() { fmt.Println("=======Todo API Start====") // 50051ポートで起動する(docker-compose.ymlと合わせる) lis, err := net.Listen("tcp", "0.0.0.0:50051") checkErr("Failed to listen: %v", err) // grpcのサーバーを作成 s := grpc.NewServer() // TodoServiceServerにマウント todopb.RegisterTodoServiceServer(s, &server{}) go func() { fmt.Println("Starting Server...") err = s.Serve(lis) checkErr("faild to server: %v", err) }() // Ctrl + cでプログラムから抜けられるようにする ch := make(chan os.Signal, 1) signal.Notify(ch, os.Interrupt) // シグナルが受け取れるまで、ブロック <-ch fmt.Println("Stopping the server") s.Stop() fmt.Println("Closing the listener") lis.Close() fmt.Println("=====Todo API End====") } |
この状態で、以下のコマンドを実行してみてください。
1 2 3 4 |
docker-compose up server | =======Todo API Start==== server | Starting Server... |
Starting Serverが出力されていればOKです。
データ登録処理
データ登録処理を実装しましょう。
まずは、プロトファイルを更新します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// api/todopb/todo.proto syntax = "proto3"; package api; option go_package = "api/todopb"; message Todo { string id = 1; string author_id = 2; string title = 3; string content = 4; } message CreateTodoRequest { Todo todo = 1; } message CreateTodoResponse { Todo todo = 1; } service TodoService { rpc CreateTodo(CreateTodoRequest) returns (CreateTodoResponse) {} } |
CreateTodoとCreateTodoRequest、CreateTodoResponseを追加しました。
protoファイルを書き換えたので、以下のgrpcコードも更新しましょう。
1 |
./generate.sh |
serverコードにMongoDBに接続する処理を書きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
// server/server.go package main import ( "context" "fmt" "log" "net" "os" "os/signal" "github.com/user/todo-app/api/todopb" objectid "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "google.golang.org/grpc" ) func checkErr(message string, err error) { if err != nil { log.Fatalf(message, err) } } func init() { log.SetFlags(log.LstdFlags | log.Lshortfile) } type server struct{} // Mongoのコレクションを設定 var collection *mongo.Collection // Toodのアイテムを設定 type todoItem struct { ID objectid.ObjectID `bson: "_id,omitempty` AuthorID string `bson:"author_id"` Content string `bson:"content"` Title string `bson:"title"` } func main() { fmt.Println("=======Todo API Start====") // Connecting Mongo // mongoは、docker-compose.ymlのservice名 client, err := mongo.NewClient(options.Client().ApplyURI("mongodb://mongo:27017")) checkErr("Faile to create mongo client: %v", err) err = client.Connect(context.Background()) checkErr("Cannot connect to mongo db: %v", err) collection = client.Database("mydb").Collection("blog") // 50051ポートで起動する(docker-compose.ymlと合わせる) lis, err := net.Listen("tcp", "0.0.0.0:50051") checkErr("Failed to listen: %v", err) // grpcのサーバーを作成 s := grpc.NewServer() // TodoServiceServerにマウント todopb.RegisterTodoServiceServer(s, &server{}) go func() { fmt.Println("Starting Server...") err = s.Serve(lis) checkErr("faild to server: %v", err) }() // Ctrl + cでプログラムから抜けられるようにする ch := make(chan os.Signal, 1) signal.Notify(ch, os.Interrupt) // シグナルが受け取れるまで、ブロック <-ch fmt.Println("Stopping the server") s.Stop() fmt.Println("Closing the listener") lis.Close() fmt.Println("=====Todo API End====") } |
続いて、先ほど作成したCreateTodoサービスを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
// server/server.go ... func (*server) CreateTodo(ctx context.Context, req *todopb.CreateTodoRequest) (*todopb.CreateTodoResponse, error) { fmt.Printf("Create Todo Item with %v\n", req) // requestにあるTodoデータを取得 todo := req.GetTodo() data := todoItem{ AuthorID: todo.GetAuthorId(), Title: todo.GetTitle(), Content: todo.GetContent(), } // MongoDBへデータを挿入 res, err := collection.InsertOne(context.Background(), data) if err != nil { return nil, status.Errorf( codes.Internal, fmt.Sprintf("Internal error: %v", err), ) } // Mongoで自動生成されるidを取得 oid, ok := res.InsertedID.(objectid.ObjectID) if !ok { return nil, status.Errorf( codes.Internal, fmt.Sprintf("Cannot convert to OID"), ) } data.ID = oid return &todopb.CreateTodoResponse{ Todo: dataToTodoPb(&data), }, nil } func dataToTodoPb(data *todoItem) *todopb.Todo { return &todopb.Todo{ Id: data.ID.Hex(), AuthorId: data.AuthorID, Title: data.Title, Content: data.Content, } } |
次にclient側のコードを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
/ client/client.go package main import ( "context" "fmt" "log" "github.com/user/todo-app/api/todopb" "google.golang.org/grpc" ) func checkError(message string, err error) { if err != nil { log.Fatalf("message", err) } } func createTodoHandler(c todopb.TodoServiceClient) { fmt.Println("Creating the todo") // Todoデータを作成 todo := &todopb.Todo{ AuthorId: "selfnote", Title: "First Post", Content: "Fist Post for US!", } createTodoRes, err := c.CreateTodo(context.Background(), &todopb.CreateTodoRequest{Todo: todo}) checkError("Failt to create todo data: %v\n", err) fmt.Printf("Todo has been created: %v\n", createTodoRes) } func main() { opts := grpc.WithInsecure() // serverは、docker-compose.ymlに定義してあるサービス名 cc, err := grpc.Dial("server:50051", opts) checkError("could not connect: %v\n", err) defer cc.Close() c := todopb.NewTodoServiceClient(cc) // Create Todo createTodoHandler(c) } |
Makefileに以下のコマンドを登録してください。
1 2 3 4 5 |
.PHONY: client client: docker-compose run --rm client bash -c "go run client/client.go" |
上記は、clientコンテナを起動するためのコマンドです。
最後に、次のコマンドをterminal上で実行します。
1 2 3 4 5 6 |
$ make client docker-compose run --rm client bash -c "go run client/client.go" Starting mongo ... done Starting server ... done Creating the todo Todo has been created: todo:{id:"5f2e6040e2c3f63a535b7804" author_id:"selfnote" title:"First Post" content:"Fist Post for US!"} |
問題なくデータが登録されました!
データ読取処理
続いて、データ読取サービスを作成しましょう。
まずは、protoファイルの編集です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// api/todopb/todo.proto syntax = "proto3"; package api; option go_package = "api/todopb"; message Todo { string id = 1; string author_id = 2; string title = 3; string content = 4; } message CreateTodoRequest { Todo todo = 1; } message CreateTodoResponse { Todo todo = 1; } message ReadTodoRequest { string todo_id = 1; } message ReadTodoResponse { Todo todo = 1; } service TodoService { rpc CreateTodo(CreateTodoRequest) returns (CreateTodoResponse) {} rpc ReadTodo(ReadTodoRequest) returns (ReadTodoResponse) {} } |
ReadTodoサービスとReadTodoRequest、ReadTodoResponseを追加しました。
以下のコマンドで、gRPCコードを更新しましょう。
1 |
./generage.sh |
続いて、server側のコードを更新します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
func (*server) ReadTodo(ctx context.Context, req *todopb.ReadTodoRequest) (*todopb.ReadTodoResponse, error) { fmt.Printf("Read Todo Item with %v\n", req) id := req.GetTodoId() oid, err := objectid.ObjectIDFromHex(id) if err != nil { return nil, status.Errorf( codes.InvalidArgument, fmt.Sprintf("Cannot parse ID"), ) } todo := &todoItem{} filter := objectid.D{{"_id", oid}} if err := collection.FindOne(context.Background(), filter).Decode(todo); err != nil { return nil, status.Errorf( codes.NotFound, fmt.Sprintf("Cannot find todo with specified ID: %v\n", err), ) } todo.ID = oid return &todopb.ReadTodoResponse{ Todo: dataToTodoPb(todo), }, nil } |
ReadTodoメソッドを追加しました。
続いて、clientコードの更新を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
// client/client.go package main import ( "context" "fmt" "log" "github.com/user/todo-app/api/todopb" "google.golang.org/grpc" ) func checkError(message string, err error) { if err != nil { log.Fatalf("message", err) } } func createTodoHandler(c todopb.TodoServiceClient) string { fmt.Println("Creating the todo") // Todoデータを作成 todo := &todopb.Todo{ AuthorId: "selfnote", Title: "First Post", Content: "Fist Post for US!", } createTodoRes, err := c.CreateTodo(context.Background(), &todopb.CreateTodoRequest{Todo: todo}) checkError("Failt to create todo data: %v\n", err) fmt.Printf("Todo has been created: %v\n", createTodoRes) return createTodoRes.GetTodo().GetId() } func readTodoHandler(c todopb.TodoServiceClient, id string) { fmt.Println("Read the todo with id") readTodoRes, err := c.ReadTodo(context.Background(), &todopb.ReadTodoRequest{TodoId: id}) checkError("Error happend while reading: %v\n", err) fmt.Printf("Todo was read: %v\n", readTodoRes) } func main() { opts := grpc.WithInsecure() // serverは、docker-compose.ymlに定義してあるサービス名 cc, err := grpc.Dial("server:50051", opts) checkError("could not connect: %v\n", err) defer cc.Close() c := todopb.NewTodoServiceClient(cc) // Create Todo todoId := createTodoHandler(c) readTodoHandler(c, todoId) } |
readTodoHandler関数を追加しました。この関数は、createTodoHandlerにて生成したIDを使用するので、createTodoHandler関数ではIDを戻り値として返すように修正しました。
以下のコマンドを実行します。
1 2 3 4 5 6 7 8 |
$ make client docker-compose run --rm client bash -c "go run client/client.go" Starting mongo ... done Starting server ... done Creating the todo Todo has been created: todo:{id:"5f2f33932b4f09a8bd09c482" author_id:"selfnote" title:"First Post" content:"Fist Post for US!"} Read the todo with id Todo was read: todo:{id:"5f2f33932b4f09a8bd09c482" author_id:"selfnote" title:"First Post" content:"Fist Post for US!"} |
OKですね。
データ更新処理
続いて、データ更新処理を実装します。
protoファイルを更新しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
... message UpdateTodoRequest { Todo todo = 1; } message UpdateTodoResponse { Todo todo = 1; } service TodoService { rpc CreateTodo(CreateTodoRequest) returns (CreateTodoResponse) {} rpc ReadTodo(ReadTodoRequest) returns (ReadTodoResponse) {} rpc UpdateTodo(UpdateTodoRequest) returns (UpdateTodoResponse) {} } |
以下のコマンドでコンパイルします。
1 |
./generate.sh |
続いて、serverコードを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
func (*server) UpdateTodo(ctx context.Context, req *todopb.UpdateTodoRequest) (*todopb.UpdateTodoResponse, error) { fmt.Printf("Update Todo Item with %v\n", req) todo := req.GetTodo() oid, err := objectid.ObjectIDFromHex(todo.GetId()) if err != nil { return nil, status.Errorf( codes.InvalidArgument, fmt.Sprintf("Cannot parse ID"), ) } data := &todoItem{} filter := objectid.D{{"_id", oid}} if err := collection.FindOne(context.Background(), filter).Decode(data); err != nil { return nil, status.Errorf( codes.NotFound, fmt.Sprintf("Cannot find todo with specified ID: %v\n", err), ) } data.ID = oid data.AuthorID = todo.GetAuthorId() data.Title = todo.GetTitle() data.Content = todo.GetContent() _, updateErr := collection.ReplaceOne(context.Background(), filter, data) if updateErr != nil { return nil, status.Errorf( codes.InvalidArgument, fmt.Sprintf("Cannot update object in MongoDB: %v", updateErr), ) } return &todopb.UpdateTodoResponse{ Todo: dataToTodoPb(data), }, nil } |
続いて、clientコードを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
func updateTodoHandler(c todopb.TodoServiceClient, id string) { fmt.Println("Update the todo with id") newTodo := &todopb.Todo{ Id: id, AuthorId: "Change Author", Title: "First Post(edit)", Content: "First Post for US!(edit)", } updateRes, updateErr := c.UpdateTodo(context.Background(), &todopb.UpdateTodoRequest{Todo: newTodo}) checkError("Error happend while updating: %v\n", updateErr) fmt.Printf("Blog was updated: %v\n", updateRes) } func main() { opts := grpc.WithInsecure() // serverは、docker-compose.ymlに定義してあるサービス名 cc, err := grpc.Dial("server:50051", opts) checkError("could not connect: %v\n", err) defer cc.Close() c := todopb.NewTodoServiceClient(cc) // Create Todo todoId := createTodoHandler(c) readTodoHandler(c, todoId) updateTodoHandler(c, todoId) } |
次のコマンドにて、動作確認をしましょう。
1 2 3 4 5 6 7 8 9 10 |
$ make client docker-compose run --rm client bash -c "go run client/client.go" Starting mongo ... done Starting server ... done Creating the todo Todo has been created: todo:{id:"5f2f3bcee46d31225b5422da" author_id:"selfnote" title:"First Post" content:"Fist Post for US!"} Read the todo with id Todo was read: todo:{id:"5f2f3bcee46d31225b5422da" author_id:"selfnote" title:"First Post" content:"Fist Post for US!"} Update the todo with id Blog was updated: todo:{id:"5f2f3bcee46d31225b5422da" author_id:"Change Author" title:"First Post(edit)" content:"First Post for US!(edit)"} |
データ削除処理
続いて、削除サービスを実装しましょう。
protoファイルを編集します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
message DeleteTodoRequest { string todo_id = 1; } message DeleteTodoResponse { string todo_id = 1; } service TodoService { rpc CreateTodo(CreateTodoRequest) returns (CreateTodoResponse) {} rpc ReadTodo(ReadTodoRequest) returns (ReadTodoResponse) {} rpc UpdateTodo(UpdateTodoRequest) returns (UpdateTodoResponse) {} rpc DeleteTodo(DeleteTodoRequest) returns (DeleteTodoResponse) {} } |
以下のコマンドで、コンパイルします。
1 |
./generate.sh |
serverコードを編集します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
func (*server) DeleteTodo(ctx context.Context, req *todopb.DeleteTodoRequest) (*todopb.DeleteTodoResponse, error) { fmt.Printf("Delete todo request with %v\n", req) oid, err := objectid.ObjectIDFromHex(req.GetTodoId()) if err != nil { return nil, status.Errorf( codes.InvalidArgument, fmt.Sprintf("Cannot parse ID"), ) } filter := objectid.D{{"_id", oid}} res, err := collection.DeleteOne(context.Background(), filter) if err != nil { return nil, status.Errorf( codes.Internal, fmt.Sprintf("Cannot delete object in MongoDB: %v", err), ) } if res.DeletedCount == 0 { return nil, status.Errorf( codes.NotFound, fmt.Sprintf("Cannot find todo in MongoDB: %v", err), ) } return &todopb.DeleteTodoResponse{TodoId: req.GetTodoId()}, nil } |
続いて、clientコードを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
func deleteTodoHandler(c todopb.TodoServiceClient, id string) { fmt.Println("Delete the todo with id") deleteRes, deleteErr := c.DeleteTodo( context.Background(), &todopb.DeleteTodoRequest{TodoId: id}) checkError("Error happend while deleting: %v\n", deleteErr) fmt.Printf("Todo was deleted: %v\n", deleteRes) } func main() { opts := grpc.WithInsecure() // serverは、docker-compose.ymlに定義してあるサービス名 cc, err := grpc.Dial("server:50051", opts) checkError("could not connect: %v\n", err) defer cc.Close() c := todopb.NewTodoServiceClient(cc) // Create Todo todoId := createTodoHandler(c) readTodoHandler(c, todoId) updateTodoHandler(c, todoId) deleteTodoHandler(c, todoId) } |
動作確認をしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 |
$ make client docker-compose run --rm client bash -c "go run client/client.go" Starting mongo ... done Starting server ... done Creating the todo Todo has been created: todo:{id:"5f308ea443386aea7f63d485" author_id:"selfnote" title:"First Post" content:"Fist Post for US!"} Read the todo with id Todo was read: todo:{id:"5f308ea443386aea7f63d485" author_id:"selfnote" title:"First Post" content:"Fist Post for US!"} Update the todo with id Blog was updated: todo:{id:"5f308ea443386aea7f63d485" author_id:"Change Author" title:"First Post(edit)" content:"First Post for US!(edit)"} Delete the todo with id Todo was deleted: todo_id:"5f308ea443386aea7f63d485" |
OKですね。
データ取得処理
最後に、Mongoからデータの一覧を取得するサービスを実装しましょう。
最初にprotoファイルを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
message ListTodoRequest {} message ListTodoResponse { Todo todo = 1; } service TodoService { rpc CreateTodo(CreateTodoRequest) returns (CreateTodoResponse) {} rpc ReadTodo(ReadTodoRequest) returns (ReadTodoResponse) {} rpc UpdateTodo(UpdateTodoRequest) returns (UpdateTodoResponse) {} rpc DeleteTodo(DeleteTodoRequest) returns (DeleteTodoResponse) {} rpc ListTodo(ListTodoRequest) returns (stream ListTodoResponse) {} } |
今までとは違って、ListTodoResponseにはstreamキーワードを追加しています。これは、サーバー側からクライアントへ複数のデータを送信することを示しています。データは一気に送信されるわけではなく、データが取得されたタイミングで送信されます。
以下のコマンドで、コンパイルしましょう。
1 |
./generate.sh |
続いて、server側のコードを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
func (*server) ListTodo(req *todopb.ListTodoRequest, stream todopb.TodoService_ListTodoServer) error { fmt.Printf("List todo request with %v\n", req) cur, err := collection.Find(context.Background(), objectid.D{}) if err != nil { return status.Errorf( codes.Internal, fmt.Sprintf("Unkown internal error: %v", err), ) } defer cur.Close(context.Background()) for cur.Next(context.Background()) { data := &todoItem{} err := cur.Decode(data) if err != nil { return status.Errorf( codes.Internal, fmt.Sprintf("Error while decoding data from MongoDB: %v", err), ) } // Straming 配信 stream.Send(&todopb.ListTodoResponse{Todo: dataToTodoPb(data)}) } if err := cur.Err(); err != nil { return status.Errorf( codes.Internal, fmt.Sprintf("Unkown internal error: %v", err), ) } return nil } |
また、clientコードを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
unc listTodoHandler(c todopb.TodoServiceClient) { fmt.Println("List the todo") stream, err := c.ListTodo(context.Background(), &todopb.ListTodoRequest{}) checkError("error while calling ListTodo RPC: %v\n", err) for { res, err := stream.Recv() if err == io.EOF { break } checkError("Something happend: %v\n", err) fmt.Println(res.GetTodo()) } } func main() { opts := grpc.WithInsecure() // serverは、docker-compose.ymlに定義してあるサービス名 cc, err := grpc.Dial("server:50051", opts) checkError("could not connect: %v\n", err) defer cc.Close() c := todopb.NewTodoServiceClient(cc) // Create Todo todoId := createTodoHandler(c) readTodoHandler(c, todoId) updateTodoHandler(c, todoId) listTodoHandler(c) deleteTodoHandler(c, todoId) } |
動作確認をしてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
make client docker-compose run --rm client bash -c "go run client/client.go" Starting mongo ... done Starting server ... done Creating the todo Todo has been created: todo:{id:"5f3092705e7b98127c8b706b" author_id:"selfnote" title:"First Post" content:"Fist Post for US!"} Read the todo with id Todo was read: todo:{id:"5f3092705e7b98127c8b706b" author_id:"selfnote" title:"First Post" content:"Fist Post for US!"} Update the todo with id Blog was updated: todo:{id:"5f3092705e7b98127c8b706b" author_id:"Change Author" title:"First Post(edit)" content:"First Post for US!(edit)"} List the todo id:"000000000000000000000000" author_id:"selfnote" title:"First Post" content:"Fist Post for US!" id:"000000000000000000000000" author_id:"selfnote" title:"First Post" content:"Fist Post for US!" id:"000000000000000000000000" author_id:"selfnote" title:"First Post" content:"Fist Post for US!" id:"000000000000000000000000" author_id:"selfnote" title:"First Post" content:"Fist Post for US!" id:"000000000000000000000000" author_id:"selfnote" title:"First Post" content:"Fist Post for US!" id:"000000000000000000000000" author_id:"Change Author" title:"First Post" content:"Fist Post for US!" id:"5f2f394b87d7e52d4caf0e67" author_id:"Change Author" title:"First Post" content:"Fist Post for US!" id:"5f2f39aae665d5ef5cf6f180" author_id:"Change Author" title:"First Post" content:"Fist Post for US!" id:"5f2f3a47186a15adde4bb73f" author_id:"Change Author" title:"First Post" content:"Fist Post for US!" id:"5f2f3b127cc32bb2fa84f0cf" author_id:"Change Author" title:"First Post" content:"Fist Post for US!" id:"5f2f3b405a136438e3a3b634" author_id:"Change Author" title:"First Post" content:"Fist Post for US!" id:"5f2f3bcee46d31225b5422da" author_id:"Change Author" title:"First Post(edit)" content:"First Post for US!(edit)" id:"5f308afaa673f15cc8cbc4c6" author_id:"Change Author" title:"First Post(edit)" content:"First Post for US!(edit)" id:"5f3092705e7b98127c8b706b" author_id:"Change Author" title:"First Post(edit)" content:"First Post for US!(edit)" Delete the todo with id Todo was deleted: todo_id:"5f3092705e7b98127c8b706b" |
OKですね。
おわりに
gRPCのコードは、慣れれば実装規則が決まっているので書きやすく感じます。
ガンガンコードを書いて使いこなしていきましょう^^
それでは、また!
ソースコード
プロトファイル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
// api/todopb/todo.proto syntax = "proto3"; package api; option go_package = "api/todopb"; message Todo { string id = 1; string author_id = 2; string title = 3; string content = 4; } message CreateTodoRequest { Todo todo = 1; } message CreateTodoResponse { Todo todo = 1; } message ReadTodoRequest { string todo_id = 1; } message ReadTodoResponse { Todo todo = 1; } message UpdateTodoRequest { Todo todo = 1; } message UpdateTodoResponse { Todo todo = 1; } message DeleteTodoRequest { string todo_id = 1; } message DeleteTodoResponse { string todo_id = 1; } message ListTodoRequest {} message ListTodoResponse { Todo todo = 1; } service TodoService { rpc CreateTodo(CreateTodoRequest) returns (CreateTodoResponse) {} rpc ReadTodo(ReadTodoRequest) returns (ReadTodoResponse) {} rpc UpdateTodo(UpdateTodoRequest) returns (UpdateTodoResponse) {} rpc DeleteTodo(DeleteTodoRequest) returns (DeleteTodoResponse) {} rpc ListTodo(ListTodoRequest) returns (stream ListTodoResponse) {} } |
サーバーコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 |
// server/server.go package main import ( "context" "fmt" "log" "net" "os" "os/signal" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/user/todo-app/api/todopb" objectid "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "google.golang.org/grpc" ) func checkErr(message string, err error) { if err != nil { log.Fatalf(message, err) } } func init() { log.SetFlags(log.LstdFlags | log.Lshortfile) } type server struct{} // Mongoのコレクションを設定 var collection *mongo.Collection // Toodのアイテムを設定 type todoItem struct { ID objectid.ObjectID `bson: "_id,omitempty` AuthorID string `bson:"author_id"` Title string `bson:"title"` Content string `bson:"content"` } func (*server) CreateTodo(ctx context.Context, req *todopb.CreateTodoRequest) (*todopb.CreateTodoResponse, error) { fmt.Printf("Create Todo Item with %v\n", req) // requestにあるTodoデータを取得 todo := req.GetTodo() data := todoItem{ AuthorID: todo.GetAuthorId(), Title: todo.GetTitle(), Content: todo.GetContent(), } // MongoDBへデータを挿入 res, err := collection.InsertOne(context.Background(), data) if err != nil { return nil, status.Errorf( codes.Internal, fmt.Sprintf("Internal error: %v", err), ) } // Mongoで自動生成されるidを取得 oid, ok := res.InsertedID.(objectid.ObjectID) if !ok { return nil, status.Errorf( codes.Internal, fmt.Sprintf("Cannot convert to OID"), ) } data.ID = oid return &todopb.CreateTodoResponse{ Todo: dataToTodoPb(&data), }, nil } func dataToTodoPb(data *todoItem) *todopb.Todo { return &todopb.Todo{ Id: data.ID.Hex(), AuthorId: data.AuthorID, Title: data.Title, Content: data.Content, } } func (*server) ReadTodo(ctx context.Context, req *todopb.ReadTodoRequest) (*todopb.ReadTodoResponse, error) { fmt.Printf("Read Todo Item with %v\n", req) id := req.GetTodoId() oid, err := objectid.ObjectIDFromHex(id) if err != nil { return nil, status.Errorf( codes.InvalidArgument, fmt.Sprintf("Cannot parse ID"), ) } todo := &todoItem{} filter := objectid.D{{"_id", oid}} if err := collection.FindOne(context.Background(), filter).Decode(todo); err != nil { return nil, status.Errorf( codes.NotFound, fmt.Sprintf("Cannot find todo with specified ID: %v\n", err), ) } todo.ID = oid return &todopb.ReadTodoResponse{ Todo: dataToTodoPb(todo), }, nil } func (*server) UpdateTodo(ctx context.Context, req *todopb.UpdateTodoRequest) (*todopb.UpdateTodoResponse, error) { fmt.Printf("Update Todo Item with %v\n", req) todo := req.GetTodo() oid, err := objectid.ObjectIDFromHex(todo.GetId()) if err != nil { return nil, status.Errorf( codes.InvalidArgument, fmt.Sprintf("Cannot parse ID"), ) } data := &todoItem{} filter := objectid.D{{"_id", oid}} if err := collection.FindOne(context.Background(), filter).Decode(data); err != nil { return nil, status.Errorf( codes.NotFound, fmt.Sprintf("Cannot find todo with specified ID: %v\n", err), ) } data.ID = oid data.AuthorID = todo.GetAuthorId() data.Title = todo.GetTitle() data.Content = todo.GetContent() fmt.Println(data) _, updateErr := collection.ReplaceOne(context.Background(), filter, data) if updateErr != nil { return nil, status.Errorf( codes.InvalidArgument, fmt.Sprintf("Cannot update object in MongoDB: %v", updateErr), ) } return &todopb.UpdateTodoResponse{ Todo: dataToTodoPb(data), }, nil } func (*server) DeleteTodo(ctx context.Context, req *todopb.DeleteTodoRequest) (*todopb.DeleteTodoResponse, error) { fmt.Printf("Delete todo request with %v\n", req) oid, err := objectid.ObjectIDFromHex(req.GetTodoId()) if err != nil { return nil, status.Errorf( codes.InvalidArgument, fmt.Sprintf("Cannot parse ID"), ) } filter := objectid.D{{"_id", oid}} res, err := collection.DeleteOne(context.Background(), filter) if err != nil { return nil, status.Errorf( codes.Internal, fmt.Sprintf("Cannot delete object in MongoDB: %v", err), ) } if res.DeletedCount == 0 { return nil, status.Errorf( codes.NotFound, fmt.Sprintf("Cannot find todo in MongoDB: %v", err), ) } return &todopb.DeleteTodoResponse{TodoId: req.GetTodoId()}, nil } func (*server) ListTodo(req *todopb.ListTodoRequest, stream todopb.TodoService_ListTodoServer) error { fmt.Printf("List todo request with %v\n", req) cur, err := collection.Find(context.Background(), objectid.D{}) if err != nil { return status.Errorf( codes.Internal, fmt.Sprintf("Unkown internal error: %v", err), ) } defer cur.Close(context.Background()) for cur.Next(context.Background()) { data := &todoItem{} err := cur.Decode(data) if err != nil { return status.Errorf( codes.Internal, fmt.Sprintf("Error while decoding data from MongoDB: %v", err), ) } // Straming 配信 stream.Send(&todopb.ListTodoResponse{Todo: dataToTodoPb(data)}) } if err := cur.Err(); err != nil { return status.Errorf( codes.Internal, fmt.Sprintf("Unkown internal error: %v", err), ) } return nil } func main() { fmt.Println("=======Todo API Start====") // Connecting Mongo // mongoは、docker-compose.ymlのservice名 client, err := mongo.NewClient(options.Client().ApplyURI("mongodb://mongo:27017")) checkErr("Faile to create mongo client: %v", err) err = client.Connect(context.Background()) checkErr("Cannot connect to mongo db: %v", err) collection = client.Database("mydb").Collection("blog") // 50051ポートで起動する(docker-compose.ymlと合わせる) lis, err := net.Listen("tcp", "0.0.0.0:50051") checkErr("Failed to listen: %v", err) // grpcのサーバーを作成 s := grpc.NewServer() // TodoServiceServerにマウント todopb.RegisterTodoServiceServer(s, &server{}) go func() { fmt.Println("Starting Server...") err = s.Serve(lis) checkErr("faild to server: %v", err) }() // Ctrl + cでプログラムから抜けられるようにする ch := make(chan os.Signal, 1) signal.Notify(ch, os.Interrupt) // シグナルが受け取れるまで、ブロック <-ch fmt.Println("Stopping the server") s.Stop() fmt.Println("Closing the listener") lis.Close() fmt.Println("=====Todo API End====") } |
クライアントコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
// client/client.go package main import ( "context" "fmt" "io" "log" "github.com/user/todo-app/api/todopb" "google.golang.org/grpc" ) func checkError(message string, err error) { if err != nil { log.Fatalf("message", err) } } func createTodoHandler(c todopb.TodoServiceClient) string { fmt.Println("Creating the todo") // Todoデータを作成 todo := &todopb.Todo{ AuthorId: "selfnote", Title: "First Post", Content: "Fist Post for US!", } createTodoRes, err := c.CreateTodo(context.Background(), &todopb.CreateTodoRequest{Todo: todo}) checkError("Failt to create todo data: %v\n", err) fmt.Printf("Todo has been created: %v\n", createTodoRes) return createTodoRes.GetTodo().GetId() } func readTodoHandler(c todopb.TodoServiceClient, id string) { fmt.Println("Read the todo with id") readTodoRes, err := c.ReadTodo(context.Background(), &todopb.ReadTodoRequest{TodoId: id}) checkError("Error happend while reading: %v\n", err) fmt.Printf("Todo was read: %v\n", readTodoRes) } func updateTodoHandler(c todopb.TodoServiceClient, id string) { fmt.Println("Update the todo with id") newTodo := &todopb.Todo{ Id: id, AuthorId: "Change Author", Title: "First Post(edit)", Content: "First Post for US!(edit)", } updateRes, updateErr := c.UpdateTodo(context.Background(), &todopb.UpdateTodoRequest{Todo: newTodo}) checkError("Error happend while updating: %v\n", updateErr) fmt.Printf("Blog was updated: %v\n", updateRes) } func deleteTodoHandler(c todopb.TodoServiceClient, id string) { fmt.Println("Delete the todo with id") deleteRes, deleteErr := c.DeleteTodo( context.Background(), &todopb.DeleteTodoRequest{TodoId: id}) checkError("Error happend while deleting: %v\n", deleteErr) fmt.Printf("Todo was deleted: %v\n", deleteRes) } func listTodoHandler(c todopb.TodoServiceClient) { fmt.Println("List the todo") stream, err := c.ListTodo(context.Background(), &todopb.ListTodoRequest{}) checkError("error while calling ListTodo RPC: %v\n", err) for { res, err := stream.Recv() if err == io.EOF { break } checkError("Something happend: %v\n", err) fmt.Println(res.GetTodo()) } } func main() { opts := grpc.WithInsecure() // serverは、docker-compose.ymlに定義してあるサービス名 cc, err := grpc.Dial("server:50051", opts) checkError("could not connect: %v\n", err) defer cc.Close() c := todopb.NewTodoServiceClient(cc) // Create Todo todoId := createTodoHandler(c) readTodoHandler(c, todoId) updateTodoHandler(c, todoId) listTodoHandler(c) deleteTodoHandler(c, todoId) } |
Dockerfile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Dockerfile FROM golang:latest ENV SRC_DIR=/go/src/github.com/user/todo-app/api WORKDIR $SRC_DIR ADD ./api $SRC_DIR RUN go get -u google.golang.org/grpc \ && go get -u github.com/golang/protobuf/protoc-gen-go \ && go get go.mongodb.org/mongo-driver/mongo |
docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
version: "3" services: server: image: todo:v1 ports: - "50051:50051" container_name: server command: bash -c "go run server/server.go" depends_on: - mongo volumes: - ./api:/go/src/github.com/user/todo-app/api client: image: todo:v1 container_name: client depends_on: - server volumes: - ./api:/go/src/github.com/user/todo-app/api mongo: image: mongo container_name: mongo |
generate.sh
1 |
protoc api/todopb/todo.proto --go_out=plugins=grpc:. |
Makefile
1 2 3 4 |
.PHONY: client client: docker-compose run --rm client bash -c "go run client/client.go" |
最近のコメント