すぎゃーんメモ

2014-10-27

#perlcasual に参加してLTしてきた

PerlCasual #06に、参加してきました。なにげに5年前の第1回ときから参加しているし縁のあるイベント。@氏に感謝!

8月のYAPC::Asiaでは何も話せなかったけど、自分の開発環境についてアウトプットすることが全然なかったなーと思って、せっかくLTの機会をいただいたので自分のEmacs環境について話してきた。

たいして需要は無かったかもしれないけど とりあえずreveal.jsを使ってプレゼンを作ってみたいと思っていたのが実現したのと、デモ用のGIFアニメを作るにあたって 以前つくっていたttyrecからGIFアニメを生成するWebアプリをちょっと改良できたので だいぶ自己満足できました。

ありがとうございました。

2014-10-22

projectileをcache有効にして高速化

Emacs 24.4がリリースされました。

それとは(多分)関係ないのだけど、愛用しているhelm-projectileが、ファイル数の多いあるprojectでとても重くなってしまって困ったので、cacheを使うようにしてみた。

(require 'projectile)
(custom-set-variables
 '(projectile-enable-caching t))
(projectile-global-mode t)

projectile.elにはprojectile-enable-cachingというカスタム変数があって、これをtrueにすれば~/.emacs.d/projectile.cacheというファイルキャッシュを作成しそれを使ってファイル一覧を得るようになるらしい。

ただそのキャッシュファイル更新projectile-modeに関連するhookで行っているようなので、globalでprojectile-modeをtrueにしておくべき、っぽい。

こうしておくと、新たにファイルを追加したり削除したりしたときも自動的にキャッシュ更新してくれるのが確認できた。

2014-07-19

ターミナル操作の記録(ttyrec)からGIFアニメを生成するツールを作った

ttyrec で録画したデータを使ってターミナル上で再生しつつ、そのスクリーンキャプチャを使ってアニメーションGIFを生成するツールをGoで作ってみた。

Mac, Linux Desktopで動作。再生速度はオプションで変更可能。

f:id:sugyan:20140719013354g:image

f:id:sugyan:20140719013353g:image


背景

percolを使ってターミナル操作を早く、便利に。 - すぎゃーんメモ のような記事を書いたりする際に、ターミナル操作を録画してGIFアニメにしたい需要があり。

そういった用途に使える汎用のデスクトップ録画ツールとしてはLICEcapやGifzoなどがある。

これらはWindows, OSXあたりが対象で、Linuxの場合はまた他のツールがあるらしい(よく知らない)。

汎用的なキャプチャツールでも良いけれど、いちいち録画対象のターミナルのウィンドウを指定するのも面倒だし、ターミナル専用のツールがあっても良いのではないか。

ttyrec というツールが、古くから存在する。これを使うとコマンドからターミナルの操作を録画・再生することができる。

これで各コマを再生しつつターミナルのスクリーンショットを撮り、それらを繋げてGIFアニメにする、というツールも既に幾つか存在する。

これらは大抵 ImageMagick に依存しており、importコマンドを利用してスクリーンショットを撮ったり、convertコマンドでGIFを生成したりしている。言語はC/C++, Pythonで、対象はOSXとLinux両方だったり片方だけだったり。


ところで Go には標準パッケージとしてGIF画像を扱うものが用意されている。

これを使えばImageMagickに依存することなく前述のようなツールが作れるのでは? しかもPure Goで書けるはずだからクロスコンパイルして各環境用のバイナリを配布することも可能なはず!

ということで作ってみた次第。

(まぁターミナルの録画を撮りたい需要がある人なら各言語の実行環境用意したりImageMagickをインストールしたりするのに障壁は無いでしょうけども)


実装

ttyrecordの読み取り

ttyrecは自分でインストールしてもらうとして(RPMパッケージが用意されていたりhomebrewでも入るし そこは問題ないはず)。

それで撮って得られたバイナリデータは、

  • 各コマの時刻と、出力内容の長さを格納したヘッダ
  • それに続く出力内容の中身

がただ順番で書き込まれているだけなので、ひたすら順番に読み取っていく。


再生

読み取った内容を元に、各時刻の差分を計算したりしつつ ただターミナル上に出力していくだけで、再生できる。


キャプチャ

各コマを再生しつつ、TMPDIR以下にスクリーンショットを放り込んでいく。が、ここは結構面倒なところで。


OSXでは、

  • AppleScriptを使って対象アプリケーション(Terminal.appだったりiTerm.appだったり)のアクティブウィンドウのIDを取得
  • そのIDを指定してscreencaptureコマンドを実行する

という手順でスクリーンショットを撮る。実行中のターミナルアプリケーションがTerminal.appなのかiTerm.appなのか、はたまた別のアプリケーションなのか?は、とりあえずは$TERM_PROGRAM環境変数で二択の判別だけしているけれど 他に良い方法ないだろうか…

あと、Retina display上で撮ると高い解像度のデカい画像が作られてしまうので、そこからさらにsipsコマンドを使ってdpi情報を取得して調整したり。


Linux というかX Window Systemでは、xwdというコマンドで指定したWINDOWIDの画面をダンプできる、ということなのでそれを使うことにした。

ただこれはxwdフォーマットという独自のビットマップ形式で保存されるので、それを読み取って画像データとして得るためのdecoderは自分で書いた。


OSX/Linux各環境で分けられるかというとそうでもなく、Mac OS X上でも XQuartz を使ってX Window Systemを立ち上げてその上でターミナル動かしたりするので、出来る限りどちらにも対応できるようにした、つもり。

f:id:sugyan:20140719090422g:image

これはXQuartz上のxtermで撮ったもの。


xwdでは余計な部分が撮られることなくて済むのだけど、OSXのscreencaptureではどうしても「ウィンドウ全体」が撮られるため、タイトルバーなどが含まれてしまう。この部分が何故かコマによって白飛びしてしまったりするし、出来れば除外したいのだけど…

少し調べた限りでは良い方法は無さそうだった。決め打ちで上20pixelくらい削ったりしようにも アプリケーションによって または単一タブのときと複数タブのときとでバーの高さが変わったりするだろうし 逆にフルスクリーンモードのときは勝手に消えるからその場合も考慮しないといけないし…


キャプチャからのGIF生成

1コマずつ再生しつつ スクリーンショット取得->画像ファイル読み取り とやっていると再生終了まで時間がかかってしまうので、とりあえず撮るだけ撮って保存だけしておき、一通り再生し終わったらそれらを読み込んでGIF生成にとりかかる、というようにしている。

ここで早速 pipeline and cancellation並行性パターンの勉強 - すぎゃーんメモ で勉強した内容が役立った。CPU使う処理なので並行化でそこまで劇的に早くなるというわけでもないけど、runtime.GOMAXPROCS(runtime.NumCPU())指定してコア数ぶん動かすことで可能な限りの処理をしてくれるようになった、と思う。


TODO

Windowsのことは完全に無視して作った(確認できる環境も持ってないし…)けど、コマンドプロンプトのスクリーンショットを簡単に撮る方法はあるのだろうか?

Linux Desktopも自分では普段使ってないからあまり詳しく知らない、他にもサポートすべきものはあるのかな。

バイナリ配布するならttyrec自体もGoで再実装して使えるようにしておいた方がよかったりするのだろうか?

スピード調整はできるようにしたけど、「この前半3コマは削りたい」「このtypoの部分を削りたい」みたいな場面は出てくると思うので、一度各コマの時刻と出力内容概要をファイル出力して それを編集して使うことで各コマのタイミング調整や削除をできるようにしたいな、と思っている。

2014-07-17

pipeline and cancellation並行性パターンの勉強

Go Concurrency Patterns: Pipelines and cancellation - The Go Blog を読んでいて、なかなか理解するまで苦しんだので復習がてら自分でもコードを書いて確かめてみた。


お題

複数の入力データそれぞれに対して重い処理を行い、結果として返ってくる値をまとめて取得する。

途中でエラーが発生したら直ちに処理を中止して終了する。


コード

いちばん簡単な例

エラーを考慮しない場合。

package main

import (
	"fmt"
	"log"
	"math/rand"
	"time"
)

func init() {
	log.SetFlags(log.Lmicroseconds)
	rand.Seed(time.Now().UnixNano())
}

func doSomething(id int) string {
	wait := rand.Intn(1000)
	time.Sleep(time.Millisecond * time.Duration(wait)) // something heavy
	return fmt.Sprintf("%02d-%03d", id, wait)
}

func getAllData() (results []string) {
	for i := 0; i < 100; i++ {
		value := doSomething(i)
		log.Println("got", value)
		results = append(results, value)
	}
	return results
}

func main() {
	data := getAllData()
	log.Println("Finished.", data)
}

こんなかんじ。100ループで毎回数百ミリ秒かかる処理(本当はCPUぶん回すような処理だったり)をして、その結果をひとつずつ繋げていって結果が格納されたsliceを返す。


$ go run example.go
22:57:33.931461 got 00-955
22:57:34.706984 got 01-774
22:57:35.204441 got 02-497

...

22:58:23.020169 got 97-116
22:58:23.528240 got 98-507
22:58:24.178353 got 99-649
22:58:24.178596 Finished. [00-955 01-774 02-497 ...

当然ながら順番に1個ずつ処理していくのでとても時間かかる。


エラー処理を加える

doSomethingの中で、もしくはその前に繰り返し処理の内部でエラーが起こりうる、とする。適当に100分の1くらいの確率で起こることにして それぞれの関数をerrorも返すよう変更

var errUnfortunate1 = errors.New("unfortunate error 1")
var errUnfortunate2 = errors.New("unfortunate error 2")

func doSomething(id int) (string, error) {
	wait := rand.Intn(1000)
	time.Sleep(time.Millisecond * time.Duration(wait)) // something heavy
	if rand.Intn(100) == 0 {
		return "", errUnfortunate1
	}
	return fmt.Sprintf("%02d-%03d", id, wait), nil
}

func getAllData() (results []string, err error) {
	for i := 0; i < 100; i++ {
		if rand.Intn(100) == 0 {
			return nil, errUnfortunate2
		}
		value, err := doSomething(i)
		if err != nil {
			return nil, err
		}
		log.Println("got", value)
		results = append(results, value)
	}
	return results, nil
}

func main() {
	data, err := getAllData()
	if err != nil {
		log.Println("Failed!", err)
		return
	}
	log.Println("Finished.", date)
}

関数の返り値からエラーチェックして 何かあればすぐにgetAllDataを抜けてmain内で出力して終了するようになっている。


並行化その1 channel化

処理を並行で行うための準備として、goroutineとchannelを使った形に変えていく。

まずは入力を送ってくれるchannelを作って返す関数を作り、そこからrangeで読み取るようにしてみる。エラー処理を無視すると

func getDataChannel() <-chan string {
	c := make(chan string)
	go func() {
		for i := 0; i < 100; i++ {
			value, _ := doSomething(i)
			log.Println("got", value)
			c <- value
		}
		close(c)
	}()
	return c
}

func getAllData() (results []string, err error) {
	c := getDataChannel()
	for value := range c {
		results = append(results, value)
	}
	return results, nil
}

こんなかんじ。


並行化その2 重い処理を並行に

引き続きエラー処理を無視したままだけど、doSomething部分をgoroutineに。

単純に即時関数で囲んで並行化するだけだと処理が終わる前にcが閉じてしまったりmainまで終了してしまったりするので、sync.WaitGroupを使って全部おわるまで待つ。

import (
...
	"sync"
)

func getDataChannel() <-chan string {
	c := make(chan string)
	go func() {
		var wg sync.WaitGroup
		for i := 0; i < 100; i++ {
			wg.Add(1)
			go func(id int) {
				value, _ := doSomething(id)
				log.Println("got", value)
				c <- value
				wg.Done()
			}(i)
		}
		wg.Wait()
		close(c)
	}()
	return c
}

早く終わったものから順にどんどんデータが送られて、全部おわるまで待ってからcがcloseされる。

エラーが何もなければこれで良いのだけど、、


並行化その3 エラー処理1

まずはdoSomethingで返ってくるerrUnfortunate1を捕捉。

これはgoroutine内で起こり得るので関数の返り値としては使いづらい。ので、返ってくるvalueとともにstructに含めてchannelに送るようにする

type result struct {
	value string
	err   error
}

func getDataChannel() <-chan result {
	c := make(chan result)
	go func() {
		var wg sync.WaitGroup
		for i := 0; i < 100; i++ {
			wg.Add(1)
			go func(id int) {
				value, err := doSomething(id)
				log.Println("got", value, err)
				c <- result{value: value, err: err}
				wg.Done()
			}(i)
		}
		wg.Wait()
		close(c)
	}()
	return c
}

func getAllData() (results []string, err error) {
	c := getDataChannel()
	for r := range c {
		if r.err != nil {
			return nil, r.err
		}
		results = append(results, r.value)
	}
	return results, nil
}

受け取る側のrangeループ内でresult.errをチェックして、エラーを検出したらそこで終了。

これだけではまだまだ問題あるのだけど とりあえずここではこれで捕捉できたことにする


並行化その4 エラー処理2

次に、繰り返し処理の内部で起こり得るerrUnfortunate2を捕捉する。

goroutineでは返り値をとれないので、ループする部分をfunc() error {}()な即時関数で囲むことで取得する。得たerrorを送る手段としてerror用のchannelを用意し、resultを送るchannelと一緒に返して使ってもらうようにする

func getDataChannel() (<-chan result, <-chan error) {
	c := make(chan result)
	errc := make(chan error)
	go func() {
		var wg sync.WaitGroup
		err := func() error {
			for i := 0; i < 100; i++ {
				if rand.Intn(100) == 0 {
					return errUnfortunate2
				}
				wg.Add(1)
				go func(id int) {
					value, err := doSomething(id)
					log.Println("got", value, err)
					c <- result{value: value, err: err}
					wg.Done()
				}(i)
			}
			return nil
		}()
		wg.Wait()
		close(c)
		errc <- err
	}()
	return c, errc
}

func getAllData() (results []string, err error) {
	c, errc := getDataChannel()
	for r := range c {
		results = append(results, r.value)
		if r.err != nil {
			return nil, r.err
		}
	}
	err = <-errc
	if err != nil {
		return
	}
	return results, nil
}

errorが起きようと起きまいと即時関数が終了した後にcはcloseされるのでrange cループが終了し、その後にerrcから即時関数の返り値として得たerrorを取得してチェックすることができる。

これまた問題があるけど一応捕捉はできた。


並行化その5 中断されたことを知らせる

ここまでだと、errUnfortunate1が起きたときにはcがcloseすることもなく走ってる処理が続くし、errUnfortunate2のときにも走ってるもの待ってからcloseすることになってしまったり、まだ正しく中断できているとは言えない。

並行化して走っている処理たちに中断されたことを知らせるために、もう一つchannelを用意してそれを使って判定するようにする。

func getDataChannel(done <-chan struct{}) (<-chan result, <-chan error) {
	c := make(chan result)
	errc := make(chan error)
	go func() {
		var wg sync.WaitGroup
		err := func(walkFunc func(int) error) (err error) {
			for i := 0; i < 100; i++ {
				time.Sleep(time.Millisecond * 50)
				if rand.Intn(100) == 0 {
					return errUnfortunate2
				}
				err = walkFunc(i)
				if err != nil {
					return
				}
			}
			return nil
		}(func(id int) error {
			wg.Add(1)
			go func() {
				value, err := doSomething(id)
				log.Println("got", value, err)
				select {
				case c <- result{value: value, err: err}:
					log.Println("sent.")
				case <-done:
					log.Println("not sent.")
				}
				wg.Done()
			}()
			select {
			case <-done:
				return errors.New("canceled")
			default:
				return nil
			}
		})
		wg.Wait()
		close(c)
		errc <- err
	}()
	return c, errc
}

func getAllData() (results []string, err error) {
	done := make(chan struct{})
	defer close(done)
	c, errc := getDataChannel(done)
	for r := range c {
		results = append(results, r.value)
		if r.err != nil {
			return nil, r.err
		}
	}
	err = <-errc
	if err != nil {
		return
	}
	return results, nil
}

getAllData側で用意したdonechannelは、deferによって関数を抜けるときにcloseする。これをgetDataChannelに渡しておいて、そちらではselectを使って処理を分岐させることができる。doneが閉じていればそちら側が実行されるのでdoSomethingから値が返ってきてもcには送信されないし、ループを実行するwalkFuncは"canceled"なエラーを受け取りループを中断するようになる。


並行化その6 完成形?

中断したときsync.WaitGroupで全部終わるまでWaitするのはブロックする必要ないのでgoroutineにする(deferでも良いかも?)。でもrangeでcが閉じるまで待っていては結局errcからすぐには受け取れないのでこちらもselectを使う。

あと、errcは送る前に受け取り側が終了してしまっていると書き込みがブロックされる可能性があるのでバッファリングしておく必要がある、のでmakeの第2引数で1以上を指定しておく。

func getDataChannel(done <-chan struct{}) (<-chan result, <-chan error) {
	c := make(chan result)
	errc := make(chan error, 1)
	go func() {
		var wg sync.WaitGroup
		err := func(walkFunc func(int) error) (err error) {
			for i := 0; i < 100; i++ {
				time.Sleep(time.Millisecond * 50)
				if rand.Intn(100) == 0 {
					return errUnfortunate2
				}
				err = walkFunc(i)
				if err != nil {
					return
				}
			}
			return nil
		}(func(id int) error {
			wg.Add(1)
			go func() {
				log.Println("start", id)
				value, err := doSomething(id)
				log.Println("got", value, err)
				select {
				case c <- result{value: value, err: err}:
				case <-done:
				}
				wg.Done()
			}()
			select {
			case <-done:
				return errors.New("canceled")
			default:
				return nil
			}
		})
		go func() {
			wg.Wait()
			close(c)
		}()
		errc <- err
	}()
	return c, errc
}

func getAllData() (results []string, err error) {
	done := make(chan struct{})
	defer close(done)
	c, errc := getDataChannel(done)

Loop:
	for {
		select {
		case r, ok := <-c:
			if !ok {
				break Loop
			}
			results = append(results, r.value)
			if r.err != nil {
				return nil, r.err
			}
		case err = <-errc:
			if err != nil {
				return
			}
		}
	}
	return results, nil
}

これで、並行かつ エラー時には即座に処理が中断されて余計なデータ送受信などもなく後始末もできるようになった。


goroutineの起動数を制限

とはいえ上記の方法だと入力受け取るたびにどんどんgoroutineを起動することになりメモリ使用量などマズいことになり得る。

ので、並行に走らせる数を制限させる別のパターンを用意する。

まず、処理の結果を送るchannelを返していたgetDataChannelを、"入力"を送るchannelを返すだけのものに変更する。

func getInputChannel(done <-chan struct{}) (<-chan int, <-chan error) {
	ids := make(chan int)
	errc := make(chan error, 1)
	go func() {
		defer close(ids)
		err := func(walkFunc func(int) error) (err error) {
			for i := 0; i < 100; i++ {
				time.Sleep(time.Millisecond * 50)
				if rand.Intn(100) == 0 {
					return errUnfortunate2
				}
				err = walkFunc(i)
				if err != nil {
					return
				}
			}
			return nil
		}(func(id int) error {
			select {
			case <-done:
				return errors.New("canceled")
			case ids <- id:
			}
			return nil
		})
		errc <- err
	}()
	return ids, errc
}

こんなかんじ、doneが閉じてない限りは入力データとなるidを送りつづける。

で、その入力channelを受け取って出力に結果を流すworker的なものを別に作る。

func worker(ids <-chan int, c chan<- result, done <-chan struct{}) {
	for id := range ids {
		value, err := doSomething(id)
		log.Println("got", value, err)
		select {
		case c <- result{value: value, err: err}:
		case <-done:
			return
		}
	}
}

単純に入力が流れてくる限りdoSomethingな処理をして、doneが閉じていない限りはcにresultを送りつづける。役割がハッキリしている。

んで、あとはこれをgoroutineで起動させて受け取るだけ。ただし終了するのを待ってからcをcloseしてやる必要はある。

func getAllData() (results []string, err error) {
	done := make(chan struct{})
	defer close(done)
	ids, errc := getInputChannel(done)

	var wg sync.WaitGroup
	c := make(chan result)
	wg.Add(1)
	go func() {
		worker(ids, c, done)
		wg.Done()
	}()
	go func() {
		wg.Wait()
		close(c)
	}()

Loop:
	for {
		select {
		case r, ok := <-c:
			if !ok {
				break Loop
			}
			results = append(results, r.value)
			if r.err != nil {
				return nil, r.err
			}
		case err = <-errc:
			if err != nil {
				return
			}
		}
	}
	return results, nil
}

この形で呼び出されるworkerは任意の数のgoroutineで並行起動してもそれぞれが「入力を受け取り出力を送る」という役目をこなすだけなので上手く動作してくれる。

	var wg sync.WaitGroup
	c := make(chan result)
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			worker(ids, c, done)
			wg.Done()
		}()
	}
	go func() {
		wg.Wait()
		close(c)
	}()

最終形

というわけで最終的に出来上がったのが以下。ちゃんと納得できるかたちで http://blog.golang.org/pipelines/bounded.go と同じような形にできたので大丈夫だと思う。

package main

import (
	"errors"
	"fmt"
	"log"
	"math/rand"
	"runtime"
	"sync"
	"time"
)

func init() {
	log.SetFlags(log.Lmicroseconds)
	rand.Seed(time.Now().UnixNano())
}

var errUnfortunate1 = errors.New("unfortunate error 1")
var errUnfortunate2 = errors.New("unfortunate error 2")

type result struct {
	value string
	err   error
}

func doSomething(id int) (string, error) {
	wait := rand.Intn(1000)
	time.Sleep(time.Millisecond * time.Duration(wait)) // something heavy
	if rand.Intn(100) == 0 {
		return "", errUnfortunate1
	}
	return fmt.Sprintf("%02d-%03d", id, wait), nil
}

func getInputChannel(done <-chan struct{}) (<-chan int, <-chan error) {
	ids := make(chan int)
	errc := make(chan error, 1)
	go func() {
		defer close(ids)
		err := func(walkFunc func(int) error) (err error) {
			for i := 0; i < 100; i++ {
				time.Sleep(time.Millisecond * 50)
				if rand.Intn(100) == 0 {
					return errUnfortunate2
				}
				err = walkFunc(i)
				if err != nil {
					return
				}
			}
			return nil
		}(func(id int) error {
			select {
			case <-done:
				return errors.New("canceled")
			case ids <- id:
			}
			return nil
		})
		errc <- err
	}()
	return ids, errc
}

func worker(ids <-chan int, c chan<- result, done <-chan struct{}) {
	for id := range ids {
		value, err := doSomething(id)
		log.Println("got", value, err)
		select {
		case c <- result{value: value, err: err}:
			log.Println("sent")
		case <-done:
			log.Println("not sent")
			return
		}
	}
}

func getAllData() (results []string, err error) {
	done := make(chan struct{})
	defer close(done)
	ids, errc := getInputChannel(done)

	var wg sync.WaitGroup
	c := make(chan result)
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			worker(ids, c, done)
			wg.Done()
		}()
	}
	go func() {
		wg.Wait()
		close(c)
	}()

Loop:
	for {
		select {
		case r, ok := <-c:
			if !ok {
				break Loop
			}
			results = append(results, r.value)
			if r.err != nil {
				return nil, r.err
			}
		case err = <-errc:
			if err != nil {
				return
			}
		}
	}
	return results, nil
}

func main() {
	defer func() {
		// 異常に大きな数のgoroutineが起動しっぱなしでないか確かめる
		time.Sleep(time.Millisecond * 2000)
		log.Println(runtime.NumGoroutine())
	}()
	data, err := getAllData()
	if err != nil {
		log.Println("Failed!", err)
		return
	}
	log.Println("Finished.", data)
}

まとめ

なかなか処理の流れが複雑なかんじがして「なんでこんな書き方するの」「ここがエラーになったらどうなるの」とか悩んだけど、書きながら読んでるうちにようやく「あー、だからこうするのか」「確かに、こうしようと思ったらこういう形になるよねー」って納得できた。

とはいえスラスラとこういうのが書ける気はまだしないけど…。

2014-07-03

はじめて、の次のGo

はじめてのGo - すぎゃーんメモ の続き?的な。

入門で終わらずにもうちょっとマトモに何か作れるようになりたい、ということで、以前にRubyで書いた「げんきだしてbot」をGoで書いてみた。

成果物のリポジトリはこちら。

「げんきだしてbot」とは

まぁUser streamからネガティヴな発言を拾ってきて「げんきだして!」とリプライを送るだけのTwitter botです。


ファイル分割

すべて同一のmainパッケージ内で書くけど、機能ごとにファイルを分けてみた。

  • twitter_client.go: Twitter APIを叩いて認証したりStreamに接続したりなど
  • config.go: 認証して得たaccess tokenを保存したり読み込んだり
  • genki-bot.go: 実際にStreamからTweetを読み取り返すべき発言を判定したりなどのロジック
  • main.go: main関数。コマンド引数を読んでgenki-botを呼ぶだけ

typeやfuncの定義が色んなところに分散していても、どうせgodef使って定義元にジャンプできるので問題ない。


設定値の保存/読取

前回は go-pit を使って設定値を読み書きしていたけど、簡単に自分で書いてみよう、と自前で用意してみた。

~/.config/以下に適当にディレクトリ掘ってini形式ファイルでaccess tokenを保存。JSONやYAMLなどの構造化データもいいけど、Goで扱う場合 それほど複雑なデータ構造を必要としないならiniファイルくらいがお手軽で良いなーという印象を持った。

HOMEディレクトリの取得の際に最初はos/userパッケージを使っていたけど、これがどうもクロスコンパイルするときの障壁になるようなので避けることにした。


Stream API

普通にOAuth認証でのリクエストまでは問題ないとして、Stream APIに接続したときのレスポンスを継続して受け続けるために。

などを参考に。

bufio.NewScanner(resp.Body)で作った*bufio.Scannerに対してScanをかけることで 新たなデータが流れてきたときにbyte列を取得できるようだ。

ただこのときclient側から接続を切ろうとすると接続時のConnectionを維持している必要がありそうで、そのへんは面倒そうなのでここでは省略した。


正規表現

Ruby版では「疲れた」に対しては「疲れてるの?げんきだして!」と返信するが「お疲れさま」などには反応しないように/疲(?!れ(?:様|さ(?:ま|ん)))/のような正規表現を使って判定している。

が、これをこのままGoで使おうとするとinvalid or unsupported Perl syntax: `(?!`とエラーを吐かれてしまう。(?!pattern)、"negative look-ahead"否定先読みというのかな、これはGoではサポートされていないらしい。注意。


テスト

いちおうどんな発言に対してどんな返信をすべきか、または返信すべきでないか、を確かめるためにテストも書いておいた。

あまり書きやすいかんじはしないけど、とりあえず書いておけばgo testコマンドで簡単にテストを実行できるので安心できる。


Goroutineで簡単非同期

リプライを送る際にちょっとsleepで間を空けたり、またそのPOSTの送信で処理がブロックされる。Streamの受け取りは止めずにリプライ処理をするために、ここで初めてGoroutineを使用。

		if mention != nil {
			go func() {
				time.Sleep(time.Second * time.Duration(rand.Int31n(5)+5))
				tweet, err := bot.client.Mention(mention)
				if err != nil {
					log.Println(err)
				} else {
					log.Printf("tweeted: %s", tweet.Text)
				}
			}()
		}

Mentionメソッド自体は普通に返り値、エラーを戻す関数として定義しておいて、それを呼ぶ側をfunc(){...}()で囲んでGoroutineとして呼んでやればいいかな、と。

ここでは並行処理同士での協調や調整は必要ないはずなのでChannelとか難しいことは考えない。


クロスコンパイル

Goは簡単に他の環境用のバイナリを生成できる(前述の通りos/userのようなcgoを使うパッケージを含んでいるとハマったりするけれど)、ということで試してみた。

Mac OS上で作ったバイナリがさくらVPSで動かしているCentOS上で動くことが確認できた。感動。

drone.iowerckerを使ってバイナリ配布とかも後でチャレンジしてみよう。

Windowsについては試せる環境もっていないので無視で…。


まとめ

まだまだ慣れないけれど 少しずつGoを書く楽しさは感じはじめてる。

やっぱりツールが充実していてすぐに文法エラーを検出できたり定義を調べたり出来るのは強いなーと思う。

もうちょい色んなパッケージやライブラリのソース読んだりして良い書き方とか身に付けたい。


次は何を作ってみようか