2016/12/02


追記: 修正が中途半端だったのでいったんリバートされ go1.9 で修正される事になりました。

UNIX に慣れている人であれば問題ないのですが、Windows で zip や tar.gz, tar.bz2, tar.xz を開くのは意外と不便で専用アーカイバをダウンロードしてきてインストールする必要があり、割かし不便なのですが archiver というツールを使うと以下のフォーマットを簡単に作成、展開できるようになります。

  • .zip
  • .tar
  • .tar.gz
  • .tgz
  • .tar.bz2
  • .tbz2
  • .tar.xz
  • .txz
  • .rar (開くのみ)

インストールは golang が入っていれば簡単です。

$ go get github.com/mholt/archiver/cmd/archiver

使い方も簡単で、圧縮は以下の手順。

$ archiver make [archive name] [input files...]

※形式は拡張子で判断します。

解凍は以下の手順。

$ archiver open [archive name] [destination]

シングルバイナリで動作するので、別の Windows にポンとバイナリ一つ持っていけば同様に動作するのが良いですね。

なお go1.7 の場合、zip のタイムスタンプが正しく保存されません。

Goでファイルをzip圧縮したときにタイムスタンプがずれる問題の回避策 - Qiita

#初めに Goでファイルをzip圧縮する方法と、それに伴う問題と、その回避の記事です。 (更にいい方法がありましたら、ご指摘よろしくお願い致します) #実験環境 go version go1.4.2 ...

http://qiita.com/yuki2006/items/0211421792bd788f8153
Golangで特定のディレクトリをZIP化する処理を書いた | SHINOFARAの日常

Go playground にも書いたけど、動かない 対象となるファイルが多すぎたか package main import ( "archive/zip" "fmt" "io" "io/ioutil...

https://log.shinofara.xyz/2016/06/30/golang%E3%81%A7%E7%89%B9%E5%AE%9A%E3%81%AE%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E3%82%92zip%E5%8C%96%E3%81%99%E3%82%8B%E5%87%A6%E7%90%86%E3%82%92%E6%9B%B8%E3%81%84%E3%81%9F/
Golangのarchive/zipを使うと、タイムゾーンが強制UTCになる問題! | SHINOFARAの日常

archive/zip/struct.go#L170 このファイルの 170 func timeToMsDosTime(t time.Time) (fDate uint16, fTime uint16...

https://log.shinofara.xyz/2016/06/30/golang%E3%81%AEarchivezip%E3%82%92%E4%BD%BF%E3%81%86%E3%81%A8%E3%80%81%E3%82%BF%E3%82%A4%E3%83%A0%E3%82%BE%E3%83%BC%E3%83%B3%E3%81%8C%E5%BC%B7%E5%88%B6utc%E3%81%AB%E3%81%AA%E3%82%8B%E5%95%8F%E9%A1%8C/
ZIP化の時に、タイムスタンプが強制UTCになってしまう。 Issue #46 shinofara/stand GitHub

https://golang.org/src/archive/zip/struct.go#L171 の仕様上 強制的にUTC時刻としてみなされてしまっている。 その為、ロケール毎に時間を増減させないと...

https://github.com/shinofara/stand/issues/46

go1.8 でビルドすると正しく扱える様になります。go1.8 では上記の様なワークアラウンドが必要無くなります。逆に言うと、これらのワークアラウンドがあると問題が発生してしまいます。

Go 1.8 Release Notes - The Go Programming Language

DRAFT RELEASE NOTES - Introduction to Go 1.8 Go 1.8 is not yet released. These are work-in-progress ...

https://beta.golang.org/doc/go1.8#archive_zip
Gerrit Code Review

...

https://go-review.googlesource.com/#/c/18274/

こういった問題ではバージョン毎に処理を分ける必要があります。以下では go1.7 と go1.8 で動作を分けたい場合のコードを説明します。まずはワークアラウンドを行っている処理を抽出します。

func FileInfoHeader(fi os.FileInfo) (*zip.FileHeader, error) {
    size := fi.Size()
    fh := &zip.FileHeader{
        Name:               fi.Name(),
        UncompressedSize64: uint64(size),
    }

    // 現在のLocalを取得する。 
    local := time.Now().Local()

    //時刻のoffset(秒)を取得する。
    _, offset := local.Zone()

    //ファイルスタンプの時間に時差分を追加する。
    fh.SetModTime(fi.ModTime().Add(time.Duration(offset) * time.Second))
    fh.SetMode(fi.Mode())
    var uintmax = uint32((1 << 32- 1)
    if fh.UncompressedSize64 > uint64(uintmax) {
        fh.UncompressedSize = uintmax
    } else {
        fh.UncompressedSize = uint32(fh.UncompressedSize64)
    }
    return fh, nil
}

ここを別のソースファイルに切り出します。その際 build constraints に go1.7 と !go1.8 を指定します。

// +build go1.7
// +build !go1.8

package main

func FileInfoHeader(fi os.FileInfo) (*zip.FileHeader, error) {
    size := fi.Size()
    fh := &zip.FileHeader{
        Name:               fi.Name(),
        UncompressedSize64: uint64(size),
    }

    // 現在のLocalを取得する。 
    local := time.Now().Local()

    //時刻のoffset(秒)を取得する。
    _, offset := local.Zone()

    //ファイルスタンプの時間に時差分を追加する。
    fh.SetModTime(fi.ModTime().Add(time.Duration(offset) * time.Second))
    fh.SetMode(fi.Mode())
    var uintmax = uint32((1 << 32- 1)
    if fh.UncompressedSize64 > uint64(uintmax) {
        fh.UncompressedSize = uintmax
    } else {
        fh.UncompressedSize = uint32(fh.UncompressedSize64)
    }
    return fh, nil
}

これでこのファイルは go1.8 未満の場合しかビルドされなくなります。あとは go1.8 以降のバージョンでビルドされる様に go1.8 の build constraints が付いた、ワークアラウンドを実行しないファイルを用意します。

// +build go1.8

package foo

import (
    "archive/zip"
)

func FileInfoHeader(fi os.FileInfo) (*zip.FileHeader, error) {
    // ワークアラウンドしないコード
    size := fi.Size()
    fh := &zip.FileHeader{
        Name:               fi.Name(),
        UncompressedSize64: uint64(size),
    }

    fh.SetModTime(fi.ModTime())
    fh.SetMode(fi.Mode())
    var uintmax = uint32((1 << 32- 1)
    if fh.UncompressedSize64 > uint64(uintmax) {
        fh.UncompressedSize = uintmax
    } else {
        fh.UncompressedSize = uint32(fh.UncompressedSize64)
    }
    return fh, nil
}
これで、go1.7 未満であればワークアラウンドが実行されるバイナリが、go1.8 以降であればワークアラウンドを実行しないバイナリが生成されます。

build constraints については、みんなのGo言語にて説明と用法含めて書かれています。

Posted at by



2016/11/15


数年前 Twitter の API はベーシック認証、かつ Public な API だったので Twitter Bot なんて楽ちんで作れたのですが、最近は認証は OAuth、Public な API も無くなってしまい Bot を作るのはとても難しくなりました。

やりたい事はそんなに大した事ではないのに、認証のおかげでコードも大きくなりがちで、気付けば本体コードよりも認証用のコードの方が多くなってしまう事もあります。Bot を作りたいのに敷居が高くて手を出せない、なんて思っている方も多いかもしれません。

でももし Twitter Bot がシェルで書けたら、簡単だと思いませんか?実は twty というツールを使うと簡単に Twitter 連携アプリケーションが作れてしまうのです。

GitHub - mattn/twty: command-line twitter client written in golang

README.md twty A command-line twitter client Install Install golang environment. see: http://golang....

https://github.com/mattn/twty

twty は端末で Twitter をする為のコンソールアプリなのですが、実はストリーミングを表示する -S オプション、出力を JSON にする -json オプションがあります。ですので出力を grep したり jq で加工したり出来るのです。

twty のインストールは以下の手順で行います。 go get github.com/mattn/twty

先に twty を1回起動しておいて下さい。もし既にログイン済みで、通常アカウントと異なるユーザで Bot を実行したいならログアウトし直して PIN コードを入力して下さい。

また通常 twty を使うユーザと Bot ユーザを分けたい場合は、twty に -account xxx フラグ(xxx は人気)を付けて起動して下さい。以降のソースコードでも変更が必要です。

まずは以下のソースコードを見て下さい。

#!/bin/sh

twty -json -S | while read -r LINE; do
  TEXT=`echo "$LINE" | jq -r .text`
  ID=`echo "$LINE" | jq -r .id_str`
  SCREENNAME=`echo "$LINE" | jq -r .user.screen_name`
  if echo "LINE" | grep -q '^RT @'; then
    continue
  fi
  if echo "$LINE" | grep -q 'ぬるぽ'; then
    twty -i $ID "@$SCREENNAME ガッ"
  fi
done

このコードは、twty から JSON ストリーミングを出力させ、1行毎に while ループが実行されます。ループ内では TEXT と ID、SCREENNAME を jq を使って取得し、grep で RT を除け、「ぬるぽ」という発言を検知したら twty を使って「ガッ」を返信します。その際 -i オプションでリプライID(in-reply-to)を指定しているのでちゃんと返信として発言します。

たったこれだけのソースですが、「ぬるぽ」の発言に「ガッ」を応答する Bot が出来ました。簡単ですね。

アイデアさえあれば色んな物が作れるはずです。

また発言に対するアクションが既に外部コマンドとして存在し、返信する必要もない場合には jsonargs が使えます。

GitHub - mattn/jsonargs: xargs for JSON stream

README.md jsonargs xargs for JSON stream Usage input data { "name": "foo1", "value": "bar1" } { "nam...

https://github.com/mattn/jsonargs

xargs の json ストリーミング版と思って下さい。例えば Twitter のタイムラインストリーミングを加工して外部コマンドを起動するのであれば以下の様に実行します。

twty -S -json | jsonargs my-command {{.user.screen_name}} {{.text}}

my-command というコマンドの第一引数に発言者の名前が、第二引数に発言内容が渡されます。

引数は golang の text/template のフォーマットで記述します。

template - The Go Programming Language

Package template implements data-driven templates for generating textual output.

https://golang.org/pkg/text/template/

応用例として、Twitter のタイムラインをデスクトップの画面に通知するコードを書いてみます。Mac であれば gntp-send の代わりに growl-notify を使うといいかもしれません。(Windows の場合は Growl for Windows が必要です)

twty -S -json | jsonargs gntp-send {{.user.screen_name}} {{.text}} {{.user.profile_image_url}}
twtyをgrowl通知

今あるツールを組み合わせて簡単に Twitter と連携できる様になりました。認証が難しいから Twitter 連携ツールが作れない、そんな風に思っておられたならぜひ twty を使ってみて下さい。

Posted at by



2016/11/06


golang 1.8 では database/sql に幾らかの新機能が追加されます。
  • キャンセル可能なクエリ
  • データベースの型の可視化
  • 複数の結果セット
  • サーバへのping
  • トランザクション分離レベル
  • 名前付きパラメータ
database/sql changes - Google ドキュメント
https://docs.google.com/document/d/1F778e7ZSNiSmbju3jsEWzShcb8lIO4kDyfKDNm4PNd8/edit#

本記事では Golang 1.8 で追加される database/sql の変更内容と、go-sqlite3 での対応状況、利用する上での注意点等を書いていきます。

キャンセル可能なクエリ

実行が長いクエリがキャンセルできるようになります。各 API に Context のサフィックスが付いた物が提供されます。具体的には Query であれば QueryContext、Exec であれば ExecContext が追加されます。第一引数に context.Context が渡され、外部からクエリのキャンセルが行えます。context.WithCancel でキャンセル用の関数を得て、キャンセルしたいタイミングで呼び出します。単純にタイムアウト目的であれば context.WithTimeout が使えます。

ctx, cancel := context.WithCancel(context.Background())
go func() {
    // 1秒待ってからキャンセル
    time.Sleep(1 * time.Second)
    cancel()
}()

stmt, err := db.PrepareContext(ctx, "SELECT name FROM test where id = ?")
if err != nil {
    panic(err)
}
defer stmt.Close()

rows, err := stmt.QueryContext(ctx, id)
if err != nil {
    log.Fatal(err)
}

ただしこの動作はデータベースのドライバにより異なります。MySQL の場合はクエリ毎にキャンセルできる仕組みがデータベースに備わっていますが、SQLite3 はクエリ毎ではありません。おそらく MySQL 向けのドライバはクエリ毎にキャンセル出来る実装を作ってくると思いますが、SQLite3 ドライバではデータベース全体でのキャンセルになります。もし個別にキャンセルしたいのであれば接続ごと作り直し別のコネクションでクエリを発行して下さい。(この動作は現状、暫定的です)

データベースの型の可視化

これまでは結果のカラム名を得る事は出来ましたが、型を得る事が出来ませんでした。しかし go1.8 からはドライバ側から型を得られる様になります。以下の情報が得られる様になります。
  • データベース上の型名
  • reflect.Type で表す型
  • 長さ
  • 厳密な数値のスケール
  • Null 可否

これまでも interface{} のポインタを使って Scan を呼び出す事でダイナミックな値をスキャンする事が出来ましたが、reflect.Type を使う事で、データベース側から予め型を提供できる様になる為、よりシームレスな値の取得が出来る様になります。また Null 可否が得られる様になるので例えばデータベースオーサリングツールを作る際には便利だと思います。実際には、Scan を使う限りそれほど便利にはなりません。しかしこれは今後、golang の database/sql として Blob をサポートしたり Blob を io.Reader/io.Writer で読み書き出来る様にする為の布石だと僕は思っています。

go-sqlite3 は上記の API を全てサポート済みです。

複数の結果セット

幾らかのデータベースでは複数の結果セットをサポートしています。複数のクエリを一度に発行し、結果セットを一度に得ます。

for {
    for rows.Next() {
        if err := rows.Scan(...); err != nil {
            log.Fatal(err)
        }
    }
    if !rows.NextResultSet() {
        break
    }
}

go-sqlite3 もサポートする予定でしたが、sqlite3 の複数クエリ実行は golang の複数結果セットが期待する物と異なる為、現状は実装を見送りました。

サーバへのPing

データベースが接続可能かどうかは、実際に接続してみないと分からない仕様であった為、今ある Ping の実装も実際に Open を呼び出しているだけでした。golang 1.8 ではこの実装をドライバ側に委ねられる様になりました。もちろん context.Context が引き渡されるため、データベースがサポートしているのであれば Ping の中断も行えます。go-sqlite3 でもサポートしています。ただし SQLite3 の場合はコネクションが閉じたかどうかでしか判断していませんが。

トランザクション分離レベル

トランザクション分離レベルをユーザ側から指定できる様になります。具体的には以下の種別を BeginContext で与えた context.Context に IsolationContext でレベル指定できる様になります。
  • LevelDefault
  • LevelReadUncommitted
  • LevelReadCommitted
  • LevelWriteCommitted
  • LevelRepeatableRead
  • LevelSnapshot
  • LevelSerializable
  • LevelLinearizable

ただしこれらはデータベースドライバによりサポート状況が異なるはずです。例えば SQLite3 であればデータベース全体でのロックしか粒度が設定できません。これについては現在実装を検討中です。それまではデフォルト以外の IsolationLevel は未サポートとなります。

名前付きパラメータ

これが今回、ユーザにとっては一番大きな変更点だと思います。これまでは Exec や Query 時に引数で値を受け渡す API であった為、名前付き引数が実現できませんでした。例えば PostgreSQL の場合はプレースホルダが $1$2 という指定であった為、引数の順番をプレースホルダ側から指定できるのですがこの形式を取らないデータベースでは引数の順番だけでプレースホルダの位置が決められません。今回の変更では以下の様に名前付き引数を値として渡せる様になりました。

row := db.QueryRow(`
    select id, extra from foo where id = :id and extra = :extra
`, sql.Param(":id"1), sql.Param(":extra""foo"))

SQL 内で値を使いまわせるのでとても便利になります。go-sqlite3 で対応済みです。

各ドライバの対応状況

現状僕が知る限り go-sqlite3 以外のドライバ全てではまだ実装が進んでいません(というか始まってすらいないかも)。go-sqlite3 がこの仕様に乗っかり人柱になろうという目論見です。名前付きパラメータはおそらく問題なく使えるかと思いますが、IsolateLevel やクエリキャンセルについては今後動作を変えるかもしれません。

Posted at by