doilux’s tech blog

ITに関する備忘録。 DDP : http://doiluxng.hatenablog.com/entry/2018/01/01/195409

GoでgRPCのサーバーを構築する

go getとかは割愛。こんなファイルを作る。

syntax = "proto3";

option go_package = "proto";

package user;

service UserService {
    rpc ListUser(RequestType) returns (stream User) {}
    rpc AddUser(User) returns (ResponseType) {}
}

message ResponseType {
}

message RequestType {
}

message User {
  string id                  = 1;
  string email               = 2;
  string name                = 3;
  Status status              = 4;
}

enum Status {
    ACTIVE = 0;
    INACTIVE = 1;
}

protocコマンドを実行する。引数はprotoファイルのパスだけど、それ以外のオプションがhelp読んでもよくわからんかったorz とりあえず、これでuser.pb.goってライブラリが完成した。

protoc --proto_path=proto --go_out=plugins=grpc:proto-out proto/user.proto

次にserverを実装する。↓を参考にした。

goのgRPCで便利ツールを使う - Qiita

メモがわりのコメント多め。

package main

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

    pb "./proto-out" // ここはgithub上のパス(例:"google.golang.org/grpc/examples/helloworld/helloworld")にしてるっぽいけど一旦こうする
    "golang.org/x/net/context"
    "google.golang.org/grpc"
)

const (
    port = ":50051" // ポート番号
)

// Serverというインターフェースを定義する
type Server struct {
    users []*pb.User
    m     sync.Mutex // ここでMutexを持たせなくてもいいかもしれない(参考:https://qiita.com/h3_poteto/items/3a39c41743b4fd87c134)
}

// ServerインターフェースにListUserメソッドとAddUserメソッドを追加する。
// これでGoのダッグタイピングによって、UserServiceServerを実装したことと同じになる

func (cs *Server) ListUser(p *pb.RequestType, stream pb.UserService_ListUserServer) error {
    cs.m.Lock() // ロックする
    defer cs.m.Unlock() // deferはこの関数終了時に行う処理を定義する。finalyみたいなもん

    for _, p := range cs.users {
        if err := stream.Send(p); err != nil {
            return err
        }
    }
    return nil
}

func (cs *Server) AddUser(c context.Context, p *pb.User) (*pb.ResponseType, error) {

    cs.m.Lock()
    defer cs.m.Unlock()

    cs.users = append(cs.users, p)
    return new(pb.ResponseType), nil
}

func main() {

    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("faild to listen: %v", err)
    }

    server := grpc.NewServer()

    // new(T)は、型Tの新しいアイテム用にゼロ化した領域を割り当て、そのアドレスである*T型の値を返す。
    // `ゼロ化した領域`はすなわちinitされた領域だと思われる。
    // 参考:http://golang.jp/effective_go#allocation_new
    pb.RegisterUserServiceServer(server, new(Server))

    go func() {
        log.Printf("start grpc Server port: %s", port)
        server.Serve(lis)
    }()

    // os.Signal型のチャネルを定義して、
    // <-quitのところでブロッキングして、
    // quitメッセージが来たらサーバーを停止させている。

    quit := make(chan os.Signal)
    
    // 多分、OSからのInterruptが来たら、quitキューに入れる、ってことを定義している
    signal.Notify(quit, os.Interrupt)
    <-quit
    log.Println("stopping grpc Server...")
    server.GracefulStop()
}

サーバーを起動してみる。

go run user_server.go
2018/05/13 23:19:52 start grpc Server port: :50051
^C2018/05/13 23:19:57 stopping grpc Server...

次にクライアントを作る。↓参考にした

gRPC(Go) で API を実装する (フェンリル | デベロッパーズブログ)

package main

import (
    "log"
    "time"

    "golang.org/x/net/context"
    "google.golang.org/grpc"
    pb "./proto-out"
    "io"
    "fmt"
)

const (
    address     = "localhost:50051"
)

func main() {
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatalln("did not connect: %v", err)
    }

    defer conn.Close()
    c := pb.NewUserServiceClient(conn)


    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    user_1 := pb.User{ Id: "doilux1", Email: "doilux1@example.com", Name: "hoge fuga" }
    user_2 := pb.User{ Id: "doilux2", Email: "doilux2@example.com", Name: "hoge fuga"}
    user_3 := pb.User{ Id: "doilux3", Email: "doilux3@example.com", Name: "hoge fuga"}


    c.AddUser(ctx, &user_1)
    c.AddUser(ctx, &user_2)
    c.AddUser(ctx, &user_3)


    stream, err := c.ListUser(ctx, &pb.RequestType{})
    if err != nil {
        log.Fatalln("could not get user: %v", err)
    }

    for {
        msg, err := stream.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatalln("Receive:", err)
        }
        fmt.Println(msg)
    }
}

実行してみる。サーバーを起動した状態で以下実行する。

go run user_client.go
id:"doilux1" email:"doilux1@example.com" name:"hoge fuga"
id:"doilux2" email:"doilux2@example.com" name:"hoge fuga"
id:"doilux3" email:"doilux3@example.com" name:"hoge fuga"

一応、できたことはできた。

わからなかったこと

ロジックどこに書く?

例えば、「すでに使われているIDは登録できない」とか、そんなルールがあるときにどこに実装するんだろう?やっぱりここになるんだろうか(実際にはどこかに委譲するとおもうけど)

func (cs *Server) AddUser(c context.Context, p *pb.User) (*pb.ResponseType, error) {
    cs.m.Lock()
    defer cs.m.Unlock()
    cs.users = append(cs.users, p)
    return new(pb.ResponseType), nil
}

エラーの時どうする?

上記に関連して、ビジネスエラーだったときに一般的にどんなレスポンスを返してるんだろうか。

ということで今日はこれまで

追記

これが参考になりそう

GitHub - harlow/go-micro-services: HTTP up front, Protobufs in the rear