Goにはなぜ基本的なデータ構造が標準ライブラリにないのか?
だらだらとpackageのdocumentを読んでいる。楽しくて仕方が無いのだけれど、変なことに気付いた。Goには標準的なデータ構造のライブラリが無い。例えば基本的なSetすらない。色々なpkgでtreeは実装されているが、treeのライブラリは無い。
(BigNumberや複素数等はある。後、suffixarrayがあるくらいかな?)
これは面白い。Java以来、新規の言語の標準ライブラリにはそういった基本的なアルゴリズムやデータ構造が標準で用意されてきた。そういうものが無いのは何でなんだろう。
btreeはGoogle自身が作っていた。
https://github.com/google/btree
4、5年前の物だが最近でもパフォーマンス改善が行われている。しかし、標準ライブラリでなく、Googleのgithubである。
何だか面白くて検索してみたらこんなのが。
data structures - golang why don't we have a set datastructure - Stack Overflow
ダメな質問としてcloseされているが、トップの回答ではGo言語にはGenericsが無いからだとdisられていた :-)
他のコメントにGoのspiritでは自前で実装するのが正しいからとある。証拠は無いが何か納得 :-)
先日のPythonにおけるstdlibはコードが死ぬ場所議論もあったのでこういう言語設計もあるかなぁとは思う。しかし、やっぱりGoは初心者向けではない感じ。自分で実装するか、まともな実装を探して使うか。後、外部コードをバリバリ使うのが正しいとの嗜好なのは今では他の言語でも普通だけど、日本のSI屋さんはGoは大丈夫なんだろうか? GAEとかだけなら十分に標準処理があるのかな。まぁ、お客さん次第か。
GoのPlaygroundでは乱数の種を撒けない
GoのPlaygroundでは時間や乱数が一意に決定され変更できない。
go - Golang random number generator how to seed properly - Stack Overflow
Inside the Go Playground - The Go Blog
コードに対するhashでmemcachedに結果を保存しており、人気のあるコードがGCPの課金を増やさないようチェックしているそう。
Playgroundはやっぱり楽でつい自前でコンパイルするよりそっちを使ってしまっていたのでこれは少々残念な制約。
んで、上のStackoverflowでは面白い指摘が出ている。Goでは時間がナノ秒まで言語仕様上は取得可能。しかし、実際の処理系がそこまで精度があるのは稀なので良くある現在時間をseedにするのは止めろよとのコメントが存在。より正しくは"crypto/rand"をseedに使えと推奨している。この前提が正しいのかちょっと自分のPCでtime.Nowを実行してみた所、自分の環境(WSL)ではtime.Nowの連続実行の差分がだいたい平均で2μsという所で1ms程は酷くなかった。しかし、自分の書いたコードがどこで実行されるのかはわからんので考慮の必要はあるのだろうか?
そもそも、暗号レベルの高い安全性がいるような状況でmath.randを使うことが正しいのかどうかという疑問はあるかもしれないが、ちょっと楽しい議論だった。
go concurrency
とりあえずgoのchannel周りは後回ししようと思っていたのだけれど、結局、勉強を始めてしまった。しかし、他言語よりは遥に簡単なGoでも難しい局面は多量に出てくる。
とりあえず、goroutineはどうやってscheduleしているのか?
素晴しい日本語解説が出てきたけど、とりあえず素人としてはgoroutineはシステムコール呼び出しで切り替えられると覚えておけば十分だろうと思う。(と言って逃げる :-)
もちろん、実行順は不定と考えるべしだし、同じgoroutineが継続される場合もあるだろう。
それよりchannel作る時の表記がchanなのがC言語っぽくてアレ。 また<-演算子は左結合なのでカッコが必要な場合も。言語仕様に載っている複雑な例を見ると頭が痛くなるのでお勧め。
んで、go blogからだけどRob Pike自身がGoのconcurrencyを説明している次のスライドが超お勧め
https://talks.golang.org/2012/waza.slide#1
非常に簡単なポンチ絵から始めてconcurrencyとparallelの違いを学び、concurrencyでも十分に速いし、そこからparallelへと発展できるんだよとの説明を行う。
その次に標準ライブラリのheapを用いてload balancerを実装してみせる。
この流れが非常にわかりやすい。
まぁ、結局、この資料ではロックについて触れていないのでsync pkgを使い始める辺りから地獄も待っているのでしょうけど。多分、goroutine大量、メッセージ通信、非共有メモリを堅実に守っていれば大抵の問題は最速でなくても優しく解けるのでは。
先は長いのだろうけど、自分のペースで頑張る。
なお、この資料でheapの存在を知り、上のtreeが無い問題にぶち当たる。またGo言語の継承が無いプログラムは本当に面白い。heapの各メソッドを自前で実装してるのに、heap.Initは突然標準の物を使う。従来のclassによるオブジェクト指向しか知らないとこの感覚になれるのが多分、大変。後、go funcで無名関数をガンガン実行していく部分はJSや関数型に慣れが必要でこれもちょっとアレだけど最近なら当たり前なんだろうか。closureとして使っている部分が多いのも慣れていない人には意味がわからない気がしてならない。*1
Goって結構大変な言語な気がする。それでも非常に人気が高いのはやっぱりGoogle先生の言語だからだろうか。もちろん、現場のクラウド化でCPU使用率の低いコンパイラ言語への移行が多いとか、バイナリ1つで配布できるとかの利点も理解しているのだけれど。Goもv2の話が出てきていて特に新しい言語と言える訳でもないけど、これくらい新しい言語で知的興奮が得られる仕事ができると良いなぁと思う。
Goでendianの判定
Goは実行系でCPUのarchが変わるのでデータが保存される順、endianが変わる。またintもJavaと違いサイズが固定されておらず、int64かint32になる。
このような点は大抵は問題にならないが必要になる場合ももちろん存在。判定するにはgo envで出る環境変数、GOARCHの値を見るのも1つの手段だ。では、それが無い場合には?
Go言語でプログラムでendianを判定する方法が紹介されていた。
https://groups.google.com/d/msg/golang-nuts/zmh64YkqOV8/mc3wZRbykiIJ
やっぱりunsafe.Pointerを利用する模様 :-)
なお、playgroundでも動いた。意外だ。
https://play.golang.org/p/jYY2r_WzCPq
ところで、Goでbinaryをバリバリ読み書きする場合に、bytesやencoding/binaryを利用することになると思う。後者のソースがとても面白い。(なお、GoDocの優れている点として関数等の名前をクリックすると直でソースコードに飛んでくれるのはとても嬉しい。使いまくっている)
https://golang.org/src/encoding/binary/binary.go?s=5180:5243#L151
Readのソースを見て欲しい。関数はInterface{}を用いて自由度を上げながらtype assertionによる型switchでとんでもなく長くなっている。
これについてはドキュメントのほうに
This package favors simplicity over efficiency. Clients that require high-performance serialization, especially for large data structures, should look at more advanced solutions such as the encoding/gob package or protocol buffers.
との説明が。うん、確かにパフォーマンスは出なさそうに見える :-)
面白いのはLittleEndianとBigEndianの両定数はByteOrderインターフェイスを別々に専用の実装をしている点だ。この部分は専用のコードが実行されるので型switchしておらず、速そう。ただし、処理系のarchと目的のarchとの組み合わせを考慮せずに常に変換を実行している。なのでプログラマは実行処理系とデータの保存形式の組み合わせが同じだと断定できる場合には使わないほうが良い? (その場合、使う訳が無いとも言える :-)
たまたまソースまで見てしまってつい悪い癖が出たのだけれど、気になる人は他にもいるよね?
ところで、このプログラムにはコンパイラに対するバイト境界の確認ヒントコードが入っている。ポインタをずらすことでセキュリティ上問題のある攻撃ができたりするらしいが、ポインタ演算の無いGoでpointerをずらすというのはunsafeでやるんだろうか。
main終了でgoroutineも終了
理解できていなかったがmainが終わればgoroutineは死ぬ。言語説明で無限ループなgoroutineが大量に利用されているけれども安心。
するとerrWriterでloopが終わらなくてもchanで通知してmainが抜ければ速攻で終了か。(まだ言うか
Goのchannelに関するMemo
資料のリンク https://talks.golang.org/2012/concurrency.slide#53
複数の並行の実装を紹介するスライド Go: code that grows with grace
Goでの並行によるエラストテネスの篩
https://play.golang.org/p/9U22NfrXeq
Haskellみたいな美しいコードだ。無限整数列を生成して素数で割っていく。channelが渡されていくのでそれまでの素数の倍数は取り除かれた数列が渡されていく。現時点での素数で割り切れない最初の数もまた素数。10個見つけた時点でmainが終了するので他のgoroutineも終了。goroutineがどんどん増えていくコードを考える頭がスゴイ。