2019年買い物ふりかえり

概要

人のレビューが買い物するのに非常に助かるので、自分も今年買ったものを振り返り。

去年の振り返りはこちら

2018年の買い物振り返り - 押してダメならふて寝しろ

とてもよかった

炊飯器

炊飯器、価格帯の真ん中ぐらいのやつを10年ぐらい使ってたんですけど、ぼろぼろになっちゃったので買い換え。お米に全く興味が無く、ご飯好きじゃ無いと思ってたんですけど、炊飯器変えたら同じ米がメッチャおいしくて今年買い換えたものの中で一番よかった。

学びとしては、

  1. フラグシップな製品を買った方がいい
  2. 炊飯器は10万くらいで世の中に出て、世代交代を経て5万くらいに下がる

ということ。さんざん迷ったあげく6万ぐらいに下がってた型落ちのこれを買いました。高い買い物だが、はるかに上回る満足度があったので自分の中では納得している。

トースター

うっかりポイントで交換したツインバードのトースターが背が低くて、丸パン入らないし、すぐ焦げてイライラ Max で買い換えることにしました。

火力(W数)じゃなくて、庫内の温度が設定できるのがよいです。 パンがおいしく焼けて満足です。一緒に買ったお皿を使うと、パン2枚と目玉焼きが一緒に作れるので非常に助かっています。

ヘッドホン

店頭でノイズキャンセリングを試したら衝撃的でさんざん迷って買いました。音楽聴かなくても静かになるので通勤の時に使ったりしています。ただ夏は暑くてつけてられません・・・。

わりとよかった

フライパン

朝食作るように T-fal の小さいフライパンを買ってたんですが、わりとすぐ傷がついてダメになってしまってました。この手のものは消耗品かなと思ってたんですが、和平フレイズのフライパンがいいとのネット情報を鵜呑みにして買ってみたら、なるほど長持ちです。

電球

廊下の電球を LED にしてみたらめっちゃ明るかった。 明るすぎるぐらいだがはやく LED にしとけばよかった。

ふつう

近所の量販店に置いてあったので買ってみた。スースーする石けん、くらいの印象。 いいと思うけどネットで高いのを買って使うほどでもなさそう。 オッサン2.0のにおいになってるのかな?

ボドゲ

基準はこども(9歳)とできるゲーム。

TAGIRON

質問カードを交互に使って2人で数字を当て合うゲーム。こどもが気に入って割とよくやった。僕はかなりぐずぐずしたゲームをしちゃうんだけど、うまい人はササッと当てられるっぽい。

ラビリンス

ルールが単純なので手軽に遊べる。好評だったけど駆け引きよりは運の要素が大きい気がする。ちょっとした集まりにさっと出来てよいかもです。

ラビリンス (Labyrinth) ボードゲーム

ラビリンス (Labyrinth) ボードゲーム

  • メディア: おもちゃ&ホビー

バトルライン

これはクリスマスプレゼントでこどもに与えてまだ遊べてないけど期待大。

バトルライン (Battle Line) カードゲーム

バトルライン (Battle Line) カードゲーム

  • メディア: おもちゃ&ホビー

Recovery Middleware for Goa v3

概要

Goa v1 にはサーバ内で panic が起こったときにリカバーしてくれるミドルウエアがありました。v3 だとこれがないので移植しました。

Goa v3 からはミドルウエアは Go の標準の方法になっているので Goa 以外でも使えます。

github.com

使い方

handler = recovery.Recover()(handler)

何もオプションを指定しないと、500 Internal Server Error でレスポンスします。

オプション デフォルト値
ContentType application/json
ResponseStatus 500 (http.StatusInternalServerError)
StackSize MinimumStackSize (4KB)
Logger 標準 Log
ErrorHandler スタックトレースはログに出力し、レスポンスボディには何も出力しません

オプションを付けるときは

handler = recovery.Recover(
    ContentType("application/json"),
    ResponseStatus(http.StatusOK),
    ErrorHandler(func(c *Config, w http.ResponseWriter, msg string, stack []string) {
            w.Header().Set("Content-Type", c.ContentType)
            w.WriteHeader(c.ResponseStatus)
            obj := map[string]string{
                "error": fmt.Sprintf("%s\n%s", msg, strings.Join(stack, "\n")),
            }
            b, err := json.Marshal(obj)
            if err != nil {
                t.Fatal(err)
            }
            w.Write(b)
        }),
)(handler)

のようにします。ログに出すかどうか、レスポンスボディにエラーを含めるかなどは エラーハンドラーを自分で書くことで調整できます。

Happy hacking!

Goa v3 で HTTP リクエストをコントローラー内で参照したい

概要

Goa v1 では、コントローラーのコンテキストに http.Request がまるっと入っていたので、 コントローラー内でリクエストを見たり、ログに出したり簡単にできたのですが、v3 では、gRPC との絡みもあるので、コントローラー(サービスメソッド)で取れるのは、基本的にはコンテキストとデザインで指定したペイロードだけです。

HTTPのAPIを書いているとどうしても v3 でも元の http.Request が欲しくなったりします。

どう解決するか?

どう解決するのが一番よいのかよく分からないところもありますが、コンテキストに http.Requset を含めるミドルウェアを書いて解決してみました。

github.com

リクエストをコンテキストに含める

リクエストをいったん全部読み出して Payload を記録しておきます。 読み出してしまったリクエストにはまた読み出せるように Reader を戻しておきます。

func NewRequestRecorder(r *http.Request) RequestRecorder {
    b, err := ioutil.ReadAll(r.Body)
    if err == nil {
        _ = r.Body.Close()
        r.Body = ioutil.NopCloser(bytes.NewReader(b))
    }
    return RequestRecorder{
        Request: r,
        Payload: b,
    }
}

これを ctx に入れておけばあとで取り出して参照できます。

レスポンスも記録しておきたい

API を書いていると、API のアクセスを入力とレスポンス合わせて記録しておきたいことがあります。実は v1 ではレスポンスも記録できるように Goa が仕組みを用意していました。 v3 でも同じようなことが出来るように、レスポンスをバッファにも元の http.ResposeWriter にも書けるような仕組みを用意しました。

Tracker はリクエストのコピーを持つ http.ResponseWriter です。 これに元の http.ResponseWriter を渡して、代わりに Tracker 自身を http.ResponseWriter として渡してやると、バッファにも書き込むようになります。

type Tracker struct {
    // リクエストのコピー
    Request RequestRecorder
    // レスポンスを記録するバッファ
    Response ResponseRecorder
        // 元のレスポンスライター
    http.ResponseWriter
}

// 自身と元の http.Writer に書き込む
func (r *Tracker) Write(b []byte) (int, error) {
    if !r.Written() {
        r.WriteHeader(http.StatusOK)
    }
    r.Response.Body.Write(b)
    return r.ResponseWriter.Write(b)
}

ミドルウエアでは、これ自体を http.ResponseWriter として渡しています。

func Trace(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tracker := NewTracker(w, r)
        ctx := r.Context()
        ctx = context.WithValue(ctx, TrackerKey, tracker)
        h.ServeHTTP(tracker, r.WithContext(ctx))
    })
}

なんかもっとシュッとした方法がある気がします。もっと簡単にやれるよ!とか、 gRPC も含めてもっと上手くやれるよ!とかありましたら是非教えてください。

Happy hacking!

Goa v3 で正常系 HTTP レスポンスが複数ある場合

概要

Goa ではエラーレスポンスが複数ある場合は、返す可能性のあるエラーをデザインで定義しておけば、 サービスメソッドでそれらのエラーを(gen 下に生成されるエラー構築用関数で組み立てて)返せばエラーレスポンスを出し分けられます。 しかし、正常系で複数のステータス、たとえば OK と Acceped と Created みたいなとき、サービスメソッドは戻り値とエラーしか取らないので、 普通にやるとステータスを出し分けることが出来ません。

これを解決するには Tag をつけてやればいいとのこと。

デザイン

結論から言うと、レスポンスにレスポンスのステータスを区別するための Attribute が必要です。 サービスメソッドでここに文字列を指定して、デザインで指定されてるのがあればそのステータスになります。

なので、レスポンスタイプは Primitive だとダメです。要するに ResultType で作るようなやつが必要です。

HTTP のレスポンスに、Tag("outcome", "accepted") のようにタグを埋め込みます。 これは、レスポンスの outcome Attribute が accepted であるときを意味します。

実際、下の例の CalcResult には outcome があります。ただ、この Attribute はこのままだとレスポンスに表示されてしまうので、 表示されないように Meta("struct:tag:json", "-") をつけておくのがよいでしょう。 `

デザインは次のようになります。

package design

import (
    . "goa.design/goa/v3/dsl"
)

var CalcResult = ResultType("application/vnd.calc+json", func() {
    Attribute("x", Int)
    Attribute("outcome", func() {
        Meta("struct:tag:json", "-")
    })
    Required("x", "outcome")
})

var _ = Service("calc", func() {
    Method("add", func() {
        Payload(func() {
            Attribute("a", Int)
            Attribute("b", Int)
            Required("a", "b")
        })
        Result(CalcResult)
        HTTP(func() {
            GET("/add/{a}/{b}")
            Response(StatusOK)
            Response(StatusAccepted, func() {
                Tag("outcome", "accepted")
            })
            Response(StatusCreated, func() {
                Tag("outcome", "created")
            })
        })
    })
})

これで、サービスメソッドでレスポンスを返す際に、outcome の値をデザイン時に指定したものに設定すれば、そのレスポンスステータスで出力してくれます。

func (s *calcsrvc) Add(ctx context.Context, p *calc.AddPayload) (res *calc.Calc, err error) {
    res.Outcome = "accepted"
    return
}

gRPC と統一的に扱うためにしかたなかったんやー感があふれ出していますね・・・(^^ゞ

Happy hacking!

Goa v3 のテストをシュッとする

はじめに

Goa というのは、Go で API を書くためのフレームワークで、APIデザインを Go のコードとして DSL を使って書けて、デザインからコードの生成、ビジネスロジックを実装というサイクルと上手に回していくことを目的にしています。

統一的なバリデーション、エラー処理、OpenAPIドキュメントの生成など API 開発で必要なあれこれが用意されています。

Goa v3 は今年の5月にリリースされました。v3 では、HTTP だけでなく、gRPC もサポートし、HTTP と gRPC の2つのトランスポートに対して共通のデザインを書くことが出来るようになっています。

Goa v3 では v1 で生成されていたようなテスト用ヘルパー関数を生成しなくなりました。また、v3 のサービスは、サービス毎にパッケージが切られてしまうので、interface での取り回しの自由も少なく、テスト用の環境を整えるのが少し面倒でした。Goa v3 用の(HTTP トランスポートの)テストヘルパーを用意したので、これを使ってテストする方法を紹介したいと思います。

gRPC を使っている場合でも、HTTP を用意しておけば特殊なものでなければサービスメソッドは共通なので、この方法でテストしてもいいのかも・・・。

サービスメソッドをテストする

サービスメソッドは REST API でいうところのエンドポイントです。サンプルとして、次のようなデザインを考えます。

    var _ = Service("calc", func() {
        Description("The calc service performs operations on numbers.")
        Method("add", func() {
            Payload(func() {
                Field(1, "a", Int, "Left operand")
                Field(2, "b", Int, "Right operand")
                Required("a", "b")
            })
            Result(Int)
            HTTP(func() {
                GET("/add/{a}/{b}")
                Response(StatusOK)
            })
        })
    })

テストしたいエンドポイントは、/add/{a}/{b} で、これは単純に ab を足すものとします。たとえば /add/1/2 とすると、レスポンスはステータス OK(200) が返ってきて、ボディは 3 が返ってくる事をテストしていきます。 goa gen すると、gen/http の下にサービス毎のフォルダが切られてパッケージ化されたコードが生成されます。

やっかいなのは、サービス毎にパッケージ化されてしまうので、サービス毎の Server は共通して使えません(パッケージが違うから)。ぎりぎり共通で使えそうなのは、エンドポイント用の http.Handler を生成するコンストラクターと、そのハンドラをサービスにマウントする関数だけです。

テストヘルパー

テストを効率よく書けるようにするために、エンドポイント用の http.Handler を生成するコンストラクターと、そのハンドラをサービスにマウントする関数、エンドポイントを指定すると、テスト用のチェッカーを用意するヘルパーを作成しました。

https://github.com/ikawaha/goahttpcheck

使い方は

  1. チェッカーを生成
    1. 必要ならオプションを設定してください。
  2. goa gen で生成されるメソッドのhttp.Handlerを生成するコンストラクター、ハンドラをサービスるにマウントする関数、エンドポイントをチェッカーに設定します。
  3. もし必要ならミドルウエアを Use 関数で追加します。
  4. Test 関数を呼び出します。

Test 関数は ivpusic/httpcheck の http checker を返すので、あとはこれに従ってテストを調整します。

上の足し算のエンドポイントの正常系テストする例は次です。

    package calcapi
    
    import (
       "encoding/json"
       "io/ioutil"
       "log"
       "net/http"
       "strings"
       "testing"
    
       "calc/gen/calc"
       "calc/gen/http/calc/server"
       "github.com/ikawaha/goahttpcheck"
       goa "goa.design/goa/v3/pkg"
    )
    
    func TestCalcsrvc_Add(t *testing.T) {
       var logger log.Logger
       checker := goahttpcheck.New()
       // テストしたいエンドポイントをマウントします
       checker.Mount(server.NewAddHandler, server.MountAddHandler, calc.NewAddEndpoint(NewCalc(&logger)))
    
       // エンドポイントをテストします
       checker.Test(t, http.MethodGet, "/add/1/2").
          Check().
          HasStatus(http.StatusOK) // OK が返ってくるはず
    }

レスポンスの値などもチェックしたければさらに細かくテストできます。

詳細は https://github.com/ivpusic/httpcheck を参照してください。

    checker.Test(t, http.MethodGet, "/add/1/2").
       Check().
       HasStatus(http.StatusOK).
       Cb(func(r *http.Response) {
          b, err := ioutil.ReadAll(r.Body)
          if err != nil {
             t.Fatalf("unexpected error, %v", err)
          }
          r.Body.Close()
          if got, expected := strings.TrimSpace(string(b)), "3"; got != expected {
             t.Errorf("got %+v, expected %v", b, expected)
          }
       })

異常系のテストも見てみましょう。足し算のエンドポイントを少し修正して、割り算のエンドポイントを用意します。割り算では0で割ろうとするとエラーを返すようになっているものとします。

    Method("div", func() {
       Error("zero_division", ErrorResult)
       Payload(func() {
          Field(1, "a", Int, "Left operand")
          Field(2, "b", Int, "Right operand")
          Required("a", "b")
       })
       Result(Int)
       HTTP(func() {
          GET("/div/{a}/{b}")
          Response(StatusOK)
          Response("zero_division", StatusBadRequest)
       })
    })

テストはこんな風に書けます。

    func TestCalcsrvc_Div(t *testing.T) {
       var logger log.Logger
       checker := goahttpcheck.New()
       checker.Mount(server.NewDivHandler, server.MountDivHandler, calc.NewDivEndpoint(NewCalc(&logger)))
    
       // see. https://github.com/ivpusic/httpcheck
       checker.Test(t, "GET", "/div/1/0").
          Check().
          HasStatus(http.StatusBadRequest).
          Cb(func(r *http.Response) {
             b, err := ioutil.ReadAll(r.Body)
             if err != nil {
                t.Fatalf("unexpected error, %v", err)
             }
             r.Body.Close()
             var resp goa.ServiceError
             if err := json.Unmarshal(b, &resp); err != nil {
                t.Fatalf("unexpected error, %v", err)
             }
             if got, expected := resp.Message, "zero division error"; got != expected {
                t.Errorf("got %+v, expected %v", got, expected)
             }
          })
    }

見通しよく非常にスッキリ書けます。テストが書きやすいと安心ですね ╭( ・ㅂ・)و ̑̑

おわりに

HTTPとgRPCの2つのトランスポート統一的にデザインするという新たな挑戦が盛り込まれた Goa v3 が今年の5月にリリースされて、半年がたちました。HTTP による REST API を構築していた v1 ユーザーからすると、DSL の変更などもあり少し敷居が高いように感じます。v3 はまだこなれていない部分もありますが、みんなが使い始めて細かな問題点が報告され修正されるようになってきました。

僕はこの仕組みがかなり気に入っていて、事ある毎に推してるんですが、「コード生成はどうなの?」とか「標準で書いてなんぼじゃない?」みたいな意見もよく聞きます。なかなか「使ってる人は使ってるツール」から抜け出せない感はあります。v1 から v3 への移行などだいぶ知見も溜まってきたのですが、なかなか意見を交換できる人に出会えません。「ウチでも使ってるよー」という方がいらっしゃいましたら是非お教えいただきたいです。

Goa いいよ、Goa!

Happy hacking!

go.mod で利用している外部ライブラリをforkして修正したのを使う

概要

外部ライブラリを使ってると、ライブラリを修正したいとか急に対応しなきゃいけないバグとかあったりして、 本家でマージされるのを待ってられないこともあります。

fork して自分で直したりして解決できたとして、すでに自分のプログラムで利用しているので、 fork してきたライブラリの全部の import パスを直すのは面倒すぎます。

そう go.mod 対応前ならね・・・

ということで対応の備忘録。

手順

  • 該当ライブラリを fork
  • fork したライブラリの go.mod のモジュール名を自分のものに修正する
    • github.com/foo/baa なら github.com/ikawaha/baa みたいにする
    • このときタグを打っておいてもいいかもしれない
  • ライブラリを利用しているプロジェクトの go.mod で該当ライブラリから fork したライブラリに replace をおこなう
    • replace github.com/foo/baa => github.com/ikawaha/baa v1.0.1

手順さえ抑えておけば簡単だ

Happy hacking!

kagome v1.11.1 をリリースしました。あるいは sync.Pool にまつわるバグの修正

概要

某フリマアプリの会社でクエリを分解するのに kagome を使っていたそうなのですが(今は知らない)、ときどき変な解析結果になるとレポートをいただきました。 なんか、最初は正しく動いているんだけど、しばらくしておかしくなり、まれにまた正常動作に戻る・・・と。

聞いただけでやっかいな類いのバグですが、原因が分かったので顛末を記録しておきます。

このバグは kagome v1.11.1 (kagome.ipadic は v1.1.1)で対応済みです。

tl;dr

形態素解析kagome では、形態素解析をおこなう際にラティスと呼ばれるグラフ構造を構築するのですが、 このグラフに使うデータ(ラティスの外枠とノード部分)を再利用するために sync.Pool を利用しています。

ラティスの外枠(ノードを格納するスライス)を戻すときに、ノードのポインタを握ったまま sync.Pool に戻していて、 どうやらこれが原因のバグだったようです。スライスの長さを 0 にするだけでなく、nil でクリアすることでバグが解消しました。

f:id:ikawaha:20191021144055p:plain
ラティス

バグ

報告

UniDic を使っているが、形態素区切りがまれ異なる。

通常

f:id:ikawaha:20191021152113p:plain
通常の区切れ方(形態素として合ってるとかではない)

おかしな出力

f:id:ikawaha:20191021152356p:plain
普段と異なるおかしな出力

おかしな出力の時、コスト最小のパスが選ばれていない。

原因と修正

ノードを入れたスライスと、ノード自体をそれぞれ sync.Pool で管理していました。 ノードを入れたスライスを sync.Pool に戻すとき、スライスの長さを 0 にして sync.Pool に戻していましたが、このやり方が悪かったようです。 スライスの長さを 0 に戻す前に、スライスの要素からノードへの参照をクリアしておく必要があったみたいです。

負荷かけるためにサーバ書いて goroutine 増やしたりいろいろテストしてみたんですが手元では再現できませんでした。

ですが、大規模に goroutine で実行するとスライスからの参照が残ってしまっていて(?)変なことになることがあったようです。 さすが日本最大級フリマアプリの会社。最大級の負荷テスト 😇

修正してからしばらく動かしてもらいましたが、修正後に変な挙動はなかったとのことです。

github.com

教訓

sync.Pool を使うときは、

  • 同じリソースを2度戻さない
    • panic したりせず動いたりする
  • Pool に戻す前に他のリソースへの参照があればクリアしておく

あと、プロダクションで kagome を使う場合は作者にこっそり使っていると知らせておくとよさそうです。 修正したことをお知らせできるかもしれません。

Happy hacking!

brew をやめて go を go get でインストールするようにした

概要

mac 使ってて、go を brew でインストールしてたんですけど、バージョン上がって brew に上がってくるまでちょっと待ってなきゃいけないのと、複数のバージョンを試したいときがあって go get で go 自体を管理するようにしてみました。

前提

go get が必要なので、(brew などでインストールした) go がないとだめです。

インストール

go1.13.3 をインストールしてみます。

go get golang.org/dl/go1.13.3

こうすると $GOBIN に go1.13.3 というコマンドがインストールされます。 この時点で、このコマンドはインストーラーです。

go1.13.3 download

を実行すると、sdk がダウンロードされます。SDK ($GOROOT) のダウンロード先は $HOME/sdk/go1.13.3 のようになります。そして、$GOBIN にある go1.13.3 が go のコマンドになります。

毎回バージョン番号の付いたコマンドを打つのもアレなので、シンボリックリンク$GOBIN/go として作っておけばいいでしょう。

ここまできたら brew から go をアンインストールできます。

追記

sdk の位置を変えることは出来ないのかな・・・

Happy hacking!

goa v1 を Go Module 環境でつかう(改)

概要

goa v1 を Go Module 環境でつかう - 押してダメならふて寝しろ でツールを Go Module 環境で管理する方法を書いたけど、このやり方はちょっと変だったので訂正。 songmu=san に go install で大丈夫だよって教えてもらいました。

方法

プロジェクトフォルダで作業するとします。プロジェクトフォルダ直下の ./bin フォルダに goagen を配置します。 バージョンは Go Module で管理されているものとします。 たとえば、v1.4.1 を使っていて、go.mod にその記述があるとします。

go install を使って、

GOBIN=$(pwd)/bin go install github.com/goadesign/goa/goagen

ただし、GOBIN は絶対パスで指定する必要があります。

もしくは go build を使って、

go build -o ./bin github.com/goadesign/goa/goagen

ちゃんと v1.4.1 のバイナリが出来てるか確認。

 $ ./bin/goagen version
goagen v1.4.1
The goa generation tool.

ちゃんと分かってスッキリした気がする。もっといい方法とか注意点とかあったら教えてください 🙇‍♂️

Happy hacking!

goa v1 を Go Module 環境でつかう

追記

やり方が変だったので訂正を書きました

ikawaha.hateblo.jp


概要

goa v1 は Go Module 環境にも対応しています。 Go Module 環境でないときは goa を vendoring で使うというテクニックがあったわけですが、 go mod vendor しても、goagen パッケージは vendor 配下に落ちてきません。 goagen パッケージはコマンドしか入ってなくて、参照されないからですかね。

ようするに Goa のバージョンを折角 go mod で指定しているのだから、goagen のバージョンもきっちり合わせたい。 どうすればいいか?を解決します。

tl;dr;

プロジェクトフォルダで作業するとします。プロジェクトフォルダ直下の ./bin フォルダに goagen を配置します。 バージョンは最新コミット 1a0d8d6d07a8f36e317f2ecf1a57e9ba0702a584 版を利用することとします。

GOBIN=$(pwd)/bin go get -u github.com/goadesign/goa/...@1a0d8d6d07a8f36e317f2ecf1a57e9ba0702a584

解説

プロジェクトフォルダで Goa のバージョンを指定して go get すると go.mod に Goa のバージョンが指定されます。 このとき一緒に goagen もインストールされますが、goagen はプロジェクトごとに管理したいので、GOBIN を指定して プロジェクトフォルダ下に配置します。ここで注意したいのは、GOBIN絶対パスで指定する必要がある事です。

これで、プロジェクトごとに goagen を Goa のパッケージと関連づけて管理できます。 あとはいつも通り goagen を利用すれば、パッケージパスを解決してコードを生成してくれます。

vendor 下でゴニョゴニョするよりスッキリした気もしますね。

Happy hacking!

メモ:pecoをbashで使う

概要

peco を bash で使うときの設定メモ。

元のスクリプトはこちら。

qiita.com

Ctr-C で中断したときに history -d 0 が実行されてしまってエラーがコンソールに出てしまうのだけ修正。

export HISTSIZE=10000
export HISTCONTROL=ignoredups
export HISTIGNORE="fg:bg:history*:cd:ls:exit:pwd"

peco-history() {
  local NUM=$(history | wc -l)
  local FIRST=$((-1*(NUM-1)))

  if [ $FIRST -eq 0 -a $HISTCMD -ge 2 ] ; then
    # Remove the last entry, "peco-history"
    history -d $((HISTCMD-1))
    echo "No history" >&2
    return
  fi

  local CMD=$(fc -l $FIRST | sort -k 2 -k 1nr | uniq -f 1 | sort -nr | sed -E 's/^[0-9]+[[:blank:]]+//' | peco | head -n 1)

  if [ -n "$CMD" ] ; then
    # Replace the last entry, "peco-history", with $CMD
    history -s $CMD

    if type osascript > /dev/null 2>&1 ; then
      # Send UP keystroke to console
      (osascript -e 'tell application "System Events" to keystroke (ASCII character 30)' &)
    fi

    # Uncomment below to execute it here directly
    # echo $CMD >&2
    # eval $CMD
  elif [ $HISTCMD -ge 2 ]; then
    # Remove the last entry, "peco-history"
    history -d $((HISTCMD-1))
  fi
}
bind -x '"\C-r":peco-history'

/以上

Goa v3 Getting Started をやってみる

概要

Goa v3 のちょっとした説明からサンプルデザインでのサーバ起動、アクセス、swagger-ui の利用までを簡単にまとめました。

Goa v3 について

Goa はひとつのデザインを書くだけで、サーバ・クライアント、swagger ドキュメントを生成できる API フレームワークです。

最近、プロジェクトが進み、Goa に v2, v3 という2つのバージョンが加わりました。 といっても、v2, v3 の違いは go.mod サポートなし/ありの違いだけです。 私見としては、v2 は過渡期に用意されたという位置づけで、v3 に移行していくのかなと思います。 また、go 1.13 から GO111MODULE=auto の振る舞いが変更されて、go.mod 対応必須の環境が整いつつあることを考えると、 これから Goa の新しいバージョンを試すなら v3 をやっていくのがいいのではないかと思います。

Goa v1 と v3 の違い

v3 では v1 での知見が生かされて、機能がかなり整理され、個々の DSL の位置づけなどが明確になりました。 また、v3 では HTTP だけでなく、gRPC をサポートすることになり、HTTP と gRPC の2つのトランスポートをほとんど意識することなく、 ビジネスロジックを作成することが出来ます。

もちろん swagger ドキュメントの生成もサポートしています。

機能が整理されたので、v3 は v1 とでは DSL の書き方がかなり変わった部分もありますが、DSL が整理されたので、 慣れれば v1 よりもデザインの見通しがよさそうです。

ドキュメント

Goa v3 のドキュメントは以下です。

goa.design

また、日本語バージョンもあります。

goa.design

準備

  • Goa v3 を go get します
    • インストール先は $GOPATH/goa.desing/goa であることに注意してください
      • go get -u goa.design/goa/v3
      • go get -u goa.design/goa/v3/...
  • 以下の2つを用意します。これは、goa.desing/goa に入っている Makefile を利用して、make depend して用意することも出来ます。

参考:make depend

depend:
        @go get -v $(DEPEND)
        @go install github.com/hashicorp/go-getter/cmd/go-getter && \
                go-getter https://github.com/google/protobuf/releases/download/v$(PROTOC_VERSION)/$(PROTOC).zip $(PROTOC) && \
                cp $(PROTOC_EXEC) $(GOBIN) && \
                rm -r $(PROTOC) && \
                echo "`protoc --version`"
        @go install github.com/golang/protobuf/protoc-gen-go
        @go get -t -v ./...
  • デザインを作成するモジュール用ディレクトリを用意します(プロジェクトのディレクトリになります)
    • どこかに calc というフォルダを用意します
    • メモ:GoLandを利用している場合は go mod を有効にしておきましょう

Getting Started

goa.desing の Getting Started に沿って説明していきます。

デザインを用意してひな形コードを生成

  • 作業フォルダを用意します
    • (go 1.13 より前のバージョンでは環境変数 GO111MODULE=on で作業します)
  • goa.desing から calc のデザインをコピーして、desing/calc.go として保存します
    • swagger の位置は ../../gen/http/openapi.json になっていますが、実行位置からの相対位置なので、作業ディレクトリで起動するなら ./gen/http/openapi.json にしておきましょう
package design

import (
    . "goa.design/goa/v3/dsl"
)

var _ = API("calc", func() {
    Title("Calculator Service")
    Description("Service for adding numbers, a Goa teaser")
    Server("calc", func() {
        Host("localhost", func() {
            URI("http://localhost:8000")
            URI("grpc://localhost:8080")
        })
    })
})

var _ = Service("calc", func() {
    Description("The calc service performs operations on numbers.")

    Method("add", func() {
        Payload(func() {
            Field(1, "a", Int, "Left operand")
            Field(2, "b", Int, "Right operand")
            Required("a", "b")
        })

        Result(Int)

        HTTP(func() {
            GET("/add/{a}/{b}")
        })

        GRPC(func() {
        })
    })

    Files("/openapi.json", "./gen/http/openapi.json")
})

goa gen

  • go mod init calc します(パッケージ名 calc を指定します)
  • goa gen calc/design
    • ここで指定している calc は mod に指定したパッケージ名

goa gen を実行すると、gen ディレクトリ以下に生成されたコードが配置されます。 gen 以下のコードは goa gen するたびに上書きされます。ユーザーが gen ディレクトリ以下のファイルを編集することは ありません

goa example

example で作成されるファイルはユーザーが調整するためのサンプル実装という位置づけです。 なので、ファイルがすでに存在する場合は 上書きされません。 上書きしたい場合はすでにあるファイルを消したり、待避したりしてから goa example します。

ビジネスロジックを実装

サービスメソッド(v1 ではコントローラと呼んでいたもの)を実装します。

calc.go というファイルがプロジェクト直下に生成されるので、このサービスメソッドを実装します。

    // Add implements add.
    func (s *calcsrvc) Add(ctx context.Context, p *calc.AddPayload) (res int, err error) {
       s.logger.Print("calc.add")
       return p.A + p.B, nil     // ← a と b を足し算するビジネスロジックを足しました
    }

起動

サーバをビルド&起動します。

ビルド

$   go build ./cmd/calc

起動

$  ./calc

サンプルでは、HTTP と gRPC の2つのトランスポートでサーバが起動します。

  • HTTP port:8000
  • gRPC port :8080

試しに HTTP にアクセスしてみます。a=1 b=2 でアクセスして 3 が返ってくる例です。

$ curl -iii -XGET localhost:8000/add/1/2
    HTTP/1.1 200 OK
    Content-Type: application/json
    Date: Mon, 10 Jun 2019 08:45:02 GMT
    Content-Length: 2
    
    3

このときのサーバログは以下のようになります。

$ ./calc
[calcapi] 16:08:56 HTTP "Add" mounted on GET /add/{a}/{b}
[calcapi] 16:08:56 HTTP "./gen/http/openapi.json" mounted on GET /openapi.json
[calcapi] 16:08:56 serving gRPC method calc.Calc/Add
[calcapi] 16:08:56 HTTP server listening on "localhost:8000"
[calcapi] 16:08:56 gRPC server listening on "localhost:8080"
[calcapi] 16:09:00  id=YIHAytTy req=GET /add/1/2 from=127.0.0.1
[calcapi] 16:09:00 calc.add
[calcapi] 16:09:00  id=YIHAytTy status=200 bytes=2 time=235.817µs
[calcapi] 16:09:04  id=9at8zmRX method=/calc.Calc/Add bytes=4
[calcapi] 16:09:04 calc.add
[calcapi] 16:09:04  id=9at8zmRX status=OK bytes=2 time=201.201µs

クライアントをビルド

HTTP は curl で確認できますが、gRPC はそうもいかないのでクライアントを用意します。 クライアントもすでに goa example で生成されているので、ビルドするだけです。

$ go build ./cmd/calc-cli

メモ: calc-cli$GOBIN/calc-cliに配置されます

実際クライアントを使ってみます。まずは gRPC にアクセスしてみます。

    $ calc-cli --url="grpc://localhost:8080" calc add --message '{"a": 1, "b": 2}'
    3

クライアントは HTTP にも利用可能です。

$ calc-cli --url "http://localhost:8000" calc add -a 1 -b 2
3

swagger-ui

サーバは swagger.json を返してくるだけにして、別途 docker で swagger-ui を立ち上げておきます。

docker-compose.yaml

    swagger-ui:
      environment:
        - API_URL=http://localhost:8000/openapi.json
      image: swaggerapi/swagger-ui
      ports:
        - "7777:8080"

docker-compose up -d すると localhost:7777 で swagger-ui が立ち上がるはずです。

CORS設定が必要なので、CORSをデザインに入れます。swagger-uiのCORS設定についての説明はここにあります。

https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/cors.md

v3では、CORS の DSLプラグインに切り出されています。

https://github.com/goadesign/plugins/tree/v3/cors

CORSのDSLは、APIServiceの下に設定できます。ここでは、localhost:7777からのアクセスを許容します。

    package design
    
    import (
       . "goa.design/goa/v3/dsl"
       cors "goa.design/plugins/v3/cors/dsl"
    )
    
    var _ = API("calc", func() {
       Title("Calculator Service")
       Description("Service for adding numbers, a Goa teaser")
       Server("calc", func() {
          Host("localhost", func() {
             URI("http://localhost:8000")
             URI("grpc://localhost:8080")
          })
       })
       cors.Origin("http://localhost:7777", func() {
          cors.Headers(" Content-Type", "api_key", "Authorization")
          cors.Methods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS")
       })
    })

f:id:ikawaha:20190907162415p:plain

swagger-ui が利用できるので gRPC を開発しているときもデバッグが楽になりそうです。

さいごに

最近、Goa v3 の開発が活発になってきて、いろいろ修正パッチがあたるようになってきました。 DSL が整理されて再設計されたことで、issue に対する対応も早くなっている気がします。 まだ細かな問題はあるかもしれませんが、積極的に使って育てていきたいと思えるフェーズには入っているのではないかと思います。

Happy hacking!

日本語的文章中国語文字列変換プログラム書了

概要

日本語の文章から平仮名を抜いて漢字だけをつなぎ合わせた、一見すると中国語のように見える文字列を偽中国語というらしい.いわゆるハナモゲラである.

すでに作ってらっしゃる方がいたのだけれど,形態素解析を WebAPI で行ってるとのことで,シェル芸botでは動かせないという問題があって,似たようなものをでっち上げました.

こちらを参考にさせていただきました.

qiita.com

というか,これのサンプルぐらいしか見なかったのでいろいろ微妙かもしれない. 一応,否定っぽいのがあったら という文字を突っ込むように改変してある.

ライセンスは mocobeta さんのおすすめで WTFPL だ.煮るなり焼くなり好きにしてください.PR もお待ちしています.

Change license to WTFPL · Issue #1 · ikawaha/nise · GitHub

こんな感じ

基本的にはひらがな落としてくだけなので,漢字が多い文章の方がそれっぽくなります.

github.com

$ nise 今日は学校に行かない
今日学校行不

$ nise 台風が近づいています
台風近了

$ nise 私はお酒が飲みたい
我酒飲希望

Happy hacking!