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

ベイズを全くわからない人がベイジアンフィルタを利用して、投稿データから投稿者を推定してみる

$
0
0

この記事は Go2 AdventCalendar の1日目の記事です。

ガチガチのGoネタは Go AdventCalendar の方が書いてくれると思いますので、
今回は弊社が利用している(作っている?) Unipos というサービスのデータをGoから利用して遊んでみようと思います。

Uniposとはご存知の方もいると思いますが、感謝の言葉とポイントを送り合うサービスで、感謝のコメントが社員から沢山投稿されています。
弊社の場合には比較的長めの投稿も多いため、今回は 投稿した内容(文章)から投稿者をベイズフィルタを利用して当てることができるだろうか?というのを試してみます。

ちなみに、記事の中にはGoのコードは殆ど出てきません・・・
あと、ベイズ方面は全く詳しくありません。調べながら書いてるので内容的にアレな部分は、 なめらかなマサカリ(コメント) でいただけるとありがたいです。

事前準備

  • goの実行環境
  • Uniposの投稿データ(画面をコツコツスクレイピングする or ユーザスクリプトなどで集めます)
    • Fringe81の直近の投稿データ
    • 学習用に8090件、確認用に1857件に分ける

Goでベイジアンフィルタをどう実装するか

ベイズの部分は実装できそうな雰囲気もあったのですが、実装を誤っていてもこの分野に明るくないので良い/悪いの判断ができそうにないので、 https://github.com/jbrukh/bayesian を利用しました。

こんな感じに実装することで実際に試せます

const (
    Good Class = "Good"
    Bad  Class = "Bad"
)

func Test_SimpleClassification(t *testing.T) {

    classifier := NewClassifier(Good, Bad)

    goodStuff := []string{"tall", "rich", "handsome"}
    badStuff := []string{"poor", "smelly", "ugly"}

    classifier.Learn(goodStuff, Good)
    classifier.Learn(badStuff, Bad)

    scores, likely, _ := classifier.LogScores([]string{"tall", "girl"})

    t.Log(scores) // [-27.12019549216256 -51.350019226428955]
    t.Log(likely) // 0

    probs, likely, _ := classifier.ProbScores([]string{"tall", "girl"})

    t.Log(probs) // [0.99999999997 2.99999999991e-11]
    t.Log(likely) // [0.99999999997 2.99999999991e-11]
}

上の実装例を見てわかるように、Learn(学習)させる場合には、トークン(文章を単語単位に分けたもの)で渡す必要があります。
英語の場合には基本的にスペースで区切られれているので単語分割が容易なのですが、日本語は簡単にはできません。

Goで日本語の分かち書きをする

日本語の文章の場合、先程も書いたように文章の中から単語を切り出すのは簡単ではなく、
形態素解析などを利用して分割するのが一般的なようです。

形態素解析(けいたいそかいせき、Morphological Analysis)とは、文法的な情報の注記の無い自然言語のテキストデータ(文)から、対象言語の文法や、辞書と呼ばれる単語の品詞等の情報にもとづき、形態素(Morpheme, おおまかにいえば、言語で意味を持つ最小単位)の列に分割し、それぞれの形態素の品詞等を判別する作業である。

MeCabなどが有名ですが、Goではikawahaさんの https://github.com/ikawaha/kagome という実装がありますので、それを利用します。

コード的には以下のような形で利用でき、かなり高速です。(私感

func Test_日本語分かち書き(t *testing.T) {

    to := tokenizer.New()
    tokens := to.Tokenize("最高の寿司体験")

    for _, token := range tokens {
        if token.Class == tokenizer.DUMMY {
            fmt.Printf("%s\n", token.Surface)
            continue
        }
        features := strings.Join(token.Features(), ",")
        fmt.Printf("%s\t%v\n", token.Surface, features)

    // BOS
    // 最高   名詞,一般,*,*,*,*,最高,サイコウ,サイコー
    // の  助詞,連体化,*,*,*,*,の,ノ,ノ
    // 寿司   名詞,一般,*,*,*,*,寿司,スシ,スシ
    // 体験   名詞,サ変接続,*,*,*,*,体験,タイケン,タイケン
    // EOS
    }
}

実際に動かしてみる

実装コードなどは省いてしまいましたが、完成したコードを動かしてみます。

前提にも書きましたが、学習用に8090件、確認用に1857件のデータを利用します。

チューニングなし(ベース)

正解 : 446件
不正解 : 1411件
正解率 : 24.0172%

学習データの文章が著しく短いものを省く

ただ単にカンですが、学習用のデータが一言ぐらいしかない場合のデータは良くないかな?ということで短めのものを省きます。
これもカンですが、twitter程度の長さ以下のものは省いてみます。

条件 : 140文字以下の学習データを除外する

正解 : 473件
不正解 : 1384件
正解率 : 25.4712%

ちょっと正解率が上がりました。
調子に乗ってもう少し除外条件を長くしてみます

条件 : 200文字以下の学習データを除外する

正解 : 481件
不正解 : 1376件
正解率 : 25.9020%

さらに正解率が上がりました。
もう少し長いほうが良いのでしょうか?長くしてみます。

条件 : 400文字以下の学習データを除外する

正解 : 423件
不正解 : 1434件
正解率 : 22.7787%

下がってしまいました・・・
おそらく学習対象データがトータルで 3186 件まで減ってしまったというのも問題かもしれません。

ちなみに、300件も試しましたが、 24.6634% ぐらいになってしまうので200文字付近が適切そうです。

学習データの一部の品詞を除外してみる

一般的にベイジアンフィルタをやる場合には 名詞 を抽出するとどこかで見かけました[要出典]ので、
品詞を抽出して試してみます

条件 : 名詞のみ学習データに含む

学習データは200文字以下除外

正解 : 320件
不正解 : 1537件
正解率 : 17.2321%

残念下がってしまいました・・・
とはいえめげずに別のパラメータを試してみます。

条件 : 助詞を省く

なんか 助詞 とかいらないっしょ、みたいなカジュアルなカンです。
やってみます。

学習データは200文字以下除外

正解 : 376件
不正解 : 1481件
正解率 : 20.2477%

だめ、全然だめ。

条件 : 記号を省く

ほぼさっきのノリです。記号いらないっしょ

学習データは200文字以下除外

正解 : 447件
不正解 : 1410件
正解率 : 24.0711%

最終的には

雑にパラメータをいろいろいじってみましたが、最終的には

条件 : 学習データは180文字以下を省く

という条件で

正解 : 487件
不正解 : 1370件
正解率 : 26.2251%

という感じが一番良さそうです。
ちなみにユニークなユーザ数(クラス数)は167です。

つまり

素人がわからないながらに適当に作っても四分の一(26%)ぐらいの確率で、
文章から誰が投稿したかと言うのを当てることができるようです。

この値が良いのか悪いのかはちょっとわかりませんが、
今日は子供の誕生日なのに準備も手伝わずに記事を書いていたら家庭が変な空気になりつつあるのでまとめますw

まとめ

  • わからない分野なのでほんと良くわからない。悔しい、、もう少し勉強しよう。
  • データ量が結構あってもgoは速い。優秀
  • 家庭は大事にしよう

GoでCGIしてみる

$
0
0

というわけでCGIしてみます。

一般的なGoのWebアプリケーションからの置き換え

普通、GoでWebアプリケーションを作るときは、net/httpを用いてHTTPをしゃべるサーバを立てるかと思います。もしくは、各種フレームワークが同じようなことをやるでしょう。

import "net/http"

func main() {
    http.ListenAndServe(
        ":8080",
        http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
            // do something
        }),
    )
}

CGIの場合は、Apache等のサーバがHTTPをしゃべるサーバとなり、CGIプログラムをサーバが実行します。なので、上記方法はそのまま使えません。

そこでGoにはnet/http/cgiというものがあります。これは上記net/httpListenAndServeを置き換えればいいだけです。

import (
    "net/http"
    "net/http/cgi"
)

func main() {
    cgi.Serve(
        http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
            // do something
        }),
    )
}

ローカルでの確認方法

現代においてはApacheを立てるというのも相対的に難儀な作業になってきました。そこでCGIアプリケーションを確認するための簡易的な方法を紹介します。

HTTPサーバミドルウェアのh2oを用います。以下の設定でCGIアプリケーションの確認ができます。

listen:
  port: 8081
user: username
hosts:
  "localhost":
    paths:
      /:
        file.dir: ./
        file.index: ["index.cgi"]
        file.custom-handler:
          extension: .cgi
          fastcgi.spawn:
            command: "exec $H2O_ROOT/share/h2o/fastcgi-cgi"

access-log: log/access-log
error-log: log/error-log
pid-file: log/pid-file

以上をh2o.ymlで保存し、

$ h2o -c h2o.yml

で起動します。GoのCGIアプリケーションはindex.cgiで保存しておきます。もちろんパーミッションは実行可能にしておきます。これで簡単にローカルでCGIアプリケーションの確認ができます。

実際のCGIアプリケーションを作ってみる

https://gist.github.com/mackee/2247f081ad860ed36c7b27bb6886b862

というわけで作ってみました。

image.png

頑張ってる点としては

  • カウンタがファイルサイズで記録して追記で済ませるタイプ
  • コメント欄はファイル記録
    • 当時は考えていなかったCSRFだとかを考えないといけない
    • XSSはhtml/templateがある程度防御してくれている
  • ログはファイルに書いておいてレンタルサーバでデバッグしやすくする

という感じです。

まとめ

  • 簡単にですがGoでCGIについて説明しました
  • コンテナだとかサーバレスだとかはCGIっぽいアーキテクチャかと思います。CGIで済むのであればCGIでプライベートサーバレス環境を作ってみると面白いかと思います

Go製WebToolKit Buffalo[概要編]

$
0
0

はじめに

Golang UK Conference 2017で紹介されていたGo製WebToolKit。日本ではまだまだ事例がなさそうなのと気になっていたのでREADME公式ドキュメントを読んで概要をまとめた。ここではBuffaloとは?を理解できるよう書いていく

概要

BuffaloはGoのWebエコシステムに沿ってWebアプリケーション開発が出来るソフトウェア。Goを使って楽に素早くWeb開発できるように設計されている。
ここでエコシステムと言ってるのはBuffaloは大半の機能を独自で実装しているわけではなく、既存のpackageを利用して成り立っているからである。

Buffaloはフロントエンド(JavaScript、SCSSなど)からバックエンド(データベース、ルーティングなど)をWebプロジェクトに含めた状態で生成し起動する。生成したプロジェクトでは簡単にWebアプリケーション用APIの開発ができる。

Buffaloは単なるWebフレームワークではなく、高速にサービスを開発するための総合的な開発環境とプロジェクト構造を提供する。

ちょっと英語の翻訳が上手くできてないが、一言でまとめると「RailsやLaravelのようにCLIでプロジェクトを作成したり便利なライブラリを含んだWebフレームワークみたいなモノ」と理解してよいと思う。

開発環境要件

buffaloを動作させるには以下の環境構築が必要となるがフロントエンドやデータベースを使用しない場合は設定はオプション扱いとなる。

事前インストール

  • Goの開発環境
  • $PATH$GOPATH/binを通している
  • Goversion 1.9.7以上

フロントエンドの環境(オプション)

  • Node.js ver8以上
  • yarnかnpmでwebpackをインストールしている

データベースの環境(オプション)

データベース(sqlite)を使用しない場合は設定不要
- SQLite3を使用する場合、mattn/go-sqlite3を使用するためGCCなどのCコンパイラが必要

install

$ go get -u github.com/gobuffalo/buffalo/buffalo

依存してるGo package

buffaloは車輪の再発明をせず、すでにGoコミュニティに存在する便利なpackageを利用してプロジェクトが成り立っている。
依存しているGo packageは巨人の肩に乗るという事でSHOULDERS.mdとしてリストアップされているので目を通すと良い

主な特徴

  • URLルーティング
    • route, session, cookieなどはWebToolBoxであるgorriaを使用
  • HTML Template
    • plushというテンプレートを使用。html/templateを使用する事も可能
  • 便利なTOOLBOX(bufffaloコマンド)
  • Test Suite
  • ホットリロード
  • フロントエンドパイプライン(option)
    • webpackを使用
  • Models/ORM(option)
  • タスクランナー(option)

buffaloコマンド

プロジェクトの作成

$ buffalo new <name>

[開発時]Webアプリケーションの実行

開発時はホットリロードがサポートされている。buffalo devコマンドで.go.htmlを監視しファイル更新時にはホットリロードが実行される。リロードと表現しているが内部的にはgo buildwebpack watchを実行している。

$ buffalo dev

[本番環境]Webアプリケーションの実行

本番環境ではgoビルドしたバイナリを実行する

ユニットテスト

buffalo testコマンドを実行すると、./vendorディレクトリをスキップしてtestコードを実行できる。depやglideを使用してpackage管理している場合はvendorディレクトリのテストは自動でスキップされる。

$ buffalo test

依存しているパッケージ達

READMEではShoulders of Giants(巨人の肩に乗る)と表現され、車輪の再発明はせずにすでに存在するpackageに依存している。

所感

RevelのようなフルスタックなWebフレームワークと思ったが内容を追っているとpackageを上手に組み合わせてプロジェクトを構築しているという印象。始めから大量のpackageを内包せずにオプションサポートしてるのでフルスタックという感じでもない。

gopher的にはミニマムな構成でのpackage群で頑張るカルチャーがあると思うが、例えばこれからWeb開発にGoを導入してみたいが既にあるpackageを使い楽をしたいというチームにはbuffaloは良いかも知れない。

Rails, Laravelとか使用していた人がGoでWeb開発する時に、取っつきやすさがあると思うので候補とするのはありだと思う。

実際にサンプルWebアプリケーションを作った記事は別のポストに書く予定です。

gocuiのコンポーネントライブラリを作った話

$
0
0

Go2 Advent Calendar 2018 4日目の記事です。

こんにちわ

最近GoでCUI・CLIツールを作るのにハマっています。
CUIツールを作るときにい使用しているライブラリでgocuiというのを使っています。

今日はgocuiのコンポーネントライブラリっぽいやつを作ったので、その話をすこしします。
ソースはこちらになります。

本記事を読む前に、gocuiの知識はあったほうが良いので、
こちらの記事を軽く読んでおく事をめちゃくちゃオススメします。

どういうやつ?

ターミナル上でhtmlのformっぽい入力インターフェイスを簡単に作ることができます。
ボタンやチェックボックスなどを用意してあります。

demo.gif

作った背景

以前gocuiを使用してDockerのCUIクライアントツールdocuiを作りましたが、
コンテナ作成などで必要な情報を入力するインターフェイスを自前で用意する必要がありました。

それがかなりめんどくさかったのと、
他のCUIツールを作るときに使用したいかもしれないし、調べた限りgocuiのcomponentライブラリがない、
というのもあってコンポーネントとして切り出したほうが良さそうというのがきっかけでした。

ちなみに、docuiの移植前後はこんな感じです。
左が旧バージョン、右が適用後のバージョンになります。
今更ながら、旧バージョンのUIずれているし味気ないし酷いな…

image.png

使い方

_demosにあるselectのサンプルをもとに説明していきます。

select.gif

func main() {
    gui, err := gocui.NewGui(gocui.Output256)

    if err != nil {
        panic(err)
    }
    defer gui.Close()

    if err := gui.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
        panic(err)
    }

    component.NewSelect(gui, "Programming Language:", 0, 0, 21, 10).
        AddOptions("Go", "Java", "PHP", "Python", "Ruby", "C", "C++", "C#").
        Draw()

    if err := gui.MainLoop(); err != nil && err != gocui.ErrQuit {
        panic(err)
    }
}
  • gocui.NewGui(gocui.Output256)でgocuiのインスタンスを生成しておく。
  • component.NewSelectでselect componentのインスタンスを生成する。
  • AddOptionsでselect一覧で選択したいオプションを追加していく。
  • Drawでgocuiインスタンスに諸々の設定を追加していく。
  • gui.MainLoop()を呼び出して、gocuiインスタンスに追加された設定をもとに画面描写やキーバインド処理などを実行する。

基本の流れは上記の様に、gocuiインスタンスを作成して、
それをcomponentに渡した後に、methodで各設定をしていき、Drawで渡されたgocuiインスタンスに対して設定を追加していくという流れになっています。

gocui自体はgocuiインスタンスにviewの設定を追加した後で、MainLoopで一気に処理する動きになっていて、
今回作成したcomponentライブラリはviewの細かい設定をより簡単にできるようにラップしたものになります。

他にもサンプルコードを_demosにおいてあるので、使ってみたい方は覗いてみてください。

内部処理

簡単な使い方を説明したところで、
上記のSelectcomponentの内部でどんな処理をしているかを見ていきます。

Selectの構造体は以下の様になっています。

type Select struct {
    *InputField
    options      []string // 選択するオプション一覧を保持する
    currentOpt   int // 現在選択しているオプションの配列インデックス
    isExpanded   bool // オプション一覧が開いているかどうかの判定フラグ
    ctype        ComponentType // componentのタイプ Formでcomponentの判定に使用する
    listColor    *Attributes // オプション一覧の色の定義
    listHandlers Handlers // オプション一覧を開いたときの操作の定義
}

Select自体はInputFieldcomponentを埋め込んでいて、それを拡張したcomponentになります。

以下がselect componentをnewするときの処理になります。

// NewSelect new select
func NewSelect(gui *gocui.Gui, label string, x, y, labelWidth, fieldWidth int) *Select {

    s := &Select{
        InputField:   NewInputField(gui, label, x, y, labelWidth, fieldWidth), // InputFieldのインスタンスを生成
        listHandlers: make(Handlers),
        ctype:        TypeSelect,
    }

    // Enterでオプション一覧を開くように、InputFieldのAddHandlerを使用して定義
    s.AddHandler(gocui.KeyEnter, s.expandOpt) 

    // オプション一覧の色を定義
    s.AddAttribute(gocui.ColorBlack, gocui.ColorWhite, gocui.ColorBlack, gocui.ColorGreen).
        // AddListHandlerを利用してオプション一覧を開いたとき、jk↑↓で移動、Enterで選択できるように定義
        AddListHandler('j', s.nextOpt).
        AddListHandler('k', s.preOpt).
        AddListHandler(gocui.KeyArrowDown, s.nextOpt).
        AddListHandler(gocui.KeyArrowUp, s.preOpt).
        AddListHandler(gocui.KeyEnter, s.selectOpt).
        // `InputField`を埋め込んでいるので、入力できないようにする必要がある
        SetEditable(false)

    return s
}

Selectで苦労したのはオプション一覧をどのように表示・選択できるようにするかという点です。
gocuiの仕様上、viewを作成してviewの領域内で文字を描写する必要があります。
つまり、オプション一覧を表示する時はオプションの数だけviewを定義する必要があります。
また、どのオプションを選択しているかをわかるように、フォーカス処理や選択後の閉じる処理も必要になります。

オプション一覧を表示する処理はexpandOptで行っているので、その処理を見ていきます。

func (s *Select) expandOpt(g *gocui.Gui, vi *gocui.View) error {
    if s.hasOpts() {
        s.isExpanded = true
        g.Cursor = false

        x := s.field.X
        w := s.field.W

        y := s.field.Y
        h := y + 2

        for _, opt := range s.options {
            // オプション一覧は下方向に展開していくので、yとh座標をインクリメントする
            y++
            h++

            // オプションごとviewを定義していく
            if v, err := g.SetView(opt, x, y, w, h); err != nil {
                if err != gocui.ErrUnknownView {
                    panic(err)
                }

                v.Frame = false
                v.SelFgColor = s.listColor.textColor
                v.SelBgColor = s.listColor.textBgColor
                v.FgColor = s.listColor.hilightColor
                v.BgColor = s.listColor.hilightBgColor

                // 設定したキーバインドをオプションごとに追加する
                for key, handler := range s.listHandlers {
                    if err := g.SetKeybinding(v.Name(), key, gocui.ModNone, handler); err != nil {
                        panic(err)
                    }
                }

                fmt.Fprint(v, opt)
            }

        }

        // 一覧を開いたときに選択したのオプションにフォーカスを当てる
        v, _ := g.SetCurrentView(s.options[s.currentOpt])
        v.Highlight = true
    }

    return nil
}

func (s *Select) selectOpt(g *gocui.Gui, v *gocui.View) error {
    // オプション一覧が開いていれば閉じる、開いていなければ展開する
    if !s.isExpanded {
        s.expandOpt(g, v)
    } else {
        s.closeOpt(g, v)
    }

    return nil
}

func (s *Select) nextOpt(g *gocui.Gui, v *gocui.View) error {
    maxOpt := len(s.options)
    if maxOpt == 0 {
        return nil
    }

    // 前のオプションのフォーカスを外す
    v.Highlight = false

    next := s.currentOpt + 1
    if next >= maxOpt {
        next = s.currentOpt
    }

    // 次のオプションにフォーカスを当てる
    s.currentOpt = next
    v, _ = g.SetCurrentView(s.options[next])

    v.Highlight = true

    return nil
}

func (s *Select) closeOpt(g *gocui.Gui, v *gocui.View) error {
    s.isExpanded = false
    g.Cursor = true

    // オプションリストのviewとキーバインドを削除する
    for _, opt := range s.options {
        g.DeleteView(opt)
        g.DeleteKeybindings(opt)
    }

    v, _ = g.SetCurrentView(s.GetLabel())

    v.Clear()

    // 選択したオプションを反映する
    fmt.Fprint(v, s.GetSelected())

    return nil
}

SelectでEnterを押下すると上記の処理が走り、オプションごとのview座標をインクリメントしながら描写します。
やり方自体はシンプルですが、コード量と処理量が多いのが難点ですね。

ざっくりまとめると、

  • オプションリストをEnterで動的に生成するにはオプションごとviewを作成し、移動と選択のキーバインドを追加する処理が必要
  • 移動は前のviewのフォーカスを外し、次のviewにフォーカスを当てる処理が必要
  • オプション一覧でEnterを押下すると選択したオプションを反映して、一覧を閉じる処理が必要

こういった事を考え実装する必要があります。
けっこう大変です。
いっそ違うライブラリを使ったほうが楽ではないか?と思います。

作る上で苦労したこと

主に苦労したのは

  1. componentのインターフェイスをどうするか
  2. formに各component(ボタンなど)を組み込むときの共通化の部分をどうするか

の2つです。

1.はどういうメソッドがあれば良いのか、どこまで設定値を使用者側で設定できるようにするか悩みました。
サクッと使いたい人もいれば、細かく設定(色など)したいもいるだろうけど、
ひとまずここはできるようにしておこう、あとは需要に応じてissueやプルリクで対応していけばよいかなというところで線引しています。

ではどのように線引して行ったかというと、既存のライブラリを参考しただけです。
せっかく世の中に素晴らしいライブラリがあるのに、そのUIを参考にしないのはもったいないし、
今の自分の経験値からでは出てこないようなアイディアが詰まっていることもあります。

今回componentライブラリを作成するにあたって、参考にしたのはVueのUIライブラリElementUItviewです。
特にtviewは標準でformなどが使えるので、それを参考にgocui版を作ったようなもんです。

2.はformは各componentを内包していて、それらをDraw()でまとめて処理しています。
まとめて処理するにはGoのインターフェイスを使います。

    for _, cp := range f.components {
        p := cp.GetPosition()
        if p.W > f.W {
            f.W = p.W
        }
        if p.H > f.H {
            f.H = p.H
        }
        cp.AddHandlerOnly(gocui.KeyTab, f.NextItem)
        cp.AddHandlerOnly(gocui.KeyArrowDown, f.NextItem)
        cp.AddHandlerOnly(gocui.KeyArrowUp, f.PreItem)
        cp.Draw()
    }

ここで苦労したのは、インターフェイスに定義をどうするかということです。
どんなmethodがformで必要なのかをcomponentを作りながら定義していきました。

ここが一番難しかったです。
設計できるひと、尊敬です…

作って学んだこと

先日、初めて外国の方からプルリクをいただきました。
感動して涙で目の前が見えませんでした。

自分では大したことがないモノを作ったと思っても、
世界中で誰かが見てくれて使ってくれているかもしれないから、
今まで通り、恐れずガンガン公開していこうと改めて思いました。

それも含めて、作っていて学んだことは

  • 仕様で悩むときは既存のライブラリを参考にしたほうが良い、自分にはないアイディアがそこに詰まっているから。
  • 共通処理はinterfaceを定義して使うと良い、よりソースがスマートになるから。
  • 質を気にせずに作ったものをどんどん公開したほうが良い、モチベの維持と勉強になるから。

です。

余談(gocuiの今後とそのかわりになるもの)

gocuiの今後ですが、作者自身があまり活動していないようで、
プルリクがマージされる気配もなさそうなので、新機能が追加されることがあまり望めないかなと思っています。

gocuiの代わりになるものをいくつかピックアップした中で一番良さそうなのがtviewでした。
gocuiと比べてtviewはまだ生まれて1年くらいのようで、
開発がそれなりに活発でformやtableなどのcomponentは標準搭載してあるのでリッチなCUIライブラリです。

tviewはhtmlの思想をいくつか取り込んでいるんだなというのがすこし使ってみた感想です。
なので、htmlをある程度しっている方であればそれほど使い方で悩むことはないんじゃないかなと思います。

興味ある方はtviewを覗いてみてください。
demosでサンプルを見れるので学習にも役立つと思います。

最後に

「作ったものは質を気にせずにどんどん公開していこう、不幸になる人なんていないから」
というのが実はこの記事で一番言いたいことだったりします。

大したやつじゃないと自分が思っても、どこかで誰かの役に立ったりするのが個人的にすごく嬉しいです。
この記事を読んで、自分も公開してみようかなって方いましたらぜひ公開していきましょ。

ちょっと使いやすくしたcutコマンド作成中

AWS Lambda + Amazon SES + aws-sdk-go で添付ファイル付きメール送信

fmt.Printfなんかこわくない

$
0
0

はじめに

Goのfmtパッケージのprintf系の関数

  • Fprintf
  • Printf
  • Sprintf

のフォーマットの指定方法についてまとめました。

Goでは書式指定子 %... のことを verb と表記しています。

すべての型に使えるverb

%v

値のデフォルトのフォーマットでの表現を出力する。

基本型の場合

verb
論理値(bool) %t
符号付き整数(int, int8など) %d
符号なし整数(uint, uint8など) %d
浮動小数点数(float64など) %g
複素数(complex128など) %g
文字列(string) %s
チャネル(chan) %p
ポインタ(pointer) %p
package main

import (
    "fmt"
)

func main() {
    fmt.Printf("%v\n", true)
    fmt.Printf("%v\n", 42)
    fmt.Printf("%v\n", uint(42))
    fmt.Printf("%v\n", 12.345)
    fmt.Printf("%v\n", 1-2i)
    fmt.Printf("%v\n", "寿司🍣Beer🍺")
    fmt.Printf("%v\n", make(chan bool))
    fmt.Printf("%v\n", new(int))
}
true
42
42
12.345
(1-2i)
寿司🍣Beer🍺
0x434080
0x416028

https://goplay.space/#z9gPcDYzgkV

コンポジット型の場合

以下のようになり、さらに各要素に対して再帰的に %v でのフォーマットをしたものが結果として出力される。

フォーマット
構造体(struct) {フィールド1 フィールド2 ...}
構造体のポインタ &{フィールド1 フィールド2 ...}
配列・スライス(array, slice) [要素1 要素2 ...]
配列・スライスのポインタ &[要素1 要素2 ...]
マップ(map) map[キー1:値1 キー2:値2 ...]
マップのポインタ &map[キー1:値1 キー2:値2 ...]
package main

import (
    "fmt"
    "net/http"
)

func main() {
    fmt.Printf("%v\n", http.Client{})
    fmt.Printf("%v\n", &http.Client{})
    fmt.Printf("%v\n", [...]int{1, 2, 3})
    fmt.Printf("%v\n", &[...]int{1, 2, 3})
    fmt.Printf("%v\n", []int{1, 2, 3})
    fmt.Printf("%v\n", &[]int{1, 2, 3})
    fmt.Printf("%v\n", map[string]int{"寿司": 1000, "ビール": 500})
    fmt.Printf("%v\n", &map[string]int{"寿司": 1000, "ビール": 500})
}
{<nil> <nil> <nil> 0s}
&{<nil> <nil> <nil> 0s}
[1 2 3]
&[1 2 3]
[1 2 3]
&[1 2 3]
map[寿司:1000 ビール:500]
&map[寿司:1000 ビール:500]

https://goplay.space/#aoO0hoy_p6a

%+v

%vと同じだが、構造体の場合にフィールド名を出力する。

package main

import (
    "fmt"
    "net/http"
)

func main() {
    fmt.Printf("%v\n", http.Client{})
    fmt.Printf("%+v\n", http.Client{})
}
{<nil> <nil> <nil> 0s}
{Transport:<nil> CheckRedirect:<nil> Jar:<nil> Timeout:0s}

https://goplay.space/#lWvspeqs7ua

%#v

値のGoの文法での表現を出力する。

package main

import (
    "fmt"
    "net/http"
)

func main() {
    fmt.Printf("%#v\n", true)
    fmt.Printf("%#v\n", 42)
    fmt.Printf("%#v\n", uint(42))
    fmt.Printf("%#v\n", 12.345)
    fmt.Printf("%#v\n", 1-2i)
    fmt.Printf("%#v\n", "寿司🍣Beer🍺")
    fmt.Printf("%#v\n", make(chan bool))
    fmt.Printf("%#v\n", new(int))
    fmt.Printf("\n")
    fmt.Printf("%#v\n", http.Client{})
    fmt.Printf("%#v\n", &http.Client{})
    fmt.Printf("%#v\n", [...]int{1, 2, 3})
    fmt.Printf("%#v\n", &[...]int{1, 2, 3})
    fmt.Printf("%#v\n", []int{1, 2, 3})
    fmt.Printf("%#v\n", &[]int{1, 2, 3})
    fmt.Printf("%#v\n", map[string]int{"寿司": 1000, "ビール": 500})
    fmt.Printf("%#v\n", &map[string]int{"寿司": 1000, "ビール": 500})
}
true
42
0x2a
12.345
(1-2i)
"寿司🍣Beer🍺"
(chan bool)(0x834100)
(*int)(0x816260)

http.Client{Transport:http.RoundTripper(nil), CheckRedirect:(func(*http.Request, []*http.Request) error)(nil), Jar:http.CookieJar(nil), Timeout:0}
&http.Client{Transport:http.RoundTripper(nil), CheckRedirect:(func(*http.Request, []*http.Request) error)(nil), Jar:http.CookieJar(nil), Timeout:0}
[3]int{1, 2, 3}
&[3]int{1, 2, 3}
[]int{1, 2, 3}
&[]int{1, 2, 3}
map[string]int{"寿司":1000, "ビール":500}
&map[string]int{"寿司":1000, "ビール":500}

https://goplay.space/#fxNuoQ2vBUj

%T

値の型のGoの文法での表現を出力する。

package main

import (
    "fmt"
    "net/http"
)

func main() {
    fmt.Printf("%T\n", true)
    fmt.Printf("%T\n", 42)
    fmt.Printf("%T\n", uint(42))
    fmt.Printf("%T\n", 12.345)
    fmt.Printf("%T\n", 1-2i)
    fmt.Printf("%T\n", "寿司🍣Beer🍺")
    fmt.Printf("%T\n", make(chan bool))
    fmt.Printf("%T\n", new(int))
    fmt.Printf("\n")
    fmt.Printf("%T\n", http.Client{})
    fmt.Printf("%T\n", &http.Client{})
    fmt.Printf("%T\n", [...]int{1, 2, 3})
    fmt.Printf("%T\n", &[...]int{1, 2, 3})
    fmt.Printf("%T\n", []int{1, 2, 3})
    fmt.Printf("%T\n", &[]int{1, 2, 3})
    fmt.Printf("%T\n", map[string]int{"寿司": 1000, "ビール": 500})
    fmt.Printf("%T\n", &map[string]int{"寿司": 1000, "ビール": 500})
}
bool
int
uint
float64
complex128
string
chan bool
*int

http.Client
*http.Client
[3]int
*[3]int
[]int
*[]int
map[string]int
*map[string]int

https://goplay.space/#mpPV8cOoc0n

%%

%そのものを出力したい場合に使う。

論理値に使えるverb

%t

truefalse

package main

import (
    "fmt"
)

func main() {
    fmt.Printf("%t\n", true)
    fmt.Printf("%t\n", false)
}
true
false

https://goplay.space/#dPg7ImEakE3

整数に使えるverb

%d

10進数での表現

%b

2進数での表現

%o

8進数での表現

%x

16進数での表現(a-fは小文字)

%X

16進数での表現(A-Fは大文字)

%c

Unicodeコードポイントに対応する文字

%q

対応する文字をシングルクォート'で囲んだ文字列

package main

import (
    "fmt"
)

func main() {
    answer := 42
    fmt.Printf("%b\n", answer)
    fmt.Printf("%c\n", answer)
    fmt.Printf("%d\n", answer)
    fmt.Printf("%o\n", answer)
    fmt.Printf("%q\n", answer)
    fmt.Printf("%x\n", answer)
    fmt.Printf("%X\n", answer)
    fmt.Printf("%U\n", answer)
}
101010
*
42
52
'*'
2a
2A
U+002A

https://goplay.space/#uOjc6CIP2Cc

浮動小数点数・複素数に使えるverb

%b

小数点なしの指数表記 指数は2の累乗

%e

指数表記

%E

%eeEで表記される

%f, %F

指数表記なし

%g

指数が大きい場合は%eそうでなければ%f

%G

指数が大きい場合は%Eそうでなければ%F

package main

import (
    "fmt"
)

func main() {
    f := 12.345
    fmt.Printf("%b\n", f)
    fmt.Printf("%e\n", f)
    fmt.Printf("%E\n", f)
    fmt.Printf("%f\n", f)
    fmt.Printf("%F\n", f)
    fmt.Printf("%g\n", f)
    fmt.Printf("%G\n", f)
    fmt.Printf("%g\n", 12345678.9)
    fmt.Printf("%G\n", 12345678.9)
}
6949617174986097p-49
1.234500e+01
1.234500E+01
12.345000
12.345000
12.345
12.345
1.23456789e+07
1.23456789E+07

https://goplay.space/#x97S-f6fLcn

文字列([]byteも同じ)に使えるverb

%s

そのままの出力

%q

Goの文法上のエスケープをした文字列

%x

1バイトにつき2文字の16進数での表現(a-fは小文字)

%X

%xと同じだが、A-Fが大文字

package main

import (
    "fmt"
)

func main() {
    s := "寿司🍣Beer🍺"
    fmt.Printf("%s\n", s)
    fmt.Printf("%q\n", s)
    fmt.Printf("%x\n", s)
    fmt.Printf("%X\n", s)
}
寿司🍣Beer🍺
"寿司🍣Beer🍺"
e5afbfe58fb8f09f8da342656572f09f8dba
E5AFBFE58FB8F09F8DA342656572F09F8DBA

https://goplay.space/#4z7ki7m7BjD

width, precision

verbの直前に整数でwidthを指定することができる。
widthはrune単位で数えられる出力する文字列の長さ。
指定しない場合、値を表現するのに必要な長さになる。

precisionはwidthの直後に.を付け、その後に整数で指定できる。
.がない場合、デフォルトのprecisionになる。
.があるが数値の指定がない場合、precisionは0になる。

width, precisionには整数の代わりに*を指定することもでき、その場合次の引数の値を指定したことになる。
その場合、その引数の値は整数である必要がある。

  • 文字列([]byteでも同じ)の場合: precisionはフォーマット対象にする文字列の長さの上限。文字列が長すぎる場合は途中で切られる。長さはrune単位だが、%x%Xでフォーマットされる場合はバイト単位。
  • 浮動小数点数の場合:

    • width: 文字列で表現される際の最小の文字数
    • precision:
      • %e, %f, 小数点以下の桁数
      • %g, %G有効桁数の最大値
      • デフォルト:
        • %e, %f, %#g: 6
        • %g: 数値を表すのに必要な桁数
  • 複素数の場合: widthとprecisionの値は実数部と虚数部にそれぞれ適用され、結果は()で囲われる。

package main

import (
    "fmt"
)

func main() {
    f := 12.345
    fmt.Printf("%f\n", f)
    fmt.Printf("%12f\n", f)
    fmt.Printf("%12.2f\n", f)
    fmt.Printf("%.2f\n", f)
    fmt.Printf("%12.f\n", f)
    fmt.Printf("%e\n", f)
    fmt.Printf("%#g\n", f)
    fmt.Printf("%g\n", f)

    fmt.Printf("%f", 1-2i)
}
12.345000
   12.345000
       12.35
12.35
          12
1.234500e+01
12.3450
12.345
(1.000000-2.000000i)

https://goplay.space/#qbrm8OIcZ0V

flag

width, precisionの他にもverbの直前に置くことでフォーマットを変えられるflagがあります。

+

  • 数値の場合: 正でも符号(+)を出力する
  • %qの場合: ASCII文字だけで出力する
package main

import (
    "fmt"
)

func main() {
    fmt.Printf("%d\n", 42)
    fmt.Printf("%+d\n", 42)
    fmt.Printf("%q\n", 945)
    fmt.Printf("%+q\n", 945)
    fmt.Printf("%q\n", "寿司🍣Beer🍺")
    fmt.Printf("%+q\n", "寿司🍣Beer🍺")
}
42
+42
'α'
'\u03b1'
"寿司🍣Beer🍺"
"\u5bff\u53f8\U0001f363Beer\U0001f37a"

https://goplay.space/#Mqe5nGa0Af_E

-

左詰めにする

package main

import (
    "fmt"
)

func main() {
    fmt.Printf("%5d\n", 42)
    fmt.Printf("%-5d\n", 42)
    fmt.Printf("%10s\n", "寿司🍣Beer🍺")
    fmt.Printf("%-10s\n", "寿司🍣Beer🍺")
}
   42
42   
  寿司🍣Beer🍺
寿司🍣Beer🍺

https://goplay.space/#0eC0HaeMk-B

#

デフォルトとは異なるフォーマットにする

  • 8進数の場合(%#o): 先頭に0を付ける
  • 16進数の場合(%#x): 先頭に0xを付ける
  • 16進数(大文字)の場合(%#X): 先頭に0Xを付ける
  • ポインタの場合(%#p): 先頭の0xを付けない
  • %qの場合: strconv.CanBackquoteがtrueを返すならraw文字列を出力する
  • %e, %E, %f, %F, %g, %Gの場合: 必ず小数点を付ける
  • %g, %Gの場合: 末尾の0を省略しない
  • %Uの場合: U+0078 'x'の形式で出力する
package main

import (
    "fmt"
)

func main() {
    answer := 42
    fmt.Printf("%o\n", answer)
    fmt.Printf("%#o\n", answer)
    fmt.Printf("%x\n", answer)
    fmt.Printf("%#x\n", answer)
    fmt.Printf("%X\n", answer)
    fmt.Printf("%#X\n", answer)
    fmt.Printf("%p\n", &answer)
    fmt.Printf("%#p\n", &answer)
    fmt.Printf("%q\n", "go")
    fmt.Printf("%#q\n", "go")
    fmt.Printf("%q\n", "`go`")
    fmt.Printf("%#q\n", "`go`")
    fmt.Printf("%.f\n", 12.345)
    fmt.Printf("%#.f\n", 12.345)
    fmt.Printf("%g\n", 12.345)
    fmt.Printf("%#g\n", 12.345)
    fmt.Printf("%U\n", answer)
    fmt.Printf("%#U\n", answer)
}
52
052
2a
0x2a
2A
0X2A
0x416020
416020
"go"
`go`
"`go`"
"`go`"
12
12.
12.345
12.3450
U+002A
U+002A '*'

https://goplay.space/#2YTrvJY308B

(スペース)

  • 数値の場合: 符号のためのスペースを空ける
  • 文字列をバイト単位で表現する場合: それぞれのバイトの間にスペースを空ける
package main

import (
    "fmt"
)

func main() {
    fmt.Printf("%d\n", 42)
    fmt.Printf("% d\n", 42)
    fmt.Printf("%x\n", "寿司🍣Beer🍺")
    fmt.Printf("% x\n", "寿司🍣Beer🍺")
    fmt.Printf("%X\n", "寿司🍣Beer🍺")
    fmt.Printf("% X\n", "寿司🍣Beer🍺")
}
42
 42
e5afbfe58fb8f09f8da342656572f09f8dba
e5 af bf e5 8f b8 f0 9f 8d a3 42 65 65 72 f0 9f 8d ba
E5AFBFE58FB8F09F8DA342656572F09F8DBA
E5 AF BF E5 8F B8 F0 9F 8D A3 42 65 65 72 F0 9F 8D BA

https://goplay.space/#_KM5ZDuGfM2

0

スペースではなく、0で埋める

数値の場合: 0埋めは符号のあと

package main

import (
    "fmt"
)

func main() {
    fmt.Printf("%10s\n", "寿司🍣Beer🍺")
    fmt.Printf("%010s\n", "寿司🍣Beer🍺")
    fmt.Printf("%10.3f\n", -12.345)
    fmt.Printf("%010.3f\n", -12.345)
}
  寿司🍣Beer🍺
00寿司🍣Beer🍺
   -12.345
-00012.345

https://goplay.space/#tGs_5FUYnVy

[n]引数のインデックス指定

  • verbの直前に[n]の表記でインデックスを指定することで、フォーマットする引数を指定できる
  • *に対しても使える
  • [n]の指定をした以降はn+1番目, n+2番目, ... となる
package main

import (
    "fmt"
)

func main() {
    fmt.Printf("%[2]s %[1]s\n", "寿司🍣", "Beer🍺")
    fmt.Printf("%[3]*.[2]*[1]f\n", 12.345, 2, 8)
    fmt.Printf("%*.*f\n", 8, 2, 12.345)
    fmt.Printf("%s %s %[1]q %q", "寿司🍣", "Beer🍺")
}
Beer🍺 寿司🍣
   12.35
   12.35
寿司🍣 Beer🍺 "寿司🍣" "Beer🍺"

https://goplay.space/#dztaNduIqv0

参考

fmt - The Go Programming Language

Go で Enum を定義するときのちょっとした気遣い

$
0
0

@mosaxiv さんの代打で、Go で Enum を定義するときに気になっていたこととその解決策についてお話します。

Go にはデフォルトで Enum を定義する仕組みがないため、一般的に const 宣言内で iota を使って次のように定義することが多いと思います。

type Type int

const (
    a Type = iota
    b
    c
)

しかし、Go には Zero-Value といって変数を初期化する際に明示的に値を代入しない場合、デフォルトで割り当てられる値が決まっています。例えば int 型の変数の場合「0」です。

すると iota は「0」から始まる連続する整数を生成する識別子であるため、上で宣言した a がデフォルト値でもない限り、変数を初期化する際に明示的に値を指定していないにも関わらずデフォルト値でもない a で勝手に初期化されてしまっている状態が発生します。

文章では少し伝わりにくいと思うのでコードで示すと

type S struct {
    T Type
}

func main() {
    s := S{}
    switch s.T {
    case a:
        fmt.Println("a") // <- 実際にはここを通る
    case b:
        fmt.Println("b")
    case c:
        fmt.Println("b")
    default:
        fmt.Println("default") // <- s.T に何も設定していなかったらここを通ってほしい
    }
}

という具合です。

この問題は iota を「1」から始めることで解決できます。このちょっとしたおせっかい気遣いによって次のように意図した処理が行われます。

const (
    a Type = iota + 1 // <- 1から始める
    b
    c
)

func main() {
    s := S{}
    switch s.T {
    case a:
        fmt.Println("a")
    case b:
        fmt.Println("b")
    case c:
        fmt.Println("b")
    default:
        fmt.Println("default") // <- s.T に何も設定していないのでここを通る!
    }
}

ただこの解決策がベストかどうか分からないので、もう少しいい案があるよ!という方はコメント欄までお願いします:bow:


ssssをgolangに移植してみた

GoのWebアプリケーションのSQLロガーとパフォーマンス分析ツールを作ってみた

165行で実装するProtocol Buffersデコーダ(ミニマム版)

golang で 2 Way SQL

$
0
0

空いていたので穴埋め。

はじめに

データベースを扱うプロジェクトでは、オンコードで SQL を書く事が割と多いのですが、そういったソースコードに埋め込まれた SQL はプレースホルダを使って値を取るので直接実行する事は出来ません。

select * from foo where id = :id and bar = :bar

RDBMS によっては :変数名 ではなく ?$1 といった表記をする物もあります。こういった SQL は直接実行できない為、どうしても結果がイメージし辛くなるのですが、2 Way SQL という方法を使う事で解決できる事があります。

2 Way SQL とは

2 Way SQL は SQL のコメントに IF や ELSE、END といった制御構文を埋め込む事で、直接実行する事も出来るし、プレースホルダを使ったオンコード用の SQL としても使う事ができるといった物です。筆者が知る限り明確な仕様は見当たらないのですが、昔は Doma/S2Dao といったデータベースの抽象化レイヤと合わせて利用されて来ました。

select * from foo where id = /*id*/5 /* IF enabled */and bar = /*bar*/'foo' /*END*/

この SQL を実行するとコメントは無視される為、実際は以下が実行されます。

select * from foo where id = 5 and bar = 'foo'

このコメントに記述された IF/ELSE/END と、値の直前に書かれたコメントが 2 Way SQL です。

2 Way SQL のルール

select * from foo where id = /*id*/5 /* IF enabled */and bar = /*bar*/'foo' /*END*/

この SQL に制御パラメータ enabled を true 渡すと IF 文が有効になるため、id と bar による抽出が有効になります。また false で渡すと IF 文が無効となるため id のみの抽出となります。

select * from foo where id = :id

この様に /*bar*/'foo' はプレースホルダ bar に置き換えられます。

2 Way SQL の実装

随分前ですが golang で実装しました。

https://github.com/mattn/ways2go

以下の様に使います。

env := make(map[string]interface{})
env["enabled"] = true

sql2way := `
select
  *
from
  foo
where
  id = /*id*/5
/* IF enabled */
  and bar = /*bar*/3
/*END*/
`

sql, err := Eval(sql2way, env, ways2go.Question)

正常に評価されると以下の SQL を得る事が出来ます。

select
  *
from
  foo
where
  id = ?
  and bar = ?

データベースによってはプレースホルダの形式が異なります。ways2go では以下の形式をサポートしています。

識別 プレースホルダ
Question ?
Dollar $1
Colon :foo

これを使えばサンプルとして実行する事もでき、プログラムから実行する事もできる SQL を同じファイルで管理する事が出来ます。

まとめ

大規模なプロジェクトで SQL が散乱する様なコードベースでは SQL を外部ファイルに切り出すことでソースコードがスッキリするかもしれません。ただし実は筆者も昔に作ったまま時間が経っており、テストは書いてあるのですが実務で使った事がありません。皆さんからの PR や機能追加をお待ちしております。

Goのnet/httpとSlackのEventAPIでHTTPベースの本棚管理Botを作ってみた

$
0
0

Goのnet/httpとSlackのEventAPIで本棚管理Botを作ってみた

POSTが遅くなってしまい申し訳ありません。Go2アドベントカレンダー13日目の記事です。
今回はアドベントカレンダーに向けて、Goのnet/httpパッケージとSlackのEventAPIを使って、書籍の検索を行ってくれる本棚管理ボットを作ってみました。今回はHTTPサーバーベースのBotです。
今回の記事ではその概要と実装について説明していきたいと思います。
実際のコードはGitHubにあります。

本棚管理Botとは

普段生活していて、いろんな場面で本を購入することは多いと思います。
しかし、本を買ってみたものの、家に帰って本棚を確認してみたら、両親や兄弟が同じ本を買っていて、同じ本が2冊になってしまったという経験はないでしょうか。家だけではなく、会社や大学の研究室など様々な場所で似たようなことが起きるのではないでしょうか。
同じ本を2冊買うのを防ぐために、購入前にSlackで今本棚にある本の検索ができたら便利だと思い、今回本棚管理Bot(bookshlfという名のBot)を作りました。
できたものは以下です。
スクリーンショット 2018-12-13 22.01.34.png
写真のようにBotにメンションして、search:<文字列>という形式で検索ワードを送ると
スクリーンショット 2018-12-13 22.01.48.png

このように検索結果を返してくれるというものです。後述しますが、この検索結果は本の一覧が書かれたCSVファイルを読み込み、参照した結果を返しています。
今回このBotをGoのnet/httpパッケージとSlackのEventAPIを使ってHTTPサーバーベースのBotを作成しました。

Slack,EventAPI

まずEventAPIはメンションなどの特定のイベントが発生すると自分が指定したURLにリクエストを投げてくれます。
今回はBotユーザーへのメンションが発生した時に自分のHTTPサーバーのURLにリクエストを送信するように設定します。(設定方法は他の記事参照)

GoのHTTPサーバー

main.goは以下のようになっています。

main.go
func main() {
    http.HandleFunc("/", handler.Handle)
    http.ListenAndServe(":8080", nil)
}

/にリクエストがきたらhandler.Handle関数が呼ばれるようにしています。

handler.Handle関数

関数はの概要は以下です。長くてすいません。

handler/handler.go
func Handle(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    defer r.Body.Close()

    byteBody, err := ioutil.ReadAll(r.Body)
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    var jsonMap map[string]interface{}
    if err := json.Unmarshal(byteBody, &jsonMap); err != nil {
        fmt.Println(err)
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    fmt.Println(jsonMap)

    token := jsonMap["token"].(string)
    if token != os.Getenv("SLACK_TOKEN") {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    eventType := jsonMap["type"].(string)
    switch eventType {
    case "url_verification":
        challenge := jsonMap["challenge"].(string)
        w.WriteHeader(200)
        w.Write([]byte(challenge))
        return
    case "event_callback":
        event := jsonMap["event"].(map[string]interface{})
        eventTypeString := event["type"].(string)
        if eventTypeString == "app_mention" {
            eventText := event["text"].(string)
            stringReader := strings.NewReader(eventText)
            scanner := bufio.NewScanner(stringReader)
            scanner.Scan()
            scanner.Scan()
            text := scanner.Text()
            splitSlice := strings.Split(text, ":")
            if len(splitSlice) < 1 {
                w.WriteHeader(http.StatusBadRequest)
                return
            }
            switch splitSlice[0] {
            case "search":
                w.WriteHeader(200)
                if err != nil {
                    fmt.Fprint(os.Stderr, err)
                    return
                }
                service, err := service.NewService()
                if err != nil {
                    fmt.Fprint(os.Stderr, err)
                    return
                }
                channelName := event["channel"].(string)
                go service.SendAnswer(splitSlice[1], channelName)
                return
            }
        }
    default:
        w.WriteHeader(http.StatusBadRequest)
        return
    }
}

jsonのリクエストを受け取り、それをjson.Unmatshal関数を使ってmap[string]interface{}型にしてそこから型アサーションを使って、type.event.type.textを取り出します。これがユーザーから送られてきたメッセージになります。メッセージは

@bookshlf
saerch:HTTP

という風になるのでscanner.Scan()を2回使って、2行目のsaerch:HTTPを取り出します。その後strings.Split(text, ":")を使って:で区切りスライスに入れます。今回でいうとスライスの1番目の要素がHTTPという検索ワードになります。
この検索ワードとchannel名をgoroutineに渡して、goroutine内でCSVを使った検索を行い、Slackのchat.postMessageのAPIにPOSTリクエストを送信しています。なぜ、1回のリクエストでメッセージを返さなかったかというと、EventAPIのページに以下のことが書いてあったからです。

Respond to events with a HTTP 200 OK as soon as you can.
Avoid actually processing and reacting to events within
the same process. Implement a queue to handle inbound events after they are received.

これをみて真っ先にgoroutineだと思いつき、goroutineを使ってみました。
(goroutineの使い方はあっているかはわからない)

goroutineの中身

goroutineとして呼び出されているservice.SendAnswer関数の中身はどうなっているかというと検索ワードに応じて、CSV内の検索を行い、検索結果を文字列にしてchat.postMessageのAPIにPOSTリクエストを送信しています。service.SendAnswer関数自体の実装はあとで提示します。

CSV内文字列の検索

以下はCSVの検索の関数です。

finder/finder.go
type Finder interface {
    Find(searchWord string) ([]domain.Book, error)
    Close()
}

type CSV struct {
    reader io.ReadCloser
}

func (c *CSV) Find(searchWord string) ([]domain.Book, error) {
    bookSlice := make([]domain.Book, 0, 10)
    csvReader := csv.NewReader(c.reader)
    record, err := csvReader.ReadAll()
    if err != nil {
        fmt.Println(err)
        return nil, err
    }
    for _, row := range record {
        if strings.Contains(row[2], searchWord) && row[2] != "" && searchWord != "" {
            newBook := domain.Book{ISBN: row[0], Title: row[2], Author: row[3], Publisher: row[4]}
            bookSlice = append(bookSlice, newBook)
        }
    }
    return bookSlice, nil
}

//Close関数は省略

CSVという構造体にreaderというio.ReadCloserを持たせていますが、これはos.File型のcsvファイルが入ります。ちなみにCSV構造体はFinderインターフェースを実装しています。csv.NewReader(c.reader)関数とcsvReader.ReadAll()関数でcsvの読み取りを行なっています。
CSVファイルのフォーマットは

csv
ISBN,found,title,author,publisher,volume,series,cover

とい風になっており、今回はtitle(row[2])が検索ワードを含んでいたら、BooK構造体にセットして、スライスに入れていきます。検索ワードを含んでいるかはstrings.Contains(row[2], searchWord)で確認しています。

Book構造体

Book構造体は以下のように定義しました。

type Book struct {
    ISBN      string
    Title     string
    Author    string
    Publisher string
}

今回はCSVのISBN,title,author,publisherの項目だけを用います。
CSV検索関数ではBook構造体のスライスをservice.SendAnswer関数に返します。

service.SendAnswer関数

service.SendAnswer関数は以下のようになっています。
finder.FinderはCSV構造体が実装している、インターフェースです。実態はCSV構造体が入ります。

service/book_service.go
type BookService struct {
    finder finder.Finder
}

func (b *BookService) SendAnswer(query string, channelName string) {
    bookSlice, err := b.finder.Find(query)
    if err != nil {
        fmt.Println(err)
    }

    var sendMessage strings.Builder
    length := strconv.Itoa(len(bookSlice))
    fmt.Println(length)
    if len(bookSlice) > 0 {
        sendMessage.WriteString("本あったよ:sunglasses:\n")
        sendMessage.WriteString("検索結果/" + length + "件\n")
    } else {
        sendMessage.WriteString("残念だ...\n")
    }

    for _, book := range bookSlice {
        sendMessage.WriteString("```")
        sendMessage.WriteString(book.ToString())
        sendMessage.WriteString("```")
        sendMessage.WriteString("\n")
    }
    message.SendMessage(channelName, sendMessage.String())
    b.finder.Close()
}

b.finder.Find(query)の部分がCSVの検索の関数です。
帰ってきた、Book構造体のスライスをもとにユーザーに送信するメッセージを組み立てています。メッセージの組み立てではstrings.Builderを使っています。
book.ToString()関数はBook構造体の情報を読みやすい形に整形した文字列を返す関数です。
メッセージの組み立てが終わったら、message.SendMessage(channelName, sendMessage.String())関数を呼び出します。この関数でchat.postMessageのAPIにPOSTリクエストを送信しています。

message.SendMessage関数

message/send_message.go
func SendMessage(channelName string, message string) {
    values := url.Values{}
    token := os.Getenv("SLACK_OAUTH_TOKEN")
    values.Add("token", token)
    values.Add("channel", channelName)
    values.Add("text", message)
    values.Add("mrkdwn", "true")
    resp, err := http.PostForm("https://slack.com/api/chat.postMessage", values)
    if err != nil {
        fmt.Println(err)
    }
    defer resp.Body.Close()

    byteString, _ := ioutil.ReadAll(resp.Body)
    fmt.Println(string(byteString))
}

今回はhttp.PostFormでPOSTリクエストを送信しています。chat.PostMessageのAPIはcontent typesapplication/x-www-form-urlencoded形式をサポートしています。application/json形式もサポートしていますが、Bodyの組み立てが簡単そうなapplication/x-www-form-urlencodedを使いました。
url.Values{}を使ってリクエストのBodyを組み立てていきます。
メッセージを送るためのtokenと送信するchannelとメッセージであるtextとメッセージのマークダウン形式を認めるmrkdwnオプションを追加します。
その後http.PostForm("https://slack.com/api/chat.postMessage", values)でメッセージが送信されます。

まとめ

今回はGoのnet/httpとSlackのEventAPIを使って、HTTPベースのBotを作成しました。HTTPベースでBotが作れるのはだいぶ大きかったです。皆さんも是非HTTPベースでお好みのBotを作成してみてください。
なお、ここが間違っている、このアーキテクチャはよくないなどのご意見ありましたら、ぜひ、ご指摘よろしくお願いいたします。
スクリーンショット 2018-12-13 23.18.38.png

Goのカスタムエラーとその自動生成について

ボイラプレート編 - #golang で CLI 作るときにいつもつかうやつ

$
0
0

技術選択編 が軽バズりして嬉しかったので続編.

TL;DR

  • 便利ライブラリ & CLI つくったよ
  • 開発用ツールの依存は gex で管理してるよ

logging

zap

Blazing fast, structured, leveled logging のとおり,はやくて構造化データを吐けてログレベルも設定できるロガー.これは知ってる人も多いハズ.

自分が使うときはデバッグフラグを定義しておき,cobra.OnInitialize で logger を初期化して global logger にセットしている.

cobra.OnInitialize(func() {
    zap.ReplaceGlobals(verboseLogger)
})

ReplaceGlobals でセットした Logger は zap.L() で取り出せるので,あとはコード中の任意の場所で zap.L().Error("failed to open file", zap.Error(err), zap.String("path", path)) とかできる.

ちなみに,ReplaceGlobals を呼ばないと Nop logger が利用されるので,テストとかに影響を及ぼすことはない.便利.

verbose logger / debug logger

自分はだいたい --verbose--debug の2種類のフラグを用意しておいて,それによって logger を使い分けている.

-v もしくは --verboseINFO level までのログを出す Logger を使う.これは一般ユーザでも使う想定で,みやすいログ形式でそこそこの情報を出すようにしている.

cfg := zap.NewDevelopmentConfig()
cfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
cfg.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel)
cfg.EncoderConfig.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
    enc.AppendString(t.Local().Format("2006-01-02 15:04:05 MST"))
}

一方で --debug は主に自分が使うものなので,すべてのログ(DEBUG level)を出力している.ProductionConfig を利用すると JSON でログが出るようになるので,それをおもむろに jq に食わせてデバッグしたりする.

cfg := zap.NewProductionConfig()
cfg.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel)
cfg.DisableStacktrace = true

ery では各モジュールのインスタンスに named logger をもたせておいて,それを呼ぶようにしている.これでより jq しやすくなる.

// https://github.com/srvc/ery/blob/v0.0.1/pkg/app/proxy/manager.go#L20-L25
return &serverManager{
    mappingRepo:     mappingRepo,
    factory:         factory,
    cancellerByPort: new(sync.Map),
    log:             zap.L().Named("proxy"),
}

DEBUG level のログは「ユーザは普段見ない」「JSON で出てくるので多少量が多くても目的のログを検索しやすい」ので,開発中の print debug 代わりに使って消さずに残しておくくらいでいいのかなと考えている.

フラグハンドリング

いつも↓みたいな感じのヘルパを定義して,フラグを定義し,アプリケーション起動タイミングで logger も初期化している.

func AddLoggingFlags(cmd *cobra.Command) {
    var (
        debugEnabled, verboseEnabled bool
    )

    cmd.PersistentFlags().BoolVar(
        &debugEnabled,
        "debug",
        false,
        "Debug level output",
    )
    cmd.PersistentFlags().BoolVarP(
        &verboseEnabled,
        "verbose",
        "v",
        false,
        "Verbose level output",
    )

    cobra.OnInitialize(func() {
        switch {
        case debugEnabled:
            enableDebugLogger()
        case verboseEnabled:
            enableVerboseLogger()
        }
    })
}

ref: pkg/cli/logging.go at master · izumin5210/clig

stdio

たとえば kubectl の実装を読むと,かなり最初の方でコンストラクタから標準入出力を注入している.

// NewDefaultKubectlCommand creates the `kubectl` command with default arguments
func NewDefaultKubectlCommand() *cobra.Command {
  return NewDefaultKubectlCommandWithArgs(&defaultPluginHandler{}, os.Args, os.Stdin, os.Stdout, os.Stderr)
}

// NewDefaultKubectlCommandWithArgs creates the `kubectl` command with arguments
func NewDefaultKubectlCommandWithArgs(pluginHandler PluginHandler, args []string, in io.Reader, out, errout io.Writer) *cobra.Command {
  cmd := NewKubectlCommand(in, out, errout)

pkg/kubectl/cmd/cmd.go#L295-L303 at v1.13.1 · kubernetes/kubernetes

これをやっておくだけで,テスト時は os.Std* の代わりに new(bytes.Buffer) でモックできるようになる.

自分も CLI 実装時はこのやり方を踏襲していたが,最近はもう一段ラッパーを噛ませている.

type IO interface {
    In() io.Reader
    Out() io.Writer
    Err() io.Writer
}

io.Writerio.Reader だとあまりに広すぎるので,もうちょっと意味をもたせる形で interface で包んでいる.この副作用として,ちゃんと型が付くので wire などの型をみるタイプの DI ツールが使いやすくなる.

また,デフォルト値を返す Stdio() という関数を用意しておいて,main() からはそれを利用するようにしている.@mattn さんに grapi の Windows 対応してもらったときの経験を生かして,mattn/go-colorable を入れている.

func Stdio() IO {
    io := &IOContainer{
        InR:  os.Stdin,
        OutW: os.Stdout,
        ErrW: os.Stderr,
    }
    if runtime.GOOS == "windows" {
        io.OutW = colorable.NewColorableStdout()
        io.ErrW = colorable.NewColorableStderr()
    }
    return io
}

ref: pkg/cli/io.go at master · izumin5210/clig

あとは,テスト用に *bytes.Buffer が詰まった fake 実装とかを作ったりしている.

Makefile / CI

Go でボイラプレートといえば Makefile と CI ですね(?)

build

cmd/<CLI_NAME> 以下に main を置くようにしているので,それをいい感じにビルドするタスクを生成している.
たとえば cmd/foobar/main.go なら

  • make foobar
    • => go build -o ./bin/foobar ./cmd/foobar
  • make package-foobar
    • => gox ... -output="dist/foobar_{{.OS}}_{{.Arch}}" ./cmd/foobar

みたいな感じになる.こういうのも Makefile でよくわかんないことせずに Go でツールを作ってあげるといいのかもしれない….

SRC_FILES := $(shell go list -f '{{range .GoFiles}}{{printf "%s/%s\n" $$.Dir .}}{{end}}' ./...)
BIN_DIR := ./bin
OUT_DIR := ./dist
GENERATED_BINS :=
PACKAGES :=

XC_ARCH := 386 amd64
XC_OS := darwin linux windows

define cmd-tmpl

$(eval NAME := $(notdir $(1)))
$(eval OUT := $(addprefix $(BIN_DIR)/,$(NAME)))

$(OUT): $(SRC_FILES)
    go build $(GO_BUILD_FLAGS) $(LDFLAGS) -o $(OUT) $(1)

.PHONY: $(NAME)
$(NAME): $(OUT)

.PHONY: $(NAME)-package
$(NAME)-package: $(NAME)
    gox \
        $(LDFLAGS) \
        -os="$(XC_OS)" \
        -arch="$(XC_ARCH)" \
        -output="$(OUT_DIR)/$(NAME)_{{.OS}}_{{.Arch}}" \
        $(1)

$(eval GENERATED_BINS += $(OUT))
$(eval PACKAGES += $(NAME)-package)

endef

$(foreach src,$(wildcard ./cmd/*),$(eval $(call cmd-tmpl,$(src))))

.DEFAULT_GOAL := all

.PHONY: all
all: $(GENERATED_BINS)

.PHONY: packages
packages: $(PACKAGES)

ldflags

たとえば Skaffold はバージョン情報とかも ldflags から読み込んでいるが,自分はバグレポート受け取るときに便利な最小限のみ ldflags 経由で注入して,バージョン情報などはコード中にハードコードするようにしている.これは go が「go get だけでツールをビルド & インストールできる」文化で,ユーザがちゃんと brew や GitHub Release からアプリを落としてくれるとは限らないため.

REVISION ?= $(shell git describe --always)
BUILD_DATE ?= $(shell date +'%Y-%m-%dT%H:%M:%SZ')
LDFLAGS := -ldflags "-X main.revision=$(REVISION) -X main.buildDate=$(BUILD_DATE)"

Release

さっきしれっと出てきたけど,gox でクロスコンパイルしている.この生成物を CI から GitHub Release に投げ込む.

# .travis.yml
# 自分は Travis CI を使うことが多いけど,いまどきの CI as service なら何でもいいと思う

language: go

go: '1.11'

env:
  global:
  - FILE_TO_DEPLOY="dist/*"

  # GITHUB_TOKEN
  - secure: "..."

jobs:
  include:
  # snip.

  - stage: deploy
    install: make setup
    script: make packages -j4
    deploy:
    - provider: releases
      skip_cleanup: true
      api_key: $GITHUB_TOKEN
      file_glob: true
      file: $FILE_TO_DEPLOY
      on:
        tags: true
    if: type != 'pull_request'

lint / reviewdog

最低限のコードの品質保証とコードレビューの負担軽減用に,いくつかの linter を reviewdog を噛ませて使っている.たとえば grapi の .reviewdog.yml では golint, govet, errcheck, wraperr, megacheck, unparam を有効にしている.

あとは Makefile に lint 用のタスクを追加して,CI (pull-req)でチェックしている.

.PHONY: lint
lint:
ifdef CI
    gex reviewdog -reporter=github-pr-review
else
    gex reviewdog -diff="git diff master"
endif
# snip.

env:
  global:
  # snip.

  - REVIEWDOG_GITHUB_API_TOKEN=$GITHUB_TOKEN

jobs:
  include:
  - name: lint
    install: make setup
    script: make lint
    if: type = 'pull_request'

  # snip.

↓こんな感じになる.

pull-request review by reviewdog

reviewdog を使うことで CI を fail させずに lint の指摘を残せる.なので, golint のコメント関係や errcheck の絶対問題ない系のエラーハンドリングなどをスルーできるようになる.

また,lint ツールやコード生成ツールなどは gex という tool 管理ツールで管理している.開発者や CI 環境によって利用するツールのバージョンに差異が発生するのを防ぐためである.gex については以前「gex で Go プロジェクトの開発用ツールの依存を管理する - Qiita」という記事で紹介したので,そちらも参考にしてほしい.

clone 直後や CI で便利なように,setup task を Makefile に用意している.

.PHONY: setup
setup:
ifdef CI
    curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
endif
    dep ensure -v -vendor-only
    @go get github.com/izumin5210/gex/cmd/gex
    gex --build --verbose

boilerplate generator & utility package

と,ここまでで「いつも書いてる boilerplate」を紹介した.紹介してないものもいくつかあるので,実際にプロジェクト新規作成時にはもっとたくさん書いている(プロジェクトの性質によって取捨選択はするが).

流石に自分でもこれを毎回書いてるのはアホらしくなってたのでプロッジェクトジェネレータとライブラリを作った.

https://github.com/izumin5210/clig

こんな感じでプロジェクトが生成されたり

$ clig init your-app-name

$ cd your-app-name
$ tree -I 'bin|vendor'
.
├── Gopkg.lock
├── Gopkg.toml
├── Makefile
├── cmd
│   └── awesomecli
│       └── main.go
├── pkg
│   └── awesomecli
│       ├── cmd
│       │   └── cmd.go
│       ├── config.go
│       └── context.go
└── tools.go

よく書くコードがまとめられたパッケージがあったりする.
また,clig 自身も clig が吐くものとほぼ同じ構成をとっている.

新しく CLI ツールを作ることがあれば参考にしてもらえるといいかもしれない.


goパッケージを使って複数ファイルを1つにまとめる gma を作った

$
0
0

これは Go2 advent calendar 16日目の記事です。

モチベーション

最近競技プログラミングをやり始めたが、web上のエディタで書いたコードをそのままsubmitする形式が多いように思う。つまりシングルファイルにまとめる必要が出てくるが、いくつか問題を解いていると似たような処理が多くなりutilファイルが欲しくなってきたので作った。

作ったもの

go パッケージを使って複数のファイルを1つにまとめる gma というツールを作った。

何ができるか

単機能なのでREADMEに書いている以上のことはないが、現状以下のように複数のファイルがあったとき、良い感じにシングルファイルにまとめてくれる。

元ファイルたち:

$ tree example/
example/
├── main.go
├── util
│   └── util.go
└── util.go
example/main.go
package main

import (
        "fmt"

        "github.com/takashabe/gma/example/util"
)

func main() {
        fmt.Println(util.Foo())
        Foo()
}
example/util.go
package main

func Foo() {}
example/util/util.go
package util

import "fmt"

func Foo() string {
        return fmt.Sprintf("util")
}

結合する:

$ gma -main example/main.go -depends example/util.go -depends example/util/util.go
package main

import (
        "fmt"
)

func main() {
        fmt.Println(_util_Foo())
        Foo()
}
func Foo() {
}
func _util_Foo() string {
        return fmt.Sprintf("util")
}

"github.com/takashabe/gma/example/util" への依存が無くなり、単体で実行可能になっていることが分かると思う。

実装について

今回のように他パッケージのファイルを扱おうとするとシングルファイルにしたときimportの問題が出てくるので、ASTをゴニョゴニョするのが良さそうというのがわかる。
goでASTを扱うことについては日本語でも素晴らしい資料があるのでそれらを参照するのが良いと思う。特に参考にさせてもらったのは以下。
- https://qiita.com/tenntenn/items/a312d2c5381e36cf4cd3
- https://motemen.github.io/go-for-go-book/

ここでは gma を実装する上で特にハマった複数ファイルの結合と、外部パッケージの外部関数呼び出しをローカル呼び出しに変換する部分について、実際のコードから抜き出して紹介したい。

複数ファイルの結合

インポートや変数、型、関数定義といった宣言は全て ast.Decl インタフェースを実装している。そのためファイルごとのASTを得たら、それらから ast.Decl を抽出して新しい *ast.File とすれば良い。

以下ではimportを別に解決したかったのでそれだけ個別に除外しているが、雰囲気は伝わると思う。

func mergeFiles(files []*ast.File) (*ast.File, error) {
...
  decls := []ast.Decl{}
  for _, file := range files {
    for _, d := range file.Decls {
    g, ok := d.(*ast.GenDecl)
    if ok && g.Tok == token.IMPORT {
      continue
    }
    decls = append(decls, d)
    }
  }

  file := &ast.File{
    Package: files[0].Package,
    Name:    files[0].Name,
    Decls:   decls,
  }
...

上記コードでは触れていないが、複数パッケージを対象にしたときに同名の宣言があるとコンフリクトするので、実際には予めパッケージごとにユニークになるように名前を変換している。

外部関数呼び出しの変換

関数呼び出しをしているのは *ast.CallExpr で、更にその中で外部パッケージの関数呼び出しをしているのは callExpr.Fun*ast.SelectorExpr のものになる。

以下では条件に合致するnodeを探索している。ASTをいじるときは大体こんな感じで必要なnodeをwalkなりして探してゴニョゴニョというコードが多くなると思う。

func(c *astutil.Cursor) bool {
  n := c.Node()

  callExpr, ok := n.(*ast.CallExpr)
  if !ok {
    return true
  }
  selector, ok := callExpr.Fun.(*ast.SelectorExpr)
  if !ok {
    return true
  }
  x, ok := selector.X.(*ast.Ident)
  if !ok {
    return true
  }
...

外部関数呼び出しを行っているnodeが特定できたら、あとは実際にそれが変換する必要があるかどうかを判定して、変換の必要があれば astutil パッケージの Cursor.Replace でnodeの変換を行っている。(この用途なら普通にast.Walkしてnodeのフィールドを上書きしても良かったかもしれない)

実際には事前に結合したファイル側の関数を全て抜き出して、変換対象リストを作ってmainファイル側で呼び出される関数ごとに突き合わせを行っている。泥臭い感じのコードになっているがもし興味があればリポジトリを見てもらえればと思う。

  cn := &ast.CallExpr{
    Fun:  repNode.Name,
    Args: callExpr.Args,
  }
  c.Replace(cn)
  return true
}

astutil パッケージは golang.org/x/tools 配下にある準公式っぽいやつで、使い方はテストを見ると何となく分かると思う。
https://github.com/golang/tools/blob/master/go/ast/astutil/rewrite_test.go

まとめ

go パッケージを使って複数のファイルを1つにまとめる gma というツールを作った。またその過程で得たASTの扱いなどについて紹介した。

まともにASTを触ったのは初めてだったが、goではASTにアクセスするためのインタフェースが揃っているのでとても楽だった。
今回のようにAST経由で変換して何かしたいといった場合、要素ごとにそれを表すASTノードが何であるかを把握出来るとあとは整合性を保って変換していくだけなので、各ノードの関係性が分かるとそれなりに動くものが作れそうな気がする。

またgoでは公式ツール内で go パッケージを使っているものも多く、特に cmd/gofmtgolang/x/tools/cmd は非常に参考にさせてもらった。

高速コレクション操作ライブラリ「Koazee」

$
0
0

この記事はGo2 Advent Calendar 2018の17日目の記事です。

コレクション操作ライブラリ Koazee

12月はじめのこと、go-nutsに「速いコレクション操作ライブラリを作ったぜ」という投稿がありました。

彼のベンチマークによると類似のライブラリに比べてかなり速いという結果が示されています。ここでは彼が作ったコレクション操作ライブラリ Koazee を紹介しつつ、高速化の一端に触れてみます。

Lazy like a koala, smart like a chimpanzee (原文ママ)

Koazeeの特徴は

  • イミュータブル
  • ストリームスタイル
  • 遅延ローディング
  • ジェネリックス
  • 速い!

とのことでここでは「ジェネリックス」と「速い!」という点を見ていきます。

ジェネリックス

Koazee を使うとストリームの操作をより自然な形で記述することができます。

まず、Koazee の使い方を見る前に、類似ライブラリの go-linq のサンプルを見てみます。

import . "github.com/ahmetb/go-linq"

type Car struct {
    year int
    owner, model string
}

...


var owners []string

From(cars).Where(func(c interface{}) bool {
    return c.(Car).year >= 2015
}).Select(func(c interface{}) interface{} {
    return c.(Car).owner
}).ToSlice(&owners)

WhereSelectに渡される関数リテラルの中で、型アサーションしています。なんだか面倒くさい。go-linqにはWhereTSelectTといった、関数リテラル自体をinterface{}で受けるものも用意されていますが、「オーバーヘッドがあるよ」と注意書きがされています。

一方、Koazeeのサンプルを見てみると

package main

import (
    "fmt"
    "github.com/wesovilabs/koazee"
)

var numbers = []int{1, 5, 4, 3, 2, 7, 1, 8, 2, 3}

func main() {
    fmt.Printf("input: %v\n", numbers)
    stream := koazee.StreamOf(numbers)
    fmt.Print("stream.Reduce(sum): ")
    fmt.Println(stream.Reduce(func(acc, val int) int {
        return acc + val
    }).Int())
}

/**
go run main.go

input: [1 5 4 3 2 7 1 8 2 3]
stream.Reduce(sum): 36
*/

型アサーションをしない形で書かれています。実際にReduceは関数リテラルをinterface{}で受け取っています。

go-linqでは遅くなるとされていた方式で、なぜKoazeeは速いのか?

速い!

本人の解説によると、「キャッシュをうまく使っている」とのこと。例として、Filterの実装にあるバリデーションを見てみると、

func (op *Filter) validate() (*filterInfo, *errors.Error) {
   item := &filterInfo{}
   fnType := reflect.TypeOf(op.Func)
   if val := cache.get(op.ItemsType, fnType); val != nil {
      return val, nil
   //...... Validations for input
   item.fnInputType = fnIn.Type()
   cache.add(op.ItemsType, fnType, item)
   return item, nil
}

cacheにバリデーション結果を保存しているのがわかります。cacheの実態はただの二次元mapです。

また、他の点でもパフォーマンスに気を使っているとのことで、Reverseではちょっと工夫して

func reverseInt16Ptr(itemsValue reflect.Value) interface{}{
   input := itemsValue.Interface().([]*int16)
   len := len(input)
   output := make([]*int16, len)
   for index := 0; index < (len/2)+1; index++ {
      output[index], output[len-1-index] = input[len-1-index], input[index]
   }
   return output
}

ループ処理が半分で済むように実装されていたりします。

まとめ

年末に彗星のごとく現れたKoazee、使いやすく、それでいて高速!

キャッシュまわりはgoroutineと併用されたときにどうなるのーとか気になりますが、今後に注目です。

Goにまつわるとっても真面目なBenchmarkクイズ!「goの正規表現(regexp)は速いのか?」編

$
0
0

今回はクソ真面目にクイズを書いていきます。
なぜ真面目を強調しているかといえば、昔書いた記事が不真面目だったからです。
鬱陶しいくらい「いいね」するよう勧めてくるgoroutineクイズ

それではクイズに行きましょう。
最初の3問は小手調べ、Goの基本事項のおさらいです。
その後、第四問から正規表現の速さ比べに入ります。

実行環境

こんなの

Distro: Ubuntu 18.04.1
Kernel: 4.15.0-42-generic
CPU:    Intel(R) Core(TM) i5-7600 CPU @ 3.50GHz
Mem:    16GB
Go:     go version go1.11.3 linux/amd64

書いている間にgo1.11.4がリリースされましたが、今回は1.11.3です。

形式

f0とf1の実行時間を比較してください。

Benchmarkはだいたい

main_test.go
func BenchmarkF0(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = f0()
    }
}

func BenchmarkF1(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = f1()
    }
}

最初の3問はこんなノリです。f0とf1のどちらが速いか、あるいはだいたい同じくらいかを当ててください。

第一問:文字列のフォーマット、結合

割と基本的な文字列の結合です。

main.go
var (
    a = 1
    b = "Fizz"
    c = false
)

func f0() string {
    return fmt.Sprintf("%d%s%v", a, b, c)
}

func f1() string {
    return strconv.Itoa(a) + b + strconv.FormatBool(c)
}

解答

解答(折りたたみ)

f1の方が速い。
printf系の関数は遅くなることが多いです。

BenchmarkF0-4       10000000           155 ns/op          40 B/op          3 allocs/op
BenchmarkF1-4       30000000            44.8 ns/op        16 B/op          1 allocs/op

第二問:スライスの定義

初心者がよくハマるあれです。

main.go
func f0() []int {
    slice := []int{}
    for i := 0; i < 1024; i++ {
        slice = append(slice, i)
    }
    return slice
}

func f1() []int {
    slice := make([]int, 0, 1024)
    for i := 0; i < 1024; i++ {
        slice = append(slice, i)
    }
    return slice
}

解答

解答(折りたたみ)
$ go test --bench . --benchmem 
goos: linux
goarch: amd64
pkg: github.com/aimof/bench/slice0
BenchmarkF0-4         500000          2522 ns/op       16376 B/op         11 allocs/op
BenchmarkF1-4        1000000          1365 ns/op        8192 B/op          1 allocs/op

最初にcapを確保するかどうかで大分変わります。
メモリの再確保は時間がかかるのです。

第三問:再帰関数

よくある再帰関数です。

main.go
func f0(n int) int {
    if n > 0 {
        return n + f0(n-1)
    }
    return 0

}

func f1(n int) int {
    sum := 0
    for i := 0; i <= n; i++ {
        sum += i
    }
    return sum
}

解答

解答(折りたたみ)

f1が速い
まあ、再帰は遅いよね。

$ go test --bench . --benchmem
goos: linux
goarch: amd64
pkg: github.com/aimof/bench/rec
BenchmarkF0-4            300       5626446 ns/op           0 B/op          0 allocs/op
BenchmarkF1-4           3000        520985 ns/op           0 B/op          0 allocs/op
PASS
ok      github.com/aimof/bench/rec  4.323s

第四問:正規表現その1

本題です。
遅いと勘違いされがちなgoの正規表現についてです。
ランダムな数字で構成された文字列、バイト列を生成して"123"を"999"に変換して出力します。
stringsパッケージとregexpパッケージの比較です。
仕様上f0が文字列、f1がバイト列をそれぞれ引数、返り値にしています。

main.go
func f0(s string) string {
    return strings.Replace(s, "123", "999", -1)
}

func f1(b []byte) []byte {
    reg, err := regexp.Compile("123")
    if err != nil {
        log.Fatalln(err)
    }
    return reg.ReplaceAll(b, []byte("999"))
}
main_test.go
var length = 1024
var target = makeBytes(length)
var str = string(target)

func makeBytes(n int) (b []byte) {
    rand.Seed(time.Now().UnixNano())
    for i := 0; i < n; i++ {
        num := rand.Intn(10)
        b = append(b, []byte(strconv.Itoa(num))...)
    }
    return b
}

func Test(t *testing.T) {
    n0 := f0(str)
    n1 := f1(target)
    if n0 != string(n1) {
        t.Error()
    }
}

func BenchmarkF0(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = f0(str)
    }
}

func BenchmarkF1(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = f1(target)
    }
}

ここからはテストまで全部書きます。
クイズ始めましょう!

n = 1024(1Ki)のとき速いのはどっち?

解答

解答(折りたたみ)

f0(strings)の方が速い。やはり遅いですね、goの正規表現。

$ go test --bench . --benchmem 
goos: linux
goarch: amd64
pkg: github.com/aimof/bench/reg
BenchmarkF0-4        1000000          3583 ns/op        2048 B/op          2 allocs/op
BenchmarkF1-4          50000         23791 ns/op       41720 B/op         33 allocs/op
PASS

第五問:正規表現その2

第四問でlength=1024*1024(1Mi)の場合は?

解答

解答(折りたたみ)

f1(regexp)の方が速い

$ go test --bench . --benchmem 
goos: linux
goarch: amd64
pkg: github.com/aimof/bench/reg
BenchmarkF0-4           1000       4911563 ns/op     2097152 B/op          2 allocs/op
BenchmarkF1-4           1000       2735865 ns/op     5859064 B/op         58 allocs/op
PASS
ok      github.com/aimof/bench/reg  10.261s

第六問:正規表現その3

第四問、第五問でlength=1024*1024*1024(1Gi)の場合は?

解答

解答(折りたたみ)

だいたいf1の方が速いです。

$ go test --bench . --benchmem 
goos: linux
goarch: amd64
pkg: github.com/aimof/bench/reg
BenchmarkF0-4              1    1939527170 ns/op    2147483744 B/op        3 allocs/op
BenchmarkF1-4              1    1693673809 ns/op    6168930776 B/op       84 allocs/op
PASS
ok      github.com/aimof/bench/reg  48.742s

ループ数1なので10回くらい試しましたが、おおよそ、f0: 19.5秒程度、f1: 17秒程度になります。

第七問:正規表現同士の比較1

解答

main.go
func f0(b []byte) []byte {
    reg, err := regexp.Compile("123")
    if err != nil {
        log.Fatalln(err)
    }
    return reg.ReplaceAll(b, []byte("999"))
}

func f1(b []byte) []byte {
    reg, err := regexp.Compile("1.3")
    if err != nil {
        log.Fatalln(err)
    }
    return reg.ReplaceAll(b, []byte("999"))
}
main_test.go
var length = 1024
var target = makeBytes(length)

func makeBytes(n int) (b []byte) {
    rand.Seed(time.Now().UnixNano())
    return bytes.Repeat([]byte("12345678"), n/8)
}

func Test(t *testing.T) {
    n0 := f0(target)
    n1 := f1(target)
    if !reflect.DeepEqual(n0, n1) {
        t.Error()
    }
}

func BenchmarkF0(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = f0(target)
    }
}

func BenchmarkF1(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = f1(target)
    }
}

"123"と"1.3"の比較です。
文字列の生成方法を工夫したので"1.3"にマッチするのは"123"だけです。
どんな感じになるのでしょうか?
length=1024のときです。(この後増えます)

解答(折りたたみ)
$ go test --bench . --benchmem 
goos: linux
goarch: amd64
pkg: github.com/aimof/bench/reg1
BenchmarkF0-4          50000         65917 ns/op       40896 B/op         37 allocs/op
BenchmarkF1-4          30000         65174 ns/op       41072 B/op         40 allocs/op
PASS
ok      github.com/aimof/bench/reg1 6.078s

第八問:正規表現同士の比較2

length=1024*1024

解答

解答(折りたたみ)
$ go test --bench . --benchmem 
goos: linux
goarch: amd64
pkg: github.com/aimof/bench/reg1
BenchmarkF0-4             50      32877722 ns/op     5865417 B/op         66 allocs/op
BenchmarkF1-4             50      32408068 ns/op     5865597 B/op         69 allocs/op
PASS
ok      github.com/aimof/bench/reg1 3.464s

あんまり変わりませんね(つまらぬ)

第九問:正規表現同士の比較3

一応、length=1024*1024*1024(1Gi)のときもやってみましょう。

解答

解答(折りたたみ)
$ go test --bench . --benchmem 
goos: linux
goarch: amd64
pkg: github.com/aimof/bench/reg1
BenchmarkF0-4              1    32865004402 ns/op   6168960968 B/op       97 allocs/op
BenchmarkF1-4              1    31682328432 ns/op   6168961144 B/op      100 allocs/op
PASS
ok      github.com/aimof/bench/reg1 195.128s

やっぱりあまり変わりません。
もうちょっと複雑な正規表現を試してみましょう。

最終問題:正規表現同士の比較

"1......8"
"12*8"
この2つのパターンで、"12222228"この繰り返しにマッチさせます。

main.go
func f0(b []byte) []byte {
    reg, err := regexp.Compile("1......8")
    if err != nil {
        log.Fatalln(err)
    }
    return reg.ReplaceAll(b, []byte("19999998"))
}

func f1(b []byte) []byte {
    reg, err := regexp.Compile("12*8")
    if err != nil {
        log.Fatalln(err)
    }
    return reg.ReplaceAll(b, []byte("19999998"))
}
main_test.go
var length = 1024 * 1024 * 1024
var target = makeBytes(length)

func makeBytes(n int) (b []byte) {
    return bytes.Repeat([]byte("12222228"), n/8)
}

func Test(t *testing.T) {
    n0 := f0(target)
    n1 := f1(target)
    if !reflect.DeepEqual(n0, n1) {
        t.Error()
    }
}

func BenchmarkF0(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = f0(target)
    }
}

func BenchmarkF1(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = f1(target)
    }
}

1Ki, 1Mi, 1Giそれぞれの場合の速い方を答えてください!

解答

解答(折りたたみ)

1Ki: 同じくらい
1Mi: f0の方が速い
1Gi: f0の方が速い

桁数が指定されていないf1のほうが遅いみたいですね。(それでも結構速いですが)

1Ki.go
$ go test --bench . --benchmem 
goos: linux
goarch: amd64
pkg: github.com/aimof/bench/reg2
BenchmarkF0-4          50000         70360 ns/op       42592 B/op         48 allocs/op
BenchmarkF1-4          20000         68998 ns/op       41216 B/op         41 allocs/op
PASS
ok      github.com/aimof/bench/reg2 5.872s
1Mi.go
$ go test --bench . --benchmem 
goos: linux
goarch: amd64
pkg: github.com/aimof/bench/reg2
BenchmarkF0-4             30      54123874 ns/op     5867170 B/op         77 allocs/op
BenchmarkF1-4             20      71250299 ns/op     5865864 B/op         75 allocs/op
PASS
ok      github.com/aimof/bench/reg2 3.372s
1Gi.go
$ go test --bench . --benchmem 
goos: linux
goarch: amd64
pkg: github.com/aimof/bench/reg2
BenchmarkF0-4              1    55755956525 ns/op   6168962664 B/op      108 allocs/op
BenchmarkF1-4              1    72950733405 ns/op   6168961416 B/op      106 allocs/op
PASS
ok      github.com/aimof/bench/reg2 324.925s

まとめ

さて、ここまでやってきてお気づきの方もいらっしゃるかもしれませんが、Goの正規表現は実行時間がほぼ線形に増えます。
1Miと1Giの実行時間を比べてみると、ほぼ1024倍に近い差になっています。
1ki程度に短い場合には、大分長くなるようです。

というわけで、長い文字列を処理する場合には、regexpは優秀です。
MiB単位くらい長くないとあまり効果は発揮できないようですね。

以上、Goにまつわるとっても真面目なBenchmarkクイズ!「goの正規表現(regexp)は速いのか?」編でした。

私の読みたいものリストに「regexpのソースコード」が追加されました。

RxGoでリアクティブプログラミングに入門する

Goのssa.htmlの変更点まとめ2018

$
0
0

Go2 Advent Calendar 2018 20日目の記事です。
空きを見つけたので代筆させていただきます。

はじめに

Goコンパイラの中間表現として採用されているSSA(Static Single Assignment)の小ネタです。

SSA(静的単一代入)についてはWikipediaが詳しいのでご覧ください。

GoコンパイラにはSSA形式が最適化パスを通してどのように変化してくのかをHTMLファイル(ssa.html)にダンプする機能があります。この記事ではssa.htmlが2018年にどのように進化していったかをコミットログから追っていきます。

ssa.htmlの作り方

$ env GOSSAFUNC=main go build

コンパイル時に環境変数GOSSAFUNCに関数名をセットすることで、ssa.htmlを生成できます。

後述しますが、2017年はGOSSAFUNCには単に関数名を指定するだけでしたが、2018年の変更で複雑な指定もできるようになりました。

変更点

では、以降でどのような変更があったかを見ていきます。変更は

  • 現在の最新安定版(Go11.4)までに入っているもの
  • まだmaster(go1.12beta1)にしか入っていないもの

に分けて見ていきます。

変更点は、ssa.htmlを生成しているcmd/compile/internal/ssa/html.goのコミットログから調べました。

今回解析対象としたコードは以下のサンプルのssa関数です。Wikipediaの例を拝借しました。

package main

import "fmt"

func ssa() {
        var w, x, y, z int

        x = 5
        x = x - 3
        if x < 3 {
                y = x * 2
                w = y
        } else {
                y = x - 3
        }
        w = x - y
        z = x + y

        fmt.Printf("%d, %d\n", w, z)
}

func main() {
        ssa()
}

1.9.2〜1.11.4の変更点

昨年末の最新版Go1.9.2と現在の最新版Go1.11.4ssa.htmlがどのくらい違うのか比べて見ましょう。

大きく違うのはいくつかの最適化パスが折りたたまれていることでしょうか。

では、順に見ていきます。

Jul 28, 2017 (1.10beta1-) Goコードの変数名が出力されるように

[dev.debug] cmd/compile: better DWARF with optimizations on

SSA形式の変数名だけでは元のコードのどの変数だったのかが分かりづらかったのですが、この変更によって対応する変数が表示されるようになりました。
この変更自体は、最適化によって失われていた変数の位置情報をコンパイラオプション-dwarflocationlistsによってデバッグ情報として付与できるようにした、
というものです。

-dwarflocationlistsオプションについては以下に少し書いてあります
https://tip.golang.org/doc/diagnostics.html#debugging

Aug 18, 2017 (1.10beta1-) (リファクタリングなのでスキップ)

cmd/compile: rename SSA Register.Name to Register.String

Oct 12, 2017 (1.10beta1-) 変数に対応する行数が表示されるように

cmd/compile: add line numbers to values & blocks in ssa.html

元のコードの行数がSSAの変数の横に括弧書きされるようになりました。

Apr 5, 2018 (go1.11beta1-) IsStmtによって行数の表示が変わるように

cmd/compile: add IsStmt breakpoint info to src.lico

Includes changes to html output for GOSSAFUNC to indicate
not-default is-a-statement with bold and not-a-statement
with strikethrough.

https://github.com/golang/go/commit/619679a3971fad8ff4aa231d7942c95579ceff23#diff-8cd6c5dd336a16009909e578d8b6fcb3R394

この変更時点ではIsStmtマークは使われていませんが、現在は例えば以下の演算で行番号がストライクアウトされます。
https://github.com/golang/go/blob/release-branch.go1.11/src/cmd/compile/internal/ssa/numberlines.go#L62

Jun 14, 2018 (go1.11beta1-) 最適化の各フェーズの列を折り畳めるように

cmd/compile: use expandable columns in ssa.html

デフォルトでは以下の名前で始まるパスが最初から広がっており(html.go#L297)、それ以外は折りたたまれるようになりました。パス名の辺りをクリックすることで畳んだり広げたりできます。

  • "start"
  • "deadcode"
  • "opt"
  • "lower"
  • "late deadcode"
  • "regalloc"
  • "genssa"

Jun 16, 2018 (go1.11beta1-) 選択した時の色の種類が増えました

cmd/compile: add more color choices to ssa.html

ブロックや各行を選択するたびに別の色でハイライトされますが、この色が5色ほど増えました。

1.12(予定)以降の変更点

まだmasterブランチにしか入ってませんが、かなり変更が入っていますので紹介しておきます。全て@ysmolskyさんによるものです。

Go1.11.4と最新リリースのGo1.12beta1でssa.htmlがどのくらい違うのか比べて見ましょう。

ソースコードやAST、そしてCFG(Control Flow Graph)が追加されています。

では、1つずつ見ていきます。

Aug 23, 2018 Goの元のソースコードがssa.htmlに含まれるように

cmd/compile: display Go code for a function in ssa.html

今まではSSA形式に変換されてからの変遷でしたが、元のソースコードと対応づけて表示できるようになりました。

Aug 23, 2018 ssa.htmlの出力パスが標準出力に表示されるように

cmd/compile: clean the output of GOSSAFUNC

この変更は、GOSSAFUNCをつけたときに標準出力に表示される大量のデバッグプリントを抑制するためのものです。従来のようにデバッグプリントが必要な時はGOSSAFUNC=Foo+のように関数名の後ろに+をつければよいです。ssa.htmlの内容に変化はありませんが、html.goの中ではssa.htmlの出力パスを表示するよう変更になりました。

Aug 23, 2018 ソースコードにインライン展開後のソースが追加

cmd/compile: add sources for inlined functions to ssa.html

https://github.com/golang/go/issues/25904

インライン展開された関数がソースコードの列に追加されるようになりました。

Aug 25, 2018 ソースコードとSSAの間にASTの列が追加

cmd/compile: display AST IR in ssa.html

https://github.com/golang/go/issues/26662

ソースコードとSSAの間にAST(Abstract Syntax Tree)の列が入りました。ソースコード⇔AST⇔SSA間も対応づけてハイライトされます。

Sep 19, 2018 [bug fix]

cmd/compile/internal/ssa: fix a == a to a == b

Goのソースコードの列の関数の順序の計算が間違ってました。

Oct 17, 2018 タブ幅を狭く

cmd/compile: make tabs narrow in src column of ssa.html

Goのソースコードのインデントのタブ幅を狭くしました。

Oct 25, 2018 ヘッダの高さを控えめに

cmd/compile: reduce the size of header in ssa.html

関数名とヘルプで上部領域が埋まってたので調整したようです。
毎日見ていると、この高さが気になるのでしょう。

Nov 21, 2018 各フェーズのCFGを表示できるように

cmd/compile: add control flow graphs to ssa.html

大きめの機能が入りました。各フェーズにCFG(Control Flow Graph)を表示できるようになりました。コミットログにも書かれていますが、関数名の後ろに次の指定をすることでCFGが生成されます。

:*            - dump CFG for every phase
:lower        - just the lower phase
:lower-layout - lower through layout
:w,x-y        - phases w and x through y

サンプルはこちらでも確認できます。

https://github.com/golang/go/issues/28177

実行には、graphviz(dotコマンド)が必要です。SVG出力したものがssa.htmlに埋め込まれています。SSAの各ブロックを選択するとそれ対応してCFGのノードがハイライトされます。反対にノードを選択すると対応するブロックが選択されます。

細かい話ですが、先ほど紹介した「Aug 23, 2018 ssa.htmlの出力パスが標準出力に表示されるように」で末尾に「+」を追加するとデバッグプリントが表示されるのは、この形式になっても有効です。

# 全フェーズでCFGを生成、デバッグプリントも表示
$ env GOSSAFUNC=ssa:*+ go build main.go

Nov 22, 2018 ↑の手直し

cmd/compile: fix TestFormats by using valid formats

Nov 24, 2018 ブロックが畳めるように

cmd/compile: make ssa blocks collapsable in ssa.html

小さいですが各ブロックの右上に-ボタンが追加されました。押すとブロックを畳めます。

まとめ

2018年に導入されたssa.htmlへの変更点をまとめました。主に次の変更点がありました。

  • 元のソースコードとの対応づけ強化
  • CFG表示
  • 最適化パスやブロックの折りたたみ
  • 色数増加

go1.12がリリースされた際にはお試しください。

それでは良いお年を。

Viewing all 25 articles
Browse latest View live