GoのType AssertionとType Switches

Goでは,型を interface{} として宣言してあげることで,とりあえずどんな値でも格納することが出来る.
SlackのEvent API等,メッセージの形式がEventごとに異なるといった場合に使うと便利.

Example

func eventHandler(w http.ResponseWriter, r *http.Request) {
    var event map[string]interface{}
    decoder := json.NewDecoder(r.Body)
    if err := decoder.Decode(&event); err != nil {
        log.Fatalln(err)
    }
}

この例だと,変数 event に受け取ったイベントが格納される.
eventmap[string]interface{} として宣言されているので, event["token"] のように値を取得しても返ってくるのは interface{} 型である.
そのため,値を使いたいときに適切な型へ変換してあげる必要がある.

このような場合に,Goの言語仕様として用意されているType AssertionやType Switchesというものを使ってあげるとうまく型変換が出来る.

Type assertions

Type assertionsを使うと, interface{} から指定した型に変換することが出来る.
Type assertionsでは,次のような順番で処理が行われる.

  1. 値がnilであるかどうかを検査
  2. 指定した型に変換して値を返却

書き方は次の2つある.

1. 基本形

var x interface{} = 7   // interface{}としてxを宣言
i := x.(int)            // Type assertionsを使ってint型に変換

Type assertionsが成功すると,指定した型に変換された値が返り値として返される(上の例だと int に変換された 7 が返ってくる).
もし次の例のように正しく変換できなかった場合は,panicが発生する.

var x interface{} = "hello" // interface{}としてxを宣言
i := x.(int)                // xの値 "hello" はint型に変換できない => panic発生

2. 別の書き方

別の書き方として,次のように返り値を2つ受け取るバージョンがある.

var x interface{} = 7   // interface{}としてxを宣言
i, ok := x.(int)        // okには正しく変換できたかどうかがboolで格納される.成功した場合はtrue

この書き方を使うと,失敗したときでもpanicは発生せず, okfalse が設定されるだけとなる.
この時,1つめの返り値は指定した型のゼロ値となる.

var x interface{} = "hello" // interface{}としてxを宣言
i, ok := x.(int)            // xの値 "hello" はint型に変換できない => okはfalse
fmt.Println(i)              // 0(intのゼロ値)が出力される

Type Switches

ドキュメントでは,Type SwitchesはType assertionの特別形であると説明されている.
Type assertionでは変換したい型名を指定していたが,Type Switchesでは代わりに type を指定する.

var x interface{} = "hello"
switch i := x.(type) {
    case int:
        fmt.Println("x is int")
    case string:
        fmt.Println("x is string")
}

これを使うと,複数の型候補がある場合にswitch文を使って分岐させることが出来る.

参考文献

Goでバイナリを作る

ネットワークパケットを作るために,構造体や変数からバイト型のスライスへの変換方法を調べた.

テストで書いてみたプログラムは次の通り.
とりあえずint型の変数をいくつか対象としてバイト変換を試してみた.

何らかの型(構造体やint等のプリミティブ型)からバイト型のスライスに変換するには, encoding/binary パッケージの Write メソッドを使用すると良い.

binary.Write() メソッドは次のような定義になっている.

func Write(w io.Writer, order ByteOrder, data interface{}) error

第3引数に指定したデータを,第2引数で指定したバイトオーダーで第1引数の w に書き込む.

このメソッドを使うために,まず io.Writer インタフェースを実装したものが必要になる.
バイト型のデータを書き込みたいので, bytes パッケージの Buffer を使用することにする.

buf := new(bytes.Buffer)

注意点として, new() を使う点が挙げられる.
bytes.BufferWrite() メソッドはポインタ型( *bytes.Buffer )がレシーバとなっているため, new() を使ってポインタを取得して上げる必要がある.

これで io.Writer を用意できたので, binary.Write() を使ってデータを書き込む.
適当にint型の変数を用意して binary.Write() による書き込みを行う.
なお,今回はネットワークパケットを作りたいので,第2引数のバイトオーダはビッグエンディアンを指定する.

var val32bit int32
val32bit = 123

err = binary.Write(buf, binary.BigEndian, val32bit)
if err != nil {
    log.Fatalln(err)
}
fmt.Printf("uint32: % x\n", buf.Bytes())

これで無事書き込みができた.

サンプルプログラムの実行結果

uint8: 7b
uint16: 00 7b
uint32: 00 00 00 7b

今回はint型のみを扱ったが,構造体を書き込みたいときも同じようにして扱うことができるようだ.

HTTPリダイレクトについて調査した

HTTPのリダイレクトには,大きく分けて次の二つ存在する.

  • 一時的なリダイレクト
  • 恒久的なリダイレクト

一般的に用いられるリダイレクトのステータスコードは 301302 である(個人的な考えですが).
しかし,他にもいくつかリダイレクトのステータスコードは存在する.

また,似たような意味を持つステータスコードもあるので,整理するために少し調査した.

今回対象とするリダイレクトのステータスコード

今回は,次の4つを対象とする.

  • 301 Moved Permanently
  • 302 Found
  • 307 Temporary Redirect
  • 308 Permanent Redirect

それぞれのステータスコードについて,RFCを参考に概要を以下に示す.
308以外はRFC 7231,308はRFC 7238で定義されている.

301 Moved Permanently

  • 恒久的なリダイレクトを表す
  • 歴史的な経緯から,301を受け取ったクライアントはリクエストメソッドをPOSTからGETに変更する可能性がある
    • これを望まない場合,代わりに307を用いることができる
    • しかし,307とは意味が異なる(恒久的↔一時的)
      • 308が新しく定義された

302 Found

  • 一時的なリダイレクトを表す
  • 歴史的な経緯から,301を受け取ったクライアントはリクエストメソッドをPOSTからGETに変更する可能性がある
    • これを望まない場合,代わりに307を用いることができる

307 Temporary Redirect

  • 一時的なリダイレクトを表す
  • これを受け取ったクライアントはリクエストメソッドを変更してはいけない

308 Permanent Redirect

  • 恒久的なリダイレクトを表す
  • これを受け取ったクライアントはリクエストメソッドを変更してはいけない

挙動の確認

それぞれのステータスコードに対する挙動を確認するため,簡単なサーバプログラムをGoで作成し,そのサーバに4種類のクライアントで接続した.
作成したサーバプログラムは mas9612/http-redirect-test に置いている.

用いたクライアント
* curl 7.43.0
* Safari 11.1.2
* Google Chrome 67.0.3396.99
* Firefox 59.0.2

上記それぞれのクライアントから,GETとPOST2つのメソッドで作成したサーバに接続した.
結果を次の表に示す.

Method GET

client 301 302 307 308
curl GET GET GET GET
Safari GET GET GET GET
Chrome GET GET GET GET
Firefox GET GET GET GET

Method POST

client 301 302 307 308
curl POST POST POST POST
Safari GET GET POST POST
Chrome GET GET POST POST
Firefox GET GET POST POST

実験結果より,curl以外のクライアントは,POSTリクエストの応答結果が301と302の時,リダイレクト先へのリクエストメソッドをGETに変更していることがわかった.
また,RFCの定義どおり,307と308はきちんとリクエストメソッドが維持されていることが確認できた.

References

Docker Engine API試用

前に少し気になっていたDocker Engine APiを使ってみたので,それについて.

特に複雑なことはせず,引数に与えたイメージを削除するというプログラムを作ってみた.
ただ単純に削除するだけではつまらないので,何世代分保存しておくか,というのをオプションで指定できるようにした.

コードは mas9612/docker-tools/image-remove に置いてある.
あまりきれいなコードではないのでご注意ください.

Client.ImageList() メソッドでローカルにあるイメージの一覧が取得できるが, filter でイメージ名を指定できなさそうだったので,愚直にfor文で1つ1つ確認している.
アルゴリズムは得意ではないので,良い方法があれば教えてください…

images, err := client.ImageList(ctx, types.ImageListOptions{})
if err != nil {
    log.Fatalf("[ERROR] client.ImageList(): %s\n", err)
}

for _, image := range images {
    for _, repotag := range image.RepoTags {
        repository := strings.Split(repotag, ":")
        if repository[0] == *imageName {
            imageInfos = append(imageInfos, imageInfo{
                ID:      image.ID,
                Created: image.Created,
                Name:    repotag,
            })
        }
    }
}

削除対象のイメージをリスト出来たら,それを作成日時でソートし,指定した世代分は残してそれ以外を Client.ImageRemove() メソッドで削除している.
デフォルトでは,イメージ名にマッチしたもの全てを削除するようになっているのでお気をつけください.

if *generation > len(imageInfos) {
    *generation = len(imageInfos)
}
removeOptions := types.ImageRemoveOptions{
    Force: *force,
}
for _, image := range imageInfos[*generation:] {
    _, err := client.ImageRemove(ctx, image.ID, removeOptions)
    if err != nil {
        log.Fatalf("[ERROR] client.ImageRemove(): %s\n", err)
    }
    fmt.Printf("Image %s was deleted.\n", image.Name)
}

やっている事自体は簡単なので,ドキュメントと見比べて頂ければわかると思います.

GoでMySQLを使う – database/sql package

GoからMySQLを使う方法について調べた.
O/Rマッパーを使う方法も気になったが,まずGo標準パッケージで用意されている機能を使い,SQLを地道に実行していく方法を試した.

ソースコードは以下.

実行

Dockerを使って簡単にローカルにMySQLを準備する.

$ docker run -p 3306:3306 -e MYSQL_ROOT_PASSWORD=mypass -e MYSQL_DATABASE=testdb -e MYSQL_USER=mysql -e MYSQL_PASSWORD=mypass -d --name mysql mysql

DBの準備後,作成したソースコードを実行する.

$ go run mysql_example.go
ID: 1, Name: Tom
ID: 2, Name: Bob
ID: 3, Name: Alice

INSERTしたデータが正しく取得できていそうである.
念のため,MySQLに入って確認してみる.

$ docker exec -it mysql mysql -u root -p
Enter password:

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| testdb             |
+--------------------+
5 rows in set (0.01 sec)

mysql> use testdb;
Database changed

mysql> show tables;
+------------------+
| Tables_in_testdb |
+------------------+
| test_tbl         |
+------------------+
1 row in set (0.00 sec)

mysql> select * from test_tbl;
+------+-------+
| id   | name  |
+------+-------+
|    1 | Tom   |
|    2 | Bob   |
|    3 | Alice |
+------+-------+
3 rows in set (0.00 sec)

上記の通り,正常にテーブルの作成とデータの追加が行えていることが確認できた.

解説

準備

Goの database/sql パッケージを使うと,色々なDBを扱うことができる.
しかし, database/sql パッケージとは別に,ここから使いたいDBのdriverを探してインストールしておく必要がある.
今回はMySQLを使いたいので,go-sql-driver/mysqlを利用した.
下記コマンドで go-sql-driver/mysql をインストールする.

$ go get -u github.com/go-sql-driver/mysql

データベースへの接続

データベースへ接続するには, sql.Open() メソッドを使用する.
第1引数に使用したいdriver名,第2引数に接続先を指定する.

db, err := sql.Open("mysql", "mysql:mypass@/testdb")

接続確認を行いたい場合は, sql.Open() の後に DB.Ping() メソッドを呼び出すことでできる.

if err = db.Ping(); err != nil {
    log.Fatalf("db.Ping(): %s\n", err)
}

SQLの実行

SQLの実行は, DB.Exec() 及び DB.Query() メソッドで行うことができる.
CREATE文やINSERT文など,DBからデータが返ってこないものに関しては DB.Exec() メソッドを用い,SELECT文などDBからデータを取得するのが目的であるものに関しては DB.Query() メソッドを用いる.

Exec()

_, err = db.Exec("create table test_tbl (id int, name varchar(32))")
if err != nil {
    log.Fatalf("db.Exec(): %s\n", err)
}

Query()

DBからの結果は sql.Rows に入っている.
Rows.Scan() メソッドで,1レコードの中から値(今回であればSELECT文での取得対象に * をしているため,全てのカラム = idname )を取得することができる.
Rows.Scan() メソッドの引数にはポインタを渡すことに注意する.

1レコード分の処理が終了し,次のレコードに移るためには Rows.Next() メソッドを呼び出す.

var rows *sql.Rows
rows, err = db.Query("select * from test_tbl")
if err != nil {
    log.Fatalf("db.Query(): %s\n", err)
}
defer rows.Close()

for rows.Next() {
    var (
        id   int
        name string
    )
    err = rows.Scan(&id, &name)
    if err != nil {
        log.Fatalf("rows.Scan(): %s\n", err)
    }

    fmt.Printf("ID: %d, Name: %s\n", id, name)
}
if err = rows.Err(); err != nil {
    log.Fatalf("rows.Err(): %s\n", err)
}