Golang Cafe #43 まとめ

2014/08/17に開催された「Golang Cafe #43」についてのまとめです。


今回はディスカッションがメインの会となりました。
まとめは簡単になります。


参加者各々が、現時点でGo言語に対して思っていること、考えていること、などなど、を共有できた良い時間だったと思います。

Golang Cafe #42 まとめ Effective Go を読む。

2014/08/10に開催された「Golang Cafe #42」についてのまとめです。

今回はEffective Goを読み進めていきました。
読み進めたところまでを簡単にまとめておきたいと思います。
独自の解釈や省略している部分もあるのでご了承ください。

はじめに

  • Go言語は既存の言語からアイデアを借りて作成された新しい言語です。
  • C++Java言語からの変換は満足のいく結果を得られない。
  • プロパティやイディオムを理解すること、ネーミング、フォーマット、プログラムの構造などの規約を知っておくことが重要です。
  • Go言語のパッケージソースコードは、コアライブラリだけではなく、使い方の例も意図しています。
  • 多くのパッケージはhttp://golang.org/で実行可能な例も含んでいます。
  • 問題へのアプローチ方法や、実装に関する質問の答えはパッケージ内のドキュメント、コード、あるいは例の中にあります

フォーマット

  • フォーマットの問題はもっとも異論が多いです。
  • gofmtプログラムが標準的なインデント、垂直位置そろえ、必要であればコメントのフォーマットをしてくれます。
  • Go言語の標準パッケージはすべてgofmtでフォーマットされています。
  • インデントにはタブを使います。
  • 1行の長さに制限はありません。
  • CやJavaに比べ括弧が少ないです。制御構造(if、for、switch)の構文には括弧はありません。

コメントの付け方

  • Cスタイルのブロックコメント、C++スタイルのラインコメントが使用できます。
  • godocプログラム、あるいはサーバーはパッケージの内容のドキュメントを抽出するための処理を行います。
  • すべてのパッケージはパッケージドキュメントを持つべきであり、パッケージが複数のファイルに分かれている場合は、1つのファイルに表示されるべきです。
  • パッケージドキュメントはパッケージ全体の情報を提供すべきです。
  • パッケージドキュメントはgodocの最初のページに表示されます。
  • プログラム内のエクスポートされる名前はドキュメントを持つべきです。
  • 最初の文は宣言されている名前で始まる要約であるべきです。
  • 宣言された名前でコメントが始まっていれば、godocによる出力を有効にgrepすることができます。
  • Go言語の宣言構文は、宣言のグループ化を許可しているので、定数や変数のグループにコメントをつけることができます。
  • グルーピングはグループ内の項目間の関係を示すことができます。

Windows上のVMWareでCoreOS、Dockerを動かす

WindowsでDockerを動かすにはVagrant + VirtualBoxを使った方法が一般的なようですが、私は普段はVMWareを利用していて、その環境にVirtualBoxを同時にインストールして競合してしまいハマった経験があるので、VMWareとCoreOSを使ったシンプルな環境を構築した際のメモです。

最終的なゴールは、
Windows上のVMWareにCoreOSを導入し、Dockerfileを使用してPostgreSQLの動作するコンテナを作成、起動し、クライアント(pgAdminまたはアプリ)からアクセすることです。

準備

  • Windows7 64bit
  • VMWare または VMPlayer(どちらでも動作を確認できました)
  • msysGitなどsshクライアントを使用できるもの


VMWare、msysGitのインストール方法や使い方には触れませんので各自お調べください。

CoreOSのセットアップ

CoreOSはVMWare用のイメージがあるので、これをダウンロードしてきます。
https://coreos.com/にアクセスして、画面左側の「Lastest Release Info」をクリックします。


今回はStable版を使用するので、StableのBrowse Imageをクリックします。


ファイルの一覧が表示されるので、その中からcoreos_production_vmware_insecure.zipをダウンロードします。


適当なフォルダに展開し、coreos_production_vmware_insecure.vmxを起動すると数十秒で起動でログイン画面が表示されます。(デバイス構成の変更は適宜してください)


※以降はsshクライアントから操作します。


画面に表示されているIPアドレスを確認して、coreos_production_vmware_insecure.vmxが存在するディレクトリに鍵ファイルがあるので、これを使ってsshクライアントから接続します。

win7@ $ ssh -i insecure_ssh_key core@IPアドレス

CoreOSには既にDockerがインストールされているのでバージョンを確認してみます。

core@$ docker version
Client version: 1.0.1
Client API version: 1.12
Go version (client): go1.2
Git commit (client): 990021a
Server version: 1.0.1
Server API version: 1.12
Go version (server): go1.2
Git commit (server): 990021a

PostgreSQLコンテナの作成

今回はPostgreSQLが動作するコンテナを作成します。なぜPostgreSQLなのかというと、Golang Cafe #41PostgreSQLの環境が欲しかったというだけです。


Dockerizing a PostgreSQL serviceを参考にしながらコンテナを作成していきます。
まずCoreOS上でDockerfileの配置場所を作成します。

core@ $ mkdir postgresql
core@$ cd postgresql/

そのままCoreOS上でDockerfileを作成してもよいのですが、CoreOSに接続した状態では私のmsysGitのvimが正常に動作しないので、ホスト(Windows7)上で作成してコピーします。

win7@ $ scp -i insecure_ssh_key ./postgresql.Dockerfile core@IPアドレス:postgresql/Dockerfile

Dockerfileの中身はリンク先を参照してください。Dockerfileの中身についても今回は触れません。


再度CoreOSに接続してイメージをビルドします。

core@ $ mkdir postgresql
core@ $ cd postgresql/
core@ $ ls 
Dockerfile

core@ $ sudo docker build -t eg_postgresql .

しばらく時間がかかりますが、イメージが作成されます。

core@ $ dokcer images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
eg_postgresql       latest              45b811be258a        52 minutes ago      366.7 MB
ubuntu              trusty              ba5877dc9bec        2 weeks ago         192.7 MB
ubuntu              latest              ba5877dc9bec        2 weeks ago         192.7 MB
ubuntu              14.04               ba5877dc9bec        2 weeks ago         192.7 MB
ubuntu              12.04               b9e56c8f2cf5        2 weeks ago         103.8 MB
ubuntu              precise             b9e56c8f2cf5        2 weeks ago         103.8 MB
ubuntu              utopic              6ef6f1a66de1        2 weeks ago         194.1 MB
.....


作成したイメージからコンテナを起動します。デーモンとして動作させ、コンテナ上のポートを公開させるために-d、-Pオプションを使います。

core@ $ sudo docker run -d -P --name pg_test eg_postgresql

外部からコンテナ上のPostgreSQLにアクセスするためのポートを確認します。

core@ $ docker ps
CONTAINER ID   IMAGE                  COMMAND                CREATED          STATUS         PORTS                    NAMES
c33476aa6397   eg_postgresql:latest   /usr/lib/postgresql/   12 seconds ago   Up 9 seconds   0.0.0.0:49153->5432/tcp  pg_test

pgAdmin等で接続を確認します。

無事に接続も確認できたのでこれで目標は達成です。


起動中のコンテナを停止します。

core@ $ docker stop pg_test

CoreOSの終了時は普通のlinuxと同じく

core@ $ sudo shutdown -h now

で数秒で終了します。


Dockerについての詳しい内容は適宜調べていただくか、下記のような書籍を参考にされるといいと思います。

まとめ

以前Golang Cafe #26〜でDockerのソースコードの一部は読みましたが、実際に試してみることはありませんでした。更にはちょっと怖いようなイメージもありました。
しかし、以前ソースコードを読んでいたせいか、触り始めるとほとんど抵抗はなくなりました。これもGolang Cafeの成果ですね。でもまだ若干ブラックボックスを触っている感じがあります。

Golang Cafe #41 まとめ gorpを試す。

2014/08/03に開催された「Golang Cafe #41」についてのまとめです。

かつてGolang Cafe #4ではGo言語の標準パッケージのみ(各DB用のドライバは別として)でデータベースへのアクセスを試してみました。
今回はGo言語のORMの1つであるgorpを使ってみてどれほど便利なのかを試してみました。


今回はPostgreSQLSQLiteで確認してみましたが、以下の説明はSQLiteでの結果を中心にまとめておきます。
検証に使用したコードはmattnさんのコードをベースにしました。

準備

以下の2つのパッケージをインストールします。

$ go get github.com/coopernurse/gorp
$ go get github.com/mattn/go-sqlite3

※mattn/go-sqlite3をインストールするにはgccが必要になるため、Windows環境の方は事前にgccをインストールしておく必要があります。(gccのインストールについては「Windows7 64bit版でGo言語のクロスコンパイルを試す」を参考にしてください)

検証

テーブルに対応した構造体を定義しておきます。

type Person struct {
    Id       int32
    Name     sql.NullString
    Age      sql.NullInt64
    Sex      sql.NullBool
    Height   sql.NullFloat64
    Birthday time.Time
    BString  []byte
    BBigInt  []byte
}

項目の型ですが、bool、、float64、int64、string、[]byte、time.Time型、Null許容型としてNullBool,、NullFloat64,、NullInt64、NullString、nilにしか対応していません。(Go言語の標準がそうだったと思います)
またDB上ではnullとして扱えても、Go言語ではNull許容型(C#のNullableJavaのInteger)というのが存在しないので注意が必要です。
因みに、このPerson型をjson.Marshalしても、NullString(ほか)の構造体まで展開されてしまうので気をつけなくてはいけません。


次に実際にデータベースにアクセスする処理を書いていきます。

func main() {
    db, err := sql.Open("sqlite3", "./foo.db")
    if err != nil {
        panic(err.Error())
    }

    dbmap := gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}}
    t := dbmap.AddTableWithName(Person{}, "person").SetKeys(true, "Id")
    t.ColMap("Id").Rename("id")
    t.ColMap("Name").Rename("name")
    t.ColMap("Age").Rename("age")
    t.ColMap("Sex").Rename("sex")
    t.ColMap("Height").Rename("height")
    t.ColMap("Birthday").Rename("birthday")
    t.ColMap("BString").Rename("bstring")
    t.ColMap("BBigInt").Rename("bbigint")
    dbmap.DropTables()
    err = dbmap.CreateTables()
    if err != nil {
        panic(err.Error())
    }

    tx, _ := dbmap.Begin()
    for i := 0; i < 100; i++ {

        p := Person{}
        if rand.Float32() > 0.5 {
            p.Name.Scan(fmt.Sprintf("mattn%03d", i))
        }

        if rand.Float32() > 0.5 {
            p.Age.Scan(i)
        }

        if rand.Float32() > 0.5 {
            p.Sex.Scan(rand.Float32() > 0.5)
        }

        if rand.Float32() > 0.5 {
            p.Height.Scan(rand.Float32())
        }

        if rand.Float32() > 0.5 {
            p.Birthday = time.Now()
        }

        if rand.Float32() > 0.5 {
            p.BString = []byte(fmt.Sprintf("mattn%03d", i))
        }

        if rand.Float32() > 0.5 {
            p.BBigInt = big.NewInt(int64(i * 10000)).Bytes()
        }

        err = tx.Insert(&p)
        if err != nil {
            tx.Rollback()
            panic(err.Error())
        }
    }
    tx.Commit()

    list, _ := dbmap.Select(&Person{}, "select * from person")
    for _, l := range list {
        p := l.(*Person)

        var (
            bstring string
            bbigint big.Int
        )

        if p.BString != nil {
            bstring = string(p.BString)
        }

        if p.BBigInt != nil {
            bbigint = big.Int{}
            bbigint.SetBytes(p.BBigInt)
        }

        fmt.Printf("%d, %s, %d, %t, %f, %v, %d\n", p.Id, p.Name, p.Age, p.Sex, p.Height, bstring, bbigint)
    }
}

簡単に流れを説明すると、まずgorp.DbMap型を作成します。作成するときに各データベース用のDialectを指定します。
dbmap.AddTableWithNameメソッドでテーブル名と対応する構造体を指定します。またSetKeysメソッドで主キーを指定します。
その後でTableMap.ColMapメソッドとColumnMap.Renameメソッドで構造体の項目とテーブルの項目を結びつけてあげます。
DbMap.DropTablesメソッドとDbMap.CreateTablesはそのままテーブルの削除と作成です。このサンプルではDbMap.DropTablesの戻り値は無視しているのですが、厳密にテーブルが存在している場合のみテーブルを削除する場合は、DbMap.DropTablesIfExistsメソッドを使用します。


DbMap.BeginTransaction.RollbackTransaction.Commitトランザクションを使用することができます。ドキュメントを見る限りではセーブポイントも利用できそうです。


クエリーの発行については、Transactionのメソッドを実行するか、DbMapのメソッドを実行するかのいずれかになります。


実際の実行結果は以下のようになります。

1, {mattn000 %!s(bool=true)}, {0 %!d(bool=true)}, {false true}, {0.000000 %!f(bool=false)}, , {%!d(bool=false) []}
2, { %!s(bool=false)}, {0 %!d(bool=false)}, {true true}, {0.000000 %!f(bool=false)}, , {%!d(bool=false) []}
3, { %!s(bool=false)}, {0 %!d(bool=false)}, {false true}, {0.000000 %!f(bool=false)}, mattn002, {%!d(bool=false) [20000]}
4, { %!s(bool=false)}, {0 %!d(bool=false)}, {false true}, {0.696719 %!f(bool=true)}, , {%!d(bool=false) []}
5, {mattn004 %!s(bool=true)}, {4 %!d(bool=true)}, {false false}, {0.059121 %!f(bool=true)}, , {%!d(bool=false) []}
...

非常に見難いのはNullInt64のような型を使用しているためです。


GUIクライアントで確認してみるとこのようになっています。


また作成されたテーブルは以下のこのようになっています。


確かに簡単にテーブルを作成してくれるのは嬉しいですが、DBの設計に拘る人にとっては思うようなテーブルを作成することは難しそうに思います。(できるのかもしれませんがそこまでは確かめていません)
それに関連してDecimal型を扱うのにはどうしたら良いのかというのが気になります。Go言語ではC#のようなdecimal型はなくmath/bigパッケージのInt型を使うしかありません。
不運なことにbig.Int型Go言語の標準あるいはDBのドライバでも対応してなさそなので、苦肉の策として
BOLB型として保存してみました。ただBLOB型では保存しているだけなのであまり意味がなさそうな気がします。
一日も早いdecimal型への対応が望まれます!

PostgreSQL

ちなみにPostgreSQLを使用する場合は、以下のように2箇所を変更するだけで動作します。

db, err := sql.Open("postgres", "user=postgres password=postgres host=localhost dbname=godbtest sslmode=disable")

dbmap := gorp.DbMap{Db: db, Dialect: gorp.PostgresDialect{}}

またpgドライバにはpq.NullTimeという型も用意されています。

まとめ

以前、ORMを使用しないで直接DBに接続したこと思えば遥かに便利にはなっていますが、さまざまな要件にそってDB設計されたものに対して使用するのは、まだまだどうなのかなという感じはしています。
最近ではGo言語でシステム開発される話を耳にしたりしますが、その辺りのギャップをどのように吸収しているのか非常に気になっています。

Golang Cafe #40 まとめ Gorilla web toolkit を試す。 その3

2014/07/27に開催された「Golang Cafe #40」についてのまとめです。

開催からかなり経ってしまいましたが、記録としてまとめておきます。


前回、前々回に引き続き今回もGorilla is a web toolkitを試してみる会となりました。
その中から、今回は残りのgorilla/reverse、gorilla/rpc、gorilla/schema、gorilla/securecookieをかなり駆け足でみていきました。

gorilla/reverse

本家の説明はこちらのページ
インストール。

$ go get github.com/gorilla/reverse

gorilla/muxパッケージはurlを解析してハンドラを呼び出していますが、このパッケージは、正規表現からurlの文字列を生成します。(muxパッケージにもにたような機能がありますが)
簡単なサンプルを載せておきます。

func main() {
    func1()
    func2()
}

func func1() {
    regexp, err := reverse.CompileRegexp(`/foo/1(\d+)3`)

    url, err := regexp.Revert(url.Values{"": {"2"}})

    fmt.Println(url, err)
}

func func2() {
    regexp, err := reverse.CompileRegexp(`/foo/1(?P<two>\d+)3`)
    if err != nil {
        panic(err)
    }
    // url is "/foo/123".
    url, err := regexp.Revert(url.Values{"two": {"2"}})

    fmt.Println(url, err)
}

実行結果は

/foo/123 <nil>
/foo/123 <nil>

1つ目の例は定義した正規表現にパラメータをセットした文字列を返しています。
2つ目の例は名前付きの正規表現を使用しています。

gorilla/rpc

本家の説明はこちらのページ
インストール。

$ go get github.com/gorilla/rpc

Go言語では標準でnet/rpcパッケージがあるのですが、gorilla/rpcは1リクエスト/1呼び出しということらしいです。
この辺りは私がnet/rpcパッケージをまだあまりみていないのではっきりとしたことはよくわかりません。
サーバー側はgorilla/rpcを使用して作成しておきますが、クライアント側はGoの標準的なrpc呼び出しの方法でよさそうです。
サンプルコードは動作が確認できたら掲載しようと思います。

gorilla/schema

本家の説明はこちらのページ
インストール。

$ go get github.com/gorilla/schema

gorilla/schemaパッケージは、マップやポストされたデータから独自に定義した構造体への詰替えを行ってくれるようです。
入れ子のパラメータにも対応しているようなのですが、個人的には配列パラメータの命名方法が気に入りません。

gorilla/securecookie

本家の説明はこちらのページ
インストール。

$ go get github.com/gorilla/securecookie

このパッケージはクッキーなどに入れる値の暗号化、復号化の機能を提供しています。
暗号化に使用するキーはsecurecookie.GenerateRandomKey関数を使用すれば良いとのことです。

まとめ

3回に渡りGorilla web toolkitをみてきましたが、(3回目はかなり駆け足な内容になってしまいましたが)メインとなるのはやはりgorilla/muxパッケージでしょうか。その他にもWebアプリケーションを作成する上で必要となりそうな機能も用意されていているので、簡単なものを作るだけなら十分な気もします。

Golang Cafe #39 まとめ Gorilla web toolkit を試す。 その2

2014/07/20に開催された「Golang Cafe #39」についてのまとめです。

開催からかなり経ってしまいましたが、記録としてまとめておきます。


前回に引き続き今回もGorilla is a web toolkitを試してみる会となりました。
その中から、今回はgorilla/contextとgorilla/sessionsをみてみました。

gorilla/context

本家の説明はこちらのページ
何ができるのかというと、リクエストスコープで値を保存しておいたり、取得したりできるものという感じでしょうか。
インストールは

$ go get github.com/gorilla/context

でOKですが、前回muxを試したときに一緒にインストールされたかと思います。


前回gorilla/muxを試してみましたが、
ハンドラ内で解析されたURLのパラメータを取得する際に、

vars := mux.Vars(request)

としたと思いますが、ソースコードを確認すると、内部で、

context.Get(request, キー)

のようなことをしています。
おそらくURLを解析した際にパラメータ(名前/値)をコンテキスト(グローバル変数)に入れておき、ハンドラ内から取得するのに使用しているのだと思います。
リクエストスコープで値を保存しておく場所というような感じで良いと思います。(有効時間は設定できそうです)

gorilla/sessions

本家の説明はこちらのページ
本家の説明を参照すると「クッキーやファイルシステムを使用したセッションや、カスタムセッションのインフラを提供してくれる」らしい。
いわゆるセッションです。
リクエストを跨いで値を保持することができます。その保存先がクッキーやらファイルシステムやらということだと思います。
インストールは他と同じです。

$ go get github.com/gorilla/sessions

本家と同じようなサンプルになりますが載せておきます。

const SESSION_NAME = "session-name"

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/set", MyHandler)
    r.HandleFunc("/get", MyHandler2)

    http.Handle("/", r)

    http.ListenAndServe(":8080", nil)
}

var store = sessions.NewCookieStore([]byte("something-very-secret"))

func MyHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, SESSION_NAME)
    session.Values["foo"] = "bar"
    session.Values[42] = 43

    session.Save(r, w)

    w.Write([]byte("MyHandler"))
}

func MyHandler2(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, SESSION_NAME)

    fmt.Println(session.Values["foo"])
    fmt.Println(session.Values[42])

    w.Write([]byte("MyHandler2"))
}

sessions.NewCookieStore でクッキーを使用するセッションストアを定義しておき、store.Getで取り出します。既存のセッションがない場合は作成されます。session.Saveで保存します。
セッションをクリアするには、context.Clear(request)を呼ぶようです。(すいません、未確認です)

基本的なセッションの機能は提供されているので、あとは各々実装すれば良いと思われます。

Golang Cafe #38 まとめ Gorilla web toolkit を試す。 その1

2014/07/13に開催された「Golang Cafe #38」についてのまとめです。

開催からかなり経ってしまいましたが、記録としてまとめておきます。


今回はGorilla is a web toolkitを試してみる会となりました。
Gorilla is a web toolkitとは何か、簡単言うと、Go言語でWeb系のプログラミングを行うのにルーティングやらクッキー(セッション)の処理やらを上手くやってくれるツールキットのようです。フレームワームでは無いようです。


有名所でいうと最近何かと話題のDockerで使われていたり(確認した当時では)、GoogleI/OのGo言語のcodelaboで使用されていたりなど、Go言語ではスタンダードになりつつあるパッケージのようです。


Gorilla is a web toolkitには

  • gorilla/context
  • gorilla/mux
  • gorilla/reverse
  • gorilla/rpc
  • gorilla/schema
  • gorilla/securecookie
  • gorilla/sessions

のパッケージがあるようです。


その中から、今回はgorilla/muxをみてみました。

gorilla/mux

本家の説明はこちらのページ
Go言語でHTTPサーバーを建てる場合には、

http.Handle("/foo", fooHandler)

http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})

http.ListenAndServe(":8080", nil)

このような感じでルーティングと対応するハンドラを定義します。
あくまでURLとハンドラを結びつけているだけで、GETだろうが、POSTだろうが、パラメータがついていようが、単純にハンドラが呼ばれます。必要であればハンドラ内で処理するというのが比較的一般的なやり方のようです。
これらのような判定処理などやより高度な処理をハンドラが呼ばれれまでにやってしまおうというのがgorilla/muxパッケージです。

まずはインストール。

$ go get github.com/gorilla/mux

ホストやメソッドやヘッダやいろいろな条件でハンドラの呼び出しを制限できるようなのですが、詳細はドキュメントを確認してもらうとして、少しだけサンプルを残しておきます。

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/products/{key}", ProductsHandler)
    r.HandleFunc("/article/{category}", ArticlesCategoryHandler)
    // 正規表現にマッチしない場合は404
    // 最後の/がないと直前のパラメータはなかったことになる
    r.HandleFunc("/article/{category}/{id:[0-9]+}/", ArticleHandler).Methods("GET")

    // 404のときのハンドラ
    r.NotFoundHandler = http.HandlerFunc(NotFoundHandler)

    http.Handle("/", r)

    http.ListenAndServe(":8080", nil)
}

func ProductsHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)

    for k, v := range vars {
        fmt.Println(k, v)
    }

    w.Write([]byte("ProductsHandler"))
}

func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)

    for k, v := range vars {
        fmt.Println(k, v)
    }

    w.Write([]byte("ArticlesCategoryHandler"))
}

func ArticleHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)

    for k, v := range vars {
        fmt.Println(k, v)
    }

    w.Write([]byte("ArticleHandleer"))
}

func NotFoundHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println(r)

    http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
}

気をつけておきたいのは、アクセスしたURLがルーティングに定義されていない場合は404になります。また定義するパスの最後に/を必ず付けておかないとダメなようです。
ルーティングが定義されていない場合は、デフォルトのハンドラが定義されているようなのですが、

r := mux.NewRouter()
r.NotFoundHandler = http.HandlerFunc(NotFoundHandler)

のようにすれば独自に404用のハンドラを定義できるようです。