2011年9月17日土曜日

Goで○○する方法 part 1

GDD 2011のチャレンジ問題をGoで解こうとコードを書いたのはいいけれど、結構前から存在してたらしいGCのバグに気付かずなんでメモリ切れになっちゃうんだろうなーと激しく時間のムダをしてしまった。で、結局C++に書きなおした。Goはデバッガもいまいちまだ整備されてないし、やっぱここぞというときは信頼できる枯れた言語と実行環境でやるのが重要っすね。

さてそんな感じにGDD 2011をやってて思ったけど、Goはやっぱり致命的にサンプルコードが足りない気がした。単純にファイルを開いてテキストを一行ずつ読む、という基本的なことすらやり方がよくわからなかったりする。言語の名前が検索しづらい、というのもそれに拍車をかけてる。

ということで、そんな基本的なコードをいくつか紹介していくことにする。

コマンドライン引数をパースして値を取得する方法

プログラム書いてるときに良く書くコードとして、コマンドライン引数をパースしてそれを取り込む、というものがあげられる。更にはhオプション付きで実行されたらヘルプドキュメントを出力する、といったことも良くやる。Goにはその辺を簡単にしてくれるflagパッケージというものが存在している。

以下はGDD 2011のチャレンジ問題を解く(はずだった)プログラムの一部で、コマンドライン引数をパースしている。

func main() {
    var inputFile *string = flag.String("in", "", "Path to the input file")
    var outputFile *string = flag.String("out", "", "Path to the output file")
    flag.Parse()
    logger.Println("Input file path:", *inputFile)
    logger.Println("Output file path:", *outputFile)
    ...
}

このコードのおかげで、以下のように引数が指定できるようになる。

$ ./SlidePuzzle -in puzzle.txt -out output.txt
Input file path: puzzle.txt
Output file path: output.txt

もし変なオプションを指定したとしたら、ちゃんとヘルプまで表示してくれる。

$ ./SlidePuzzle -aardvark
flag provided but not defined: -aardvark
Usage of ./SlidePuzzle:
  -in="": Path to the input file
  -out="": Path to the output file
$

そんな便利なパッケージです。みなさん使いましょう。

テキストファイルを開いて一行づつ読む方法

前項の方法でファイルパスを得たら、今度はそのファイルを開いて読んでいきたいと思うのが普通の人。ということでそのやり方となるのが下の方法。

    inFile,err := os.Open(*inputFile)
    if err != nil {
        logger.Println(err)
        return
    }

    defer func() {
        inFile.Close()
    }()

    reader := bufio.NewReader(inFile)

    // Get the maxinum number of moves allowed
    numberOfHands, err := reader.ReadString('\n')
    if err != nil {
        logger.Println(err)
        return
    }

    var maxLeft, maxRight, maxUp, maxDown int
    fmt.Sscanf(numberOfHands, "%d %d %d %d\n", &maxLeft, &maxRight, &maxUp, &maxDown)
    logger.Printf("Left: %d Right: %d Up: %d Down: %d\n", maxLeft, maxRight, maxUp, maxDown)

入力となるのが下のテキスト。

72187 81749 72303 81778

手順としては以下のようにやっている。

  1. ファイルを開く
  2. ファイルがちゃんと閉じられるようdeferで指定する
  3. 開いたファイルからbufio.Readerを生成する
  4. bufio.Readerで一行読む
  5. 読みこんだ一行をfmt.Sscanfでパースする

上のやり方はわりと普通だけど、一点注意が必要なのはbufio.Readerを生成するところ。ここはGoのインタフェースの仕組みがわかってないと「?」となる。

インタフェースを活用したパッケージ

Goはインタフェースさえ実装されてれば何でもカモン、というスタンスの言語だけど、それはもちろんライブラリにも適用されている。たとえばbufio.NewReader()のインタフェースは下の通り。

func NewReader(rd io.Reader) *Reader

で、io.Readerのインタフェースは以下の通り。

type Reader interface {
    Read(p []byte) (n int, err os.Error)
}

従来のオブジェクト指向言語に慣れてると「io.Readerというインタフェースを実装したインスタンスをbufio.NewReader()に渡せばいい」と考えてしまって、それはある意味正しいんだけど、Goだと少し注意が必要。なぜなら、Goではどの型がどのインタフェースを実装しているかが明示されていないから。

実際、os.Openから返ってくるFileという型のドキュメントを見ると、そこにはio.Readerというインタフェースの名前は一切出てこない。書いてあるのはこのFileという型が実装している各種の関数だけ。でもよくよく見ると、実はFileがio.Readerのインタフェースを満たしていることがわかる。下はFile.Read()のインタフェース。

func (file *File) Read(b []byte) (n int, err Error)

これ、io.Readerが定義しているインタフェースそのまま。なので、Fileはbufio.NewReaderにそのまま渡して問題無いということ。このように、いまのドキュメントだとどの型がどのインタフェースを実装しているのかがわかりにくいため、パッケージ同士がどう連携するかがつかめなくてちょっとしたことをするにも意外と苦労することになる。

まとめ

  • flagパッケージでコマンドライン引数をパースすると楽。
  • 各種パッケージ間の連携はインタフェースによってのみ定義されており、ドキュメント上のどこかに明示されているわけではない。なので、パッケージ間で連携させるような処理を書く場合はどの型がどのインタフェースを実装しているか注意してドキュメントを読む必要がある。