二日間ほど集中してgo言語でコード書いたので、その間に感じたことをまとめてみます。普段はだいたいPerlやJavaScriptでWebアプリケーションを書いています。
まとめの要約
- go言語良い
- Webアプリケーション書くならPerlとかRubyが良い
- PerlとかRuby書ける人がミドルウェア書くならgo言語良い
気に入ったところ
コンパイルエラーが親切
たとえば気楽な気持ちで以下のようなコードを書くと
package main import ("fmt"; "net/http"; "log") func main() { resp := http.Get("http://hatenablog.com/") fmt.Println(math.Pi) }
以下のように丁寧に問題箇所を教えてくれます。
./hoge.go:6: imported and not used: "log" ./hoge.go:10: multiple-value http.Get() in single-value context ./hoge.go:11: undefined: math
10行目で、すでにおかしくても11行目のエラーも教えてくれます。この段階で表示されたエラーを全部なおすと、このファイルに関してはだいたいコンパイルが通るので、ちょっとなおしてはコンパイルして様子をみてまたなおす、ということをあまりする必要がないのが良いですね。
以下追記
http://t.co/V777wL4ivA
"コンパイルエラーが親切"
のあたり、なにが親切なのか全くわからなかったんだけど。
"10行目で、すでにおかしくても11行目のエラーも教えてくれます"
って当たり前だと思ってたけどそうじゃないの?
— Kenji Yoshida (@xuwei_k) 2013, 12月 22
たしかに!
普段PerlやRubyを使ってると、どうしてもその行が実行されるまでわからない、意味的エラーが起こり得ます。また、エラーが発生した後の部分にあるエラーはその時点ではわからないことも多いので、コンパイラの存在を便利に感じました。(Goに限らずコンパイラが便利)
コンパイラのある言語を常用してないので、他のコンパイラ言事比較してより親切なのか評価するのが不可能ですが、エラーの内容が具体的でわかりやすいと感じて使っていました。単に型があるので、情報量が多くてわかりやすかったと感じているのかもしれません。
エラー処理を忘れにくい
go言語では一般的に返り値の二番目にエラーが設定されます。コードの字面にエラー変数が現れるのでちゃんとエラー処理をしようという気分になりますし、そもそもerr変数を一度も利用していないとコンパイルエラーになります。
file,err := os.Open("/tmp/hoge")
とだけ書いていると、以下の様なエラーが出ます。
./hoge.go:8: err declared and not used
エラー処理はともすれば見落としがちですが、忘れず書くよう誘導される言語設計になっているのが良いですね。
_ 変数をつかうと無視できますが、いかにもエラーを無視しているというコードの字面になるので、レビューで怒られそうですし、なんか事情があって無視してても気づきやすそうです。
resp,_ := http.Get("http://hatenablog.com/") // あ、これエラー処理してない...
また、戻り値を受け取らないとエラーになります。
resp := http.Get("http://hatenablog.com/") // 2つ目の戻り値を無視
./hoge.go:9: multiple-value http.Get() in single-value context
エラー変数を使いまわすと、エラー処理が抜けることはあるので気をつける必要はあります。
file1,err := os.Open("/tmp/no_such_file") // ここで起きたエラーは処理されてない file2,err := os.Open("/tmp/hoge") if err != nil { fmt.Println("error") } file1.Close() file2.Close()
以下追記。
概ね同意。ただエラーについては、「戻り値を取らなくてもコンパイル落ちない」という点は注意が必要。(それを知る仕組みも今はない) / “Go言語の気に入ったところ/気に入らなかったところ - はこべブログ ♨” http://t.co/RXhSEMnyPL
— Jxck (@Jxck_) 2013, 12月 21
@hakobe 正確には「エラー1値しか返さない関数で〜」ですね。例えば os にはエラーのみ返す関数がいくつかあって、それらは戻り値を取らないコードを書いても、コンパイルは通ります。
— Jxck (@Jxck_) 2013, 12月 21
たしかに!
range便利
For文の言語仕様にrangeを使うとどういうことが起きるのか書いてあるので要チェックです。高機能です。
goroutineとchannel便利
2つの組み合わせで、並列処理や非同期処理のの記述が気楽です。今回おしゃれなgoroutineのパターンが書けたので、別の記事で紹介したい。
クロスコンパイルが便利
簡単にクロスコンパイルできるので便利です。今回はLinuxで使うことを想定してるコードを書いていたのですが、開発環境はMacだったので、MacでLinux用のバイナリをビルドしたりしてました。Goはクロスコンパイルが簡単 - unknownplace.org が大変参考になりました。
気に入らなかったところ
リスト処理を関数型言語のように書けない
リストのマップやフィルタ処理をする処理を考えます。trivialな例ですが、数値のリストから5以下の数値をとりだし、それを辞書に変換する処理をPerlで書くと以下のようになります。
my @maps = map { +{ value => $_} } grep { $_ < 5 } (0, 1, 2, 3, 4, 5)
同等のコードをgo言語で書くと以下の様な手続き志向なスタイルになります。
maps := make([]map[string]int) for _,v := range []int{0,1,2,3,4,5} { if ! v < 5 { continue } maps = append(maps, map[string]int{"value" : v}) }
僕はPerlのような関数型言語スタイルのほうが宣言的で読みやすくて好きなので、go言語でもそういえた書き方ができればうれしかった。
とはいえ、スライスに対して使える関数やメソッドのようなものを増やすとシンプルな言語仕様というgo言語の利点が失われるし、go言語にgenericsがないためブロックの代わりに渡す関数の型の解決が難しそうです。実現は簡単でないのかもしれません。
genericsがない
これはgo言語的なFAQで公式サイトにものってますね。(参考: http://golang.org/doc/faq#generics) 実際に必要性を感じたのは以下の様な場合です。
Show というインターフェースに準拠しているリストを渡すと連結しているという関数を実装するために、ためしに以下のように書いてみました。
package main import ( "fmt" ) type Show interface { Show() string } type ConcretShow string func (c ConcretShow) Show() string { return string(c) } func showAll( shows []Show ) string { result := "" for _,s := range shows { result += s.Show() } return result } func main() { str := showAll( []ConcretShow { ConcretShow("hello, "), ConcretShow("world") } ) fmt.Println(str) }
しかし、このコードは以下の様なエラーが発生するため動作しません。
./hoge.go:26: cannot use []ConcretShow literal (type []ConcretShow) as type []Show in function argument
goのスライスの型は共変ではないんですね。Javaの配列の型は共変らしいですが、配列の要素への代入時にランタイムエラーが発生する原因になっているらしくて、あんまりうまくいってないらしいです(参考:http://www.ne.jp/asahi/hishidama/home/tech/java/array.html#h2_covariant) *1。確かに、という感じ。
ここでもし、generics的なものがあれば、以下のように書けそうです (記法は適当)。
func showAll[T <: Show]( shows []T ) string { result := "" for _,s := range shows { result += s.Show() } return result }
実際に、上記の様な記法はないので empty interfaceを活用しつつtype assertionでShow型に変換します。
func showAll( shows []interface{} ) string { // interface {} のスライスを受け取る result := "" for _,s := range shows { result += s.(Show).Show() // Show型へ } return result } func main() { str := showAll( []interface{} { ConcretShow("hello, "), ConcretShow("world") } ) fmt.Println(str) }
type assertionによるランタイムエラーの可能性が残りましたが、うまく動作するようになりました。先ほどのgoのFAQでも、interface {} を活用すればなんとかなると書かれていますね。
genericsを書けたほうが自然で堅牢な気もしますが、言語仕様や実装の複雑さが高くなりそうで、難しいのかもしれません。
JSONのパースたいへん
なんかたいへんでした。事前にJSONに対応した構造体を準備するか、type assertionやtype checkをして少しづつ値を取り出す必要があります(参考: http://blog.golang.org/json-and-go)。静的片付けの言語ではどのみち大変そう。
まとめ
go言語をちょっと書いてみた結果の感想を、気に入ったところ、気に入らなかったところという視点でまとめました。
全体的には、これまでのプログラミング言語の問題点をうまく解決しつつも、シンプルな言語仕様になっていて、筋が良いなという印象でした。すぐに覚えられて、ほどほどに書きやすいですし、適当に書いていてもコンパイラがチェックしてくれるので、コンパイルがとおれば、エラー処理なども抑えられた品質のコードになっています。
ただ、動的型付けでダックタイピング最高!という文化圏でこれまでコードを書いてきたので、すこし不自由に感じる部分もありました。
Webアプリケーションのフロントに近い(がサーバサイドの)ソフトウェアは、比較的機能が単純で、ただし変更されることが多く、また一度に実行される時間が短い(一回のHTTPリクエストであれば数秒以内)という特徴があります。そういったソフトウェアを作る場合は、go言語よりも、より柔軟なRubyやPerlのような言語を利用して生産性を重視するのが良いように感じました。
一方、長時間動作し変更頻度が比較的低いミドルウェア層のソフトウェアを記述するのに、go言語はぴったりだと感じます。ミドルウェア層では、goroutineなどの並列処理機構も有用に働きそうです。Webアプリケーションエンジニアも習得が容易ですし、書けるようになっておくと便利かもしれませんね。
*1:ScalaのListは共変だけどimmutableだから問題ない