この記事はただの学習記録です。
意味もなくGoを使いたい!!でもWeb作るのもつまらないということで題材を探していました。
ちなみに私は普段JavaScriptを書く人種です。
だいぶ前の記事ですがたまたま上記のようなリンクに当たり、面白く感じました。
ちょうど去年私が一人でハマっていた2048というすごくシンプルで奥の深いゲームがありましてですね、何処かのタイミングでソースをみたいなぁって思ってたので今回移植するタイミングでソースを確認すると同時にGoを学習できると思い作ってみました。
Termbox-goについて
TermboxはもともとC言語で実装されている、CLI上に描画、イベントのハンドリングなどができるライブラリです。
https://github.com/nsf/termbox
Goの学習の中で学んだこと
インデント
インデントがTabあとgo fmt
は素晴らしくいろんな言語に欲しい。
Go入門はどうやって
こちらを一通りやれば大体コード書けました。あとは適宜ググりました。
https://tour.golang.org/welcome/1
ディレクトリ構成
当初GOPATH以下でpackageを作成するのが一般的。今回は$GOPATH/src/github.com/1984weed/2048-go
に作成しました。
ステップ実行
最初は意味が全く分からなかったんですが、結局$GOPATH/src
以下にソースを置いてpackage実行すればintellijでできました。こんな簡単なことに悩んでいたのが恥ずかしい。
CLIツールの配布方法
参考というかほとんど自分の知りたいことが書いてあったのでこれからCLIツール作る人は参考にしてください。
http://qiita.com/futoase/items/73b7ca9fb16ca588ad6f
テスト
assertがないです。
http://golang.jp/go_faq#assertions
適切なエラーハンドリングとは、致命的ではないエラーが起きたときにクラッシュさせずに、処理を継続させることです
ユニットテストでAsserionルーレットがアンチパターンとされているのはエラーが起きた時にメソッド内の後続のコードが実行されないからですが、Goのテスト思想だと処理が続くのでアンチパターンとならないので一定の良いところがありそうです。
http://xunitpatterns.com/Assertion%20Roulette.html
今回は抽象化も共通化もしないので、同じようなコードを大量に作りました。エラーメッセージも一つずつ書くので適切なものになりやすい??のかもしれません。
エラー処理
goのエラー処理は以下のように行われます
if err != nil {
// process the error
}
なぜtry-catch-finally
のような例外制御をしていないかというと、例外が制御構造と結びつくと複雑性が高まるとの判断からだそうです。Errorとは値であり値をプログラマブルに制御するというのがGoのスタイルです。
https://blog.golang.org/error-handling-and-go
https://golang.org/doc/faq#exceptions
リンク先にもありますが、if err != nil
は大量の繰り返しを生み出す可能性があります。リンク先の解決方法では同じエラーに対処していますが、違うエラーを大量に処理するようなメソッドだとどどうしても if err != nil
を繰り返し使うことは避けられないように感じます。
ただ一つ一つのerrをどう処理したのかが分かりやすいので、読む人に対してGoに対する知識の差が出にくく見通しの良いコードが残ると感じました。
ちなみにgoが例外を持っていない訳ではなくPanic
やRecover
などの例外処理機構を持っています。のでそれを利用したtry-catchのライブラリなどもあるようです。
https://github.com/manucorporat/try
しかし言語設計の思想として例外制御構造とErrorを切り離したかったとのことなので郷に従うのがいいのではと思いました。
Log設計
前述の通りGoではErrorは例外処理とは切り離し、値として扱うのでmain関数まで必ず返却しています。
そしてエラーがある場合にはmain関数でpanicを呼ぶ直前にlog.Fatalf
を呼び出すようにしています。
ライブラリ単体でpanic
やlog.Fatalf
を呼び出すのはお行儀が悪いらしいが、今回はアプリケーションを書いたのでmainで例外を発生させてアプリケーションを落とすことでOKだと思いました。
http://qiita.com/methane/items/cedbf546ae2db8a63c3d
継承ない
今回は特に継承使わなかったのであんまりはまってはないです。
ジェネリクスない
関数型erではないので配列の処理でしか使わないんですけど、最初はすごく配列の処理に悩みました。しかし結果そんなに必要ないなぁって結論に今の所至ってます(今回のアプリではあまり抽象度の高いプログラミングを必要としなかったってのはあります)
ちょっとだけ例を確認してみます。
例えばFizzBuzzをJavaScriptでは
Array(100).fill(0).forEach((a, i) => console.log(i === 0 ? i : i % 15 === 0 ? "FizzBuzz" : i % 3 === 0 ? "Fizz" : i % 5 === 0? "Buzz" : i ))
このコードがいいとか悪いとかではなくあくまでかけるという話です。表現力豊かなゆえ一行で結構複雑な表現がかけてしまえます。
一方Goでは
for i := 0; i < 100; i++ {
if i == 0 {
fmt.Println(i)
continue;
}
if i % 15 == 0 {
fmt.Println("FizzBuzz")
} else if i%3 == 0 {
fmt.Println("Fizz")
} else if i%5 == 0 {
fmt.Println("Buzz")
} else {
fmt.Println(i)
}
}
このようにするしかないです。いやもっといいコードあるよって話もあると思いますが、大枠はこのラインから外れることはないはずです。
エラー処理やテストのassertionについても同じですがGoは他の言語に比べてボイラープレートが増えたり、一行で表現できる幅が少し少くなってしまう印象を受けました。
しかし後で見るとシンプルで分かりやすいコードが残ります。コードは書くより見る時間の方が長いので、このコード思想はすごく好きです。
nil値の型推論
JavaScriptは静的な型情報を求められませんが、例えばTSではよくこのようなコードを書いてしまいます。いいか悪いかは別として
const hoge = (i: number): MyInterface => {
if(i === 0){
return null
} else {
return new MyInterface();
}
}
と空であることをnull値として表現するケースです。
Goではnilについて型推論ができる形にするか、nilのポインターを返すという形にしなければならないです。
例えば
func Hoge(x int) MyStruct, error {
if x == 0 {
return nil, errors.New("error")
} else {
q := MyStruct{}
return q, nil
}
ではコンパイラに怒られるので
func Hoge(x int) MyStruct, error {
var q MyStruct
if x == 0 {
return q, errors.New("error")
} else {
q := MyStruct{}
return q, nil
}
か
func Hoge(x int) *MyStruct, error {
if x == 0 {
return nil, errors.New("foobar")
} else {
q := MyStruct{}
return q, nil
}
としなければなりません。
調べてるともっとハマりどころがありそうでした。
http://qiita.com/umisama/items/e215d49138e949d7f805
データの保存
tomlを使用しています。なぜtomlかというとググってるGoではtomlが扱いやすそうだったからです。(ただそれだけ)
Goではオブジェクトのリストをシリアライズできないようだったので、プリミティブ型への変換をかけてFileに保存しています。
デモ
2048-goからREADMEを見つつインストールして遊んでみてください。
Macしか対応できてませんので悪しからず
まとめ
よく触れられる速度の部分などは今回はわからなかったですが、コードを書く / 読むということにおいて、均整感抜群のGoって素晴らしいんじゃないかと感じました。
次回はWebServerで使用してみたいと思ってます。
参考