Quantcast
Channel: Go3 Advent Calendarの記事 - Qiita
Viewing all 25 articles
Browse latest View live

Goでプログラムの動的アップデートについてのトリビアです。アドカレ初参加です


Golangで書かれたStatefullなVirtual Web BrowserライブラリのSurfでJavascriptが処理出来るようにしてみたかった

$
0
0

はじめに

これは Go3 Advent Calenderの22日の記事です。
大分遅れて申し訳ありません…。

TL; DR;
出来る限り頑張りましたがottoとwebloopでは実現することが出来ませんでした…。orz
今後ottoとwebloop以外の方法もトライしてみたいと思います。

今回Advent Calender駆動開発で、以前作ったSlackにemojiをアップロードするツールを最新化しようかと考えていましたが、いきなり壁にぶち当たりました。
上記のツールはSlackにemojiをアップロードするAPIがないため、Surfという仮想ブラウザのライブラリを使って、半ば無理やりemojiをアップロードしていたのですが、SlackのUIがアップデートされてから動かなくなっていたのです。
Slackのemojiをアップロードするまでの遷移が

  • ページ内のフォームに入力

から

  • 「絵文字を追加する」ボタンをクリック
  • モーダルダイアログ内のフォームに入力

という遷移に変わったため、それに合わせてプログラムを修正する必要がありました。
最初は単純に考えていて、Surfの brower.Click() でダイアログを出して入力するだけでよいと思っていました。

しかしそう単純ではなかったのです…。
SurfのClick()はJavascriptのイベントの発火はサポートしていなかったのです…。
実際Surfのbrowserのソースを見るとhttpのRequestやResponse、Cookie、DOM用にgoqueryなど、Javascript関連の処理は行っていない様でした。

これは困りました。これが出来ないとそもそもやりたいことが出来ません。
という訳でSurfにどうやったらJavascriptを実行する機能が付けられるか挑戦してみました。

まずは調査

GolangでJavascriptを実行出来る様なヘッドレスブラウザがないかどうか調べていたところ、goqueryのTips and tricksにHandle Javascript-based Pagesという項目がありました。
ここでは以下の2つのライブラリが紹介されていました。

  • Javascriptパーサー otto
  • ヘッドレスブラウザ webloop

この2つでどうにか出来ないかやってみました。

otto

こちらはJavascriptを実行して結果を取得できるというものですが、全てGolangで書かれているというものすごいライブラリでした。
これでページ内のJavascriptを実行すれば出来るはず…と思って調べたのですが、こちらあくまでもjavascriptを実行するためのライブラリとの事で、HTMLを読み込んでDOMを操作するといったことは出来ないとのことでした。
ですが、イシュー等を見た限り、Reactを動かしたりページ内の一部のJavaScriptを動かして必要な情報を取得したりといった使い方も出来るらしいです。

上記の事から今回はottoではHTMLを渡してJavascriptを実行することは難しいと判断しました。

webloop

こちらはGolangからwebkit2を動かすgo-webkit2を使っているヘッドレスブラウザだそうで、URLを読み込んでJSを実行したり出来るそうなので、これならやりたい事が出来るのでは、と思いました。

方法としては、webloopはURIからソースを読み込むだけではなく、HTMLをstringにして読み込ませることが出来る様なので、Surfから必要な時にHTMLを取り出してwebloopでJavaScriptを処理して結果をSurfから返すという流れを考えました。

環境のセットアップ

今回使ったのは(ちょっと古いのですが)macOS Sierraです。

go-webkit2のページを見て必要な依存関係をインストールします。
こちらのイシューを見て、Macの場合以下のコマンドを発行すれば良いことが分かりました。

$ sudo port install webkit2-gtk
$ sudo port install webkit-gtk3

しかし、ずっと前にMacPortsをインストールしたまま放置していた私は既に入っていたものが古かったのか、中々ビルドが成功しませんでした。
結局以下のコマンドで一度全部消してインストールし直しました。

$ sudo port -fp uninstall installed

途中依存関係のあるもののinstallに失敗した場合は、一度失敗したものだけcleanしてinstallしたら成功しました。

sudo port clean 失敗したもの
sudo port install 失敗したもの

テストしてみる

エラーが解消されたら、まずはgo-webkit2のテストをいくつか動かしてみました。
しかしなんだか動きが怪しい…。
VS Codeでテストを実行すると失敗したり、処理が帰って来なかったりと動かない。
ステップ実行すると成功したりと、もう既に不安しかない。

そしてwebloopの方のテストはデバッグ実行でもうまく動かない…。
webloopは諦め、go-webkit2で頑張ることにしました。

既存のテストにないファイルから読み込んでテストするパターンを試してみます。

test_simple.html
<!doctype html>
<html>
    <head>
        <title>test</title>
    </head>
    <body>
        <button id="button1">test</button>
        <div id="target"></div>
        <script>
            document.getElementById('button1').addEventListener('click', function() {
                document.getElementById('target').innerHTML = '<p>good</p>';    
            });
        </script>
    </body>
</html>
simple_test.go
func TestWebView_RunSimpleJS(t *testing.T) {
    webView := NewWebView()
    defer webView.Destroy()

    loadOk := false
    webView.Connect("load-failed", func() {
        t.Errorf("load failed")
    })
    webView.Connect("load-changed", func(_ *glib.Object, loadEvent LoadEvent) {
        switch loadEvent {
        case LoadFinished:
            webView.RunJavaScript(`document.getElementById("button1").click()`, func(result *gojs.Value, err error) {
                if err != nil {
                    t.Errorf("RunJavaScript error: %s", err)
                }
                webView.RunJavaScript(`document.getElementsByTagName("body")[0].outerHTML`, func(result *gojs.Value, err error) {
                    resultString := webView.JavaScriptGlobalContext().ToStringOrDie(result)
                    fmt.Println(resultString)
                    if strings.Count(resultString, "<p>good</p>") > 1 {
                        loadOk = true
                    }
                    gtk.MainQuit()
                })
            })
        }
    })

    f, err := os.OpenFile("test_simple.html", os.O_RDONLY, 0755)
    if err != nil {
        t.Errorf("File open err: %s", err)
    }
    defer f.Close()

    scanner := bufio.NewScanner(f)

    htmlData := ""
    for scanner.Scan() {
        htmlData = htmlData + scanner.Text() + "\n"
    }

    glib.IdleAdd(func() bool {
        webView.LoadHTML(htmlData, "")
        return false
    })

    gtk.Main()

    if !loadOk {
        t.Error("!loadOk")
    }
}

結果無事にステップ実行しなくても成功しました。

API server listening at: 127.0.0.1:16583
dbus[55590]: Dynamic session lookup supported but failed: launchd did not provide a socket path, verify that org.freedesktop.dbus-session.plist is loaded!
dbus[55594]: Dynamic session lookup supported but failed: launchd did not provide a socket path, verify that org.freedesktop.dbus-session.plist is loaded!
<body>
        <button id="button1">test</button>
        <div id="target"><p>good</p></div>
        <script>
            document.getElementById('button1').addEventListener('click', function() {
                document.getElementById('target').innerHTML = '<p>good</p>';    
            });
        </script>


</body>
PASS

本命のHTMLを突っ込んで見る

さて、簡単なHTMLが動作したので本命のページから抜いてきたHTMLを突っ込んでJavaScriptを実行してみたのですが…。
結果は駄目でした。
Javascriptを実行する箇所が待っても返ってきません。

まだ調査の余地はあるのですが、時間も大分過ぎてしまっているので今回はここまでとすることにしました :bow:

やってみて

そもそもscriptタグ内で指定された外部のスクリプトは取得されているのかとか、普通に見る場合は認証が必要なページだが大丈夫なのだろうかなど、色々と検証が不足しているのですが、Webloopがちゃんと動くのかどうかが知りたいところです。
また、認証情報ごと渡してJavaScriptを動かす必要があるページだけ動作させられれば良さそうですが、その辺りも調べてみたいです。
他にもいくつかGolangで動作するHeadless Browserはある様なので、そちらが使えそうかどうか見てみたいと思いました。

P. S.

このイシューの今年の9月辺りの書き込みを見たところ、ドキュメントには乗ってないけど /api/emoji.add にユーザートークンとmultipart formアップロードすればemojiが追加できると書かれていました。
いやーAPIが追加されていないか、もっと早い段階で念のため調べて置くべきでした…。orz
このAPIを使用したプログラムに書き換えれば、取り敢えずやりたい事は出来そうです。
皆さんも時間が経ったらまずは欲しいAPIが提供されてないか確認しましょう。

Fo言語のご紹介

$
0
0

この記事はGo3 Advent Calendar23日目の記事です。

Go3アドベントカレンダーなんですが、 あんまりGoの話が出てきません。
Foの話をします。

Foとは

Foとは、Goに関数型言語の機能を追加したプログラミング言語です。

いくつか特徴的な点があります。ひとつずつ見ていきます。

ジェネリクス

Foでは、ジェネリクスを定義することができます。

type A[T] []T

type B[T, U] map[T]U

type C[T, U] func(T) U

type D[T] struct{
    a T
    b A[T]
}

ジェネリクスを利用して、Mapを定義してみます。

func MapSlice[T](f func(T) T, list []T) []T {
    result := make([]T, len(list))
    for i, val := range list {
        result[i] = f(val)
    }
    return result
}

MapSlice関数は、 func(T) T[]T を引数に受け取って、listの中身をfuncに渡した結果からなる新しいsliceを返します。
RubyやJavascriptではおなじみですね。

今回はTをintとして、funcは以下のように実装してみます。

func incr(n int) int {
    return n+1
}

これを実行してみましょう。main関数は以下のように書けます

func main() {
    fmt.Println(MapSlice[int](incr, []int{1, 2, 3}))
}

どうやって実行するのかですが、今回はThe Fo Playgroundを利用します。
実行すると、incrされたlistが表示されますね。

次は、Tをstringとしてみます。
このようなコードを書きました。

package main

import "fmt"


func MapSlice[T](f func(T) T, list []T) []T {
    result := make([]T, len(list))
    for i, val := range list {
        result[i] = f(val)
    }
    return result
}

func past(n string) string {
    return n + "ed"
}

func main() {
    fmt.Println(MapSlice[string](past, []string{"want", "watch", "point"}))
}

実行結果は [wanted watched pointed] のようになります。

カリー化

もうちょっとFunctional Programmingっぽいことをしたいので、カリー化を実装してみます。

このようなコードを書きました。

package main

import "fmt"

func curry2[P1, P2, R](f func(P1, P2) R) func(P1) func(P2) R {
  return func(p1 P1) func(P2) R {
    return func(p2 P2) R {
      return f(p1, p2)
    }
  }
}

func main() {
  add := curry2[int, int, int](
    func(a, b int) int {
      return a + b
    },
  )

  incr := add(1)
  fmt.Println(incr(1))
  fmt.Println(incr(10))
}

curry2関数は、f func(P1, P2) R を受け取って、 func(P1) func(P2) R を’返す関数です。
ここでは、incrがfです。

ちなみに、そもそもGoは、関数を引数で受け取る・関数を戻り値で返す関数を定義することができるため、
がんばればGo単体でもカリー化を実装することは可能です。

まとめ

その他の例はexamplesにあります。
Goでもジェネリクスがサポートされるという話がありますね。
Foは開発がストップしてしまっている雰囲気があるのですが、面白い試みだと思ったため紹介しました。

君は全てのケースに備えているか? 〜コード静的解析のススメ〜 #golang

$
0
0

動機

こんなカンジの型と定数リストがあったとします。

a/a.go
package a

type TestKind int

const (
    TestKindHoge TestKind = iota
    TestKindFuga
    TestKindPiyo
)

いわゆる列挙というか区分みたいなヤツですが、これに対してswitch文とか書きますよね。

sample.go
switch v {
case a.TestKindHoge:
    // do something
case a.TestKindFuga:
    // do something
case a.TestKindPiyo:
    // do something
}

で、実装後に誰か他の開発者が区分が追加したとします。

a/a.go
package a

type TestKind int

const (
    TestKindHoge TestKind = iota
    TestKindFuga
    TestKindPiyo
    TestKindBosukete // Add!
)

その時にswitch文のcaseの追加が漏れていて意図しない挙動になったりすることってないでしょうか。

switch v {
case a.TestKindHoge:
    // do something
case a.TestKindFuga:
    // do something
case a.TestKindPiyo:
    // do something
default:
    panic("unexpected")
}

などとしておけば最悪実行時に検知はできるかもですが、可能ならビルド時に検知したいですよね。

静的解析

go vetgolint みたいなコンパイル前のコード静的解析で検知することができるのでは?
と思い立ちました。

https://godoc.org/golang.org/x/tools/go/analysis

上のパッケージを使用するとGoの静的解析ツールが作成できます(しかもGo1.12からgo vetで呼び出せる様になるらしい)。

ちょうど日本におけるGo静的解析の(というかGoの)伝道師 @tenntenn さんが超わかりやすい記事を書いてくれました!

Goにおける静的解析のモジュール化について
モジュール化された静的解析の実装を追ってみよう

本記事ではanalysisパッケージについての詳細は割愛し、上記を参考に作成したツールを紹介します。

allcasesチェッカー

てな訳で作りました。

allcases

インストール

$ go get github.com/knightso/allcases/cmd/allcases

使い方

$ allcases [-flag] [package] 
  • flagは全て go/analysis から引き継いだもので、必須ではないです。興味ある方は allcases -help でチェックしてください
  • package指定はgo tool準拠です

アノテーション

switch文の前に // allcases というコメントをつけることで、評価する値の型の定数全てのcaseが網羅されるかをチェックします。

sample.go
// allcases
switch v {
case a.TestKindHoge:
    // do something
case a.TestKindFuga:
    // do something
case a.TestKindPiyo:
    // do something
}

出力例↓

/src/sample/sample.go:36:2: no case of a.TestKindBosukete

最後に

個人的にコード静的解析は今後ブームになるのではないかと予想していたのですが、go/analysisパッケージとgo1.12のgo vet組み込みの話を聞いてますます確信に近づきました。(というか自分が遅れてるだけですでに流行ってるってことかな?^^;)

go vetやgolintなどに用意されている汎用的なチェッカーを使うのみでなく、各プロジェクトに特化したカスタムlinterやanalyzerを開発者が気軽につくることで生産性を上げる開発手法が流行る気がしてます。

さいごにもう一つ、今回のツールを作るのに、やはり @tenntenn さん作の commentmap.Analyzer を利用させて貰いました。ありがとうございました。 :bow:
https://github.com/tenntenn/comment
アノテーションコメントの解析を簡単に行うことができるユーティリティAnalyzerです。Analyzer.Requiresフィールドに設定することで利用できます(^^)

grapi : #golang で interface driven かつボイラプレートに悩まされない API 開発

$
0
0

Go Conference 2018 Spring にて, Go で快適に Web API 開発をするための CLI + ライブラリである grapi について話した.
本記事では,grapi で典型的なAPIをどう実装するかのワークフローとともに,grapi の特徴や思想を紹介する.

記事中では grapi v0.3.2 について扱う.

grapi の特徴・やること

  • 開発者は gRPC IDL でスキーマを定義し,gRPC server を実装する
  • Ruby on Rails を意識した file generator
    • rails new に対応した grapi init
    • rails gに対応した, grapi g NAMEgrapi generate NAME
      • Google API Design Guide に準拠
      • protobuf のスキーマや gRPC server の実装スケルトンも生成できる
      • → 開発者は server の中身の実装に集中できる
  • grapi server を実行すればデフォルトで :3000application/json な http server が立つ
    • ちょっと設定を変えれば application/grpc も話せるようになる

プロジェクト新規作成

grapi init APP_NAME でプロジェクトのボイラプレートが生成される.APP_NAME. を入れるとカレントディレクトリに生成される.
いまは2分くらいかかってるが,これは dep ensure がこっそり2回走っているのが原因.

asciicast

生成されるファイルは以下.

  • api
    • protobuf のスキーマ定義と生成されたコードが入る
  • app
    • アプリケーションコードが入る
    • cmd/server に合わせて pkg/server にしたら良かったってちょっと後悔している)
  • cmd
    • エントリポイント(main パッケージ)が入る
    • cmd/CMD_NAME があるとき grapi CMD_NAME でコマンドが起動できる(後述)
  • tools.go
    • gex が利用する,ツール間利用マニフェストファイル
    • protoc や grapi のプラグインはこのファイルで管理されている
  • Gopkg.{toml,lock}
    • dep
    • Go Module 安定したらそっちを柄用にしたい
  • grapi.toml
    • grapi の設定ファイル
    • protoc のプラグインや引数などもここに記述する
:) % tree -I "vendor|bin"
.
├── Gopkg.lock
├── Gopkg.toml
├── api
│   └── protos
│       └── type
├── app
│   ├── run.go
│   └── server
├── cmd
│   └── server
│       └── run.go
├── grapi.toml
└── tools.go

7 directories, 6 files

ここまでは大したことはしていない.

API 定義

asciicast

API スキーマの生成

grapi の API は gRPC の IDLgoogle.api.Http で HTTP へのマッピングを記述したもので定義される.
grapi g service でそのスキーマ定義 + Go の server 実装の雛形が生成できる.また,grapi g scaffold-service を利用すると Google API Design Guide の Standard Methods に則った形式の,いわゆる RESTful っぽいスキーマ定義を生成することができる.

ここは完全に rails g (scaffold_)?controller を意識している.
普通に gRPC server を実装しようとすると「.proto を書いて」「頑張って protoc の引数を組み立てて実行して」「生成された interface を実装した Go のオブジェクトを作る」までやったあとにようやくサーバの実装に取り掛かれる.
grapi g (scaffold)?-service だとその準備をすべてすっ飛ばして最低限の雛形ができるので,結構きもちいい.

API スキーマの更新

grapi による API 開発における開発者のメンタルモデルは,unary な gRPC server を実装しているときとだいたい同じ,大まかにつぎの3ステップのループになるはず(厳密にはテスト書いたりいろいろあるけど,そのへんはよしなに補完してほしい).

  1. .proto ファイルの更新
  2. protoc の実行
  3. 実装の変更

grapi はこのうち「protoc の実行」について面倒を見る.grapi protoc を叩くことで .proto ファイル全てに対してそれぞれ必要なプラグインすべてを実行してくれる.これは先述した grapi.toml に記述されているとおりに実行される.

# `grapi init` で生成される `grapi.toml` の一部
# 標準では `protoc-gen-go`, `protoc-gen-grpc-gateway`, `protoc-gen-swagger` の3つが利用される

[protoc]
protos_dir = "./api/protos"
out_dir = "./api"
import_dirs = [
  "./api/protos",
  "./vendor/github.com/grpc-ecosystem/grpc-gateway",
  "./vendor/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis",
]

  [[protoc.plugins]]
  name = "go"
  args = { plugins = "grpc", paths = "source_relative" }

  [[protoc.plugins]]
  name = "grpc-gateway"
  args = { logtostderr = true, paths = "source_relative" }

  [[protoc.plugins]]
  name = "swagger"
  args = { logtostderr = true }

API 実装

とりあえずモックデータを返す

asciicast

grapi init で生成したプロジェクトでは,grapi server で API サーバを起動できる.
しかし,grapi g (scaffold-)?service でファイル生成した直後は,どれだけ curl しても返事がない(正確には 501 Not Implemented が返ってくるが).

$ curl localhost:3000/todos                                                                                     
{"code":12,"message":"Not Implemented"}

まず最初に,grapi のサーバに生成されたサーバ実装を登録する必要がある.

diff --git a/app/run.go b/app/run.go
index c9c9016..46ac33c 100644
--- a/app/run.go
+++ b/app/run.go
@@ -1,6 +1,7 @@
 package app

 import (
+   "github.com/izumin5210-sandbox/todoapp-grapi/app/server"
    "github.com/izumin5210/grapi/pkg/grapiserver"
 )

@@ -9,7 +10,7 @@ func Run() error {
    s := grapiserver.New(
        grapiserver.WithDefaultLogger(),
        grapiserver.WithServers(
-       // TODO
+           server.NewTodoServiceServer(),
        ),
    )
    return s.Serve()

ここで再度 curl すると,微妙に結果が変わる.

$ curl localhost:3000/todos                                                                                     
{"code":12,"message":"TODO: You should implement it!"}

これは grapi g (scaffold-)?service で生成された Go のコードがそういう実装になっているため.

// `grapi g scaffold-service todo` で生成された Go のコードの一部
// rpc の実装はすべて `codes.Unimplemented` を返すようになっている

func (s *todoServiceServerImpl) ListTodos(ctx context.Context, req *api_pb.ListTodosRequest) (*api_pb.ListTodosResponse, error) {
    // TODO: Not yet implemented.
    return nil, status.Error(codes.Unimplemented, "TODO: You should implement it!")
}

ここのコードをとりあえずモックを返すように書き換えてみる.

diff --git a/app/server/todo_server.go b/app/server/todo_server.go
index eab2a9c..f71bb38 100644
--- a/app/server/todo_server.go
+++ b/app/server/todo_server.go
@@ -26,8 +26,14 @@ type todoServiceServerImpl struct {
 }

 func (s *todoServiceServerImpl) ListTodos(ctx context.Context, req *api_pb.ListTodosRequest) (*api_pb.ListTodosResponse, error) {
-   // TODO: Not yet implemented.
-   return nil, status.Error(codes.Unimplemented, "TODO: You should implement it!")
+   return &api_pb.ListTodosResponse{
+       Todos: []*api_pb.Todo{
+           &api_pb.Todo{TodoId: "1", Title: "Write Go 4 Advent Calendar at 2018/12/05", Done: true},
+           &api_pb.Todo{TodoId: "2", Title: "Write Go 2 Advent Calendar at 2018/12/15", Done: true},
+           &api_pb.Todo{TodoId: "3", Title: "Write Go 3 Advent Calendar at 2018/12/20", Done: true},
+           &api_pb.Todo{TodoId: "4", Title: "Write Go 3 Advent Calendar at 2018/12/20", Done: false},
+       },
+   }, nil
 }

 func (s *todoServiceServerImpl) GetTodo(ctx context.Context, req *api_pb.GetTodoRequest) (*api_pb.Todo, error) {

これで改めて curl をすると,それっぽい JSON が返ってくるようになる.

$ curl localhost:3000/todos | jq .                                                                              
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current                                    
                                 Dload  Upload   Total   Spent    Left  Speed                                      
100   315  100   315    0     0  45559      0 --:--:-- --:--:-- --:--:-- 52500                                     
{                                                                                                                  
  "todos": [                                                                                                       
    {                                                                                                              
      "todo_id": "1",                                                                                              
      "title": "Write Go 4 Advent Calendar at 2018/12/05",                                                         
      "done": true                                                                                                 
    },                                                                                                             
    {                                                                                                              
      "todo_id": "2",                                                                                              
      "title": "Write Go 2 Advent Calendar at 2018/12/15",                                                         
      "done": true                                                                                                 
    },                                                                                                             
    {                                                                                                              
      "todo_id": "3",                                                                                              
      "title": "Write Go 3 Advent Calendar at 2018/12/20",                                                         
      "done": true                                                                                                 
    },                                                                                                             
    {                                                                                                              
      "todo_id": "4",                                                                                              
      "title": "Write Go 3 Advent Calendar at 2018/12/20"                                                          
    }                                                                                                              
  ]                                                                                                                
}

grapi が面倒を見てくれるのはここまで: API Desing Guide に従ったコード生成と protoc のラップ,あとはコマンドの実行とビルドだけ.
実際のアプリケーション開発はここからが本番になるが,その「本番」に至るまでの瑣末事を肩代わりするのが grapi の仕事になる.

おまけ

DB をつなぎ込む

実際にデータ永続化とかをするときは,*sql.DB などのコネクションなりクライアントなりを fooServiceServerImpl struct にもたせると良い.
ちょっと古い & 内容が異なるサンプルだけど,Go + grpc-gateway でつくる JSON API 速習会 @ Wantedly で利用したサンプルリポジトリの sqlx 導入 PR が参考になると思う.

アプリケーションが大きくなってくると server に *sql.DB を直接もたせるんじゃなくて,DAO や repository などの抽象化パターンの導入を検討するといい.

gRPC を使う

grapi init した直後のプロジェクトは普通に localhost:3000application/json で話しかけると返事をしてくれる.
これは内部で2つのサーバを動かすことで実現されている.

  • gRPC server
    • unix domain socket(デフォルトだと tmp/server.sock)を listen
  • grpc-gateway server
    • tcp の :3000 を listen
    • gRPC client デフォルトだと tmp/server.sock にある gRPC server を見に行く

これらのサーバがどこを listen するかは,以下に挙げる grapi の起動オプションで切り替えることが可能.

当然だけど,grpc-gateway を経由せず gRPC server と直接 application/grpc で通信することもできる.

なので,「今後 gRPC を導入していきたいのでアプリケーションレイヤでの知見がほしい・IDL を先行利用していきたい」みたいなユースケースでとりあえず grapi を使っておいて,サーバ・クライアント・インフラすべての準備が整ったタイミングで gateway を剥がす…という使い方も可能(というか,まさにその用途で使いたくて grapi を作った).

既存のパッケージ・ツールとの比較

ginecho, net/http.Server を使った実装との比較のメリットとしては,やはり json.Unmarshal をしなくていいことに尽きると思っている,ちゃんとした struct の形でパラメタが渡ってくるのが一番嬉しい.これはコード生成ベースでやることの大きな強みである.

goa は同じくコード生成ベース・interface driven な開発ができるパッケージである.これに対する強みとしては前述した通り「gRPC 移行への前段階として利用できる」ほか,「protobuf は実装言語に依存しない IDL で記述するので,多言語・プラットフォームからの利用も簡単」というのは大きいはず.

生 gRPC server との比較としては…,「gRPC をいつでも使いはじめられる環境」が既にあるのなら grpc-gateway を通さずに使うべきである.grpc-gateway を使わないとしても,grapi のもつ Google API Design Guide に則ったボイラプレート生成は有効に使えると思う.また,この「gRPC をいつでも使いはじめられる環境」というのは実は結構難度が高い(インフラ的な障壁, スキーマ共有どうする, 生成コードはどう扱う, etc).なので,そこまですぐに用意できない・だけど近い内に gRPC を導入しておきたい という状況のときには grpc-gateway を内包する grapi は強い味方になると思う.

ちなみに,「IDL から(サーバ・クライアントの)コード生成」というのは microservices architecture かつサービス数が増えれば増えるほど欲しくなってくる.なので,そういう状況に置かれてる現場では gRPC の投入を見越しつつ grapi の採用を検討してもらえると嬉しい.

雑感

grpc-gateway は直感的でわかりやすいんだけど,むだに json -> pb 変換が挟まるので用途によってはパフォーマンスが気になるかもしれない.
grpc-gateway 以外の,でも gRPC は使わないアプローチとしては

  • reflection protocol をうまく使ったやり方(e.g. mercari/grpc-http-proxy)を考える
  • protobuf をしゃべる http client を生成 & protobuf をそのまま proxy するだけの server を作る
  • protobuf IDL から http.Server & HTTP クライアントを生成

等があるかもしれない.
一方で,protobuf & gRPC まわりはかなりエコシステムが発展してきているので,最終的には生で gRPC を利用できるに越したことはないはず.

Viewing all 25 articles
Browse latest View live