Goの最適化で性能爆上げ!GC対策、ゼロコピー、オブジェクトプールまで徹底解説
引用元:https://news.ycombinator.com/item?id=43539585
GCの時間を減らすためにallocationを減らすのが良いってよく言われるけど、Goのアプリのpprofを見ると、GC sweepじゃなくてGC mark phaseが時間かかってるんだよね。GC markはいつもlive root(goroutineのstackとかglobalsとか)から始まって、そこから参照を辿ってpointerを色付けする。だからGCの時間を減らすには、long living allocationを避けるのが一番良いんだ。短いallocation、GC mark phaseが届かないやつは、GCの時間にほとんど影響ないんだよ。Allocation自体はGCを早くtriggerする効果があるけど、現実のアプリではGCを避けるのはほぼ無理ゲー。依存関係がないプログラムとか、すごく丁寧に書かれたプログラム以外はね。だからGCが起きたら、GC markの時間を減らすのが一番効果的だと思うよ。
abstractionはGoでは予期せぬ形で足を引っ張ることがあるから注意が必要だよ。Interfaceを使うと、たとえオブジェクトが読み取り専用で同じscope内だけで使われるとしても、heap allocationが強制されるんだ。fmt.Printf()みたいな関数もそう。だから、for loopでiの値をprintすると、iのintegerがheapにallocationされちゃうんだ。ライブラリのAPIを全部interfaceにすると、呼び出し元は毎回heap allocationしないといけなくなるよ。
integerはinterfaceにinlineできると思ったんだけどなー。Goは昔はそうしてた気がする。でもplaygroundで試したら、heap allocationされちゃった:https://go.dev/play/p/zHfnQfJ9OGc
Goは昔はそうしてたんだよ。でも1.4で削除されたんだ:https://go.dev/doc/go1.4#runtime
基本的には、thin pointer(*T, chan, map)以外はboxingされるんだよね。結局、interfaceの値の2つのwordは常にpointerになるんだ。これはgarbage collectorにとっては都合が良い(escape analysisが失敗した時のallocationは別として)。標準ライブラリにはboxingを避けるためのtrickがいくつかあるみたいだよ。例えば、log/slogでstringとかtimeがどう扱われてるかとか[2]。[1]:https://github.com/teh-cmc/go-internals/blob/master/chapter2… [2]:https://cs.opensource.google/go/go/+/refs/tags/go1.24.1:src/…
slog.Valueはすごく便利そうだね。database/sqlがslog.Valueみたいなのを使うようになったら、garbageを大量に生成しなくなる日が来るかも?
go1.15でsmall integerのpackingがinterfaceにre-addedされたんだよ:https://go.dev/doc/go1.15#runtime
いや、違うんだ。go 1.15には最初の256個のpositive integerのstatic arrayがあって、interfaceのためにboxingする必要があるときは、そのarrayへのpointerを取得するんだ:https://go-review.googlesource.com/c/go/+/216401/4/src/runti… このarrayはsingle-byte stringにも使われるんだ(以前は独自のarrayがあった):https://go-review.googlesource.com/c/go/+/221979/3/src/runti…
>It didn’t, do what?
それの何が違うの?最初の256個のintegerは“small integer”だと思うけど😉
>Converting a small integer value into an interface value no longer causes allocation
single byte stringにも使えるのは知らなかった。それが役に立ったことはないけどね!
>It didn’t, do what?
>Reintroduce “packing into interfaces”.
完全に違うことをしたんだよ。small integerはinlineされないまま。
アロケートにかかる時間とかリソースも考慮してる?高パフォーマンスシステム作るなら、GCだけじゃなくてアロケーションも最小限にしたいよね。
結局は“do less”ってことじゃん?それってどんなパフォーマンスガイドにも書いてあるし。アロケーションも他のアプリの処理と変わらないよ。
僕が言いたいのは、よくある「アロケーション減らしてGCの負担を減らす」ってアドバイスが全てじゃないってこと。短いアロケーションはGCの負担にならないし。みんな時間かけてアロケーション減らしても、結局GCの時間は変わらないってことあるある。
言いたいことはわかるけど、それってコインの裏表だよね。完全なパフォーマンス分析せずに、ある方法だけが有効だって言うのは違うと思う。(君はGCのmark時間を減らすこと、他の人はアロケーション時間を減らすこと、この記事に書いてある他の方法も同様)
ちなみに、GoのGCについてはhttps://tip.golang.org/doc/gc-guideを見てね。
GCの頻度は、アロケーションレート(バイト単位)とライブヒープサイズに直接影響されるよ。例えば:
-アロケーションレートを半分にすると、GCの頻度も半分になる。
-ライブヒープサイズを2倍にすると、GCの頻度は半分になる(GOGC=100の場合)。
>…でもGoアプリのpprofを見ると、時間がかかるのはGCのmarkフェーズで、GCのsweepじゃないよね。
>sweepがmarkよりずっと安いのは確かで、君の次の発言も:
>短いアロケーションはGCのmarkフェーズに到達しないから、GC時間にほとんど影響がない。
…技術的には正しい。でも、2つの重要な点を省略してる:
-短いアロケーションを大量に生成すると、GCがより頻繁にトリガーされる。
-ライブヒープサイズを小さくすると(何も保持しないようにすると)、GCがより頻繁にトリガーされる。
だから、GCサイクルは安くなるけど、回数は増える。それに、アロケーションコストも大幅に増加する。これが本当に得策なのかはわからない。僕の経験ではそうじゃない。
興味深いね、ありがとう。でも、それらの点はそんなに相関性がないと思うな。例えば、ループ内で不要なラッパーを作成すると、アロケーションレートは2倍になるかもしれないけど、ライブヒープサイズは半分にならないよね?だって、以前はループの外にラッパーがなかったんだから。
要するに、スタイル変更(例えば、すべてのエラーに対してラッパーを作成したり、time.Timeの代わりに生の整数を使用したりする)が与える影響を見積もりたいんだ。そして、どんな例でも、君の指摘の一方に他方よりもはるかに影響を与えると思うから、「短いイテレータを作るのは全く問題ない」って主張できると思う。
詳細な回答ありがとう。議論の価値を高めてくれて感謝するけど、僕のコメントの意図が伝わってない気がする。
僕は「アロケーションを減らしてGCの負担を減らす」って blanket statement に反対なんだ。それが人々を間違った方向に導いてる。Go benchの“allocs/op”でライブラリを比較したり、sync.Poolの馬鹿げた(誰がタイトループで8KBもアロケートするんだ?)マイクロベンチマークを信じたりして、GCの問題を解決しようとする。そして、多くの努力を費やした結果、GC時間がほとんど変わらなかったことに気づくんだ。
一般化するなら、僕の「長生きするアロケーションを避ける」とか、君の「バイト単位でのアロケーションレートを減らす」って方が、この記事や他の多くの記事が説いていることよりも、実際にははるかに役に立つよ。
.NETでも同じような話だね。まずインナーループがallocation-freeであることを確認して、次にallocationが短命であることを確認して、最後に大きなallocationのlong tailをクリーンアップする。
.NETは、GCがgenerationalで全体的にもっと洗練されているから、高いアロケーショントラフィックにもっと寛容だよ(tail latencyというコストはあるけど、それはワークロード次第)。
LOHに行くような巨大なアロケーションはかなり厳しいけど、実質的なinter-generationalトラフィックでも死ぬことはない。
ランタイムは2分ごとにGCを強制するからね。だから、頻繁にアロケートしなくても、長生きするアロケーションはGCに負担をかける可能性がある。だからDiscordはRead StatesサーバーをGoからRustに移行したんだ。
まぁ、そうかもね。hot loopでアロケートしてたら、どっちにしろ最悪だよ。Object poolは、汎用アロケータよりも効率がずっと良いから、高パフォーマンスを目指すならマジで重要。
GCを完全に避けるって話じゃなくて、allocationの圧力を減らすのが大事なんだよね。アツいループでallocを避けられるなら、絶対やる価値あり。もし無理でもsync.Poolが使えるなら試してみて。allocを半分に減らしても大したことないかもしれないけど、100万回のループで毎回allocationしてたのを無くせたら、たとえすぐに死ぬallocでも効果あるよ。実際、このテクニックでパフォーマンスが2倍以上になったコードもある。 mark phaseが重いのは同意。でも、短命なallocationを減らす価値がないってのは違うと思うな。Goプログラムのパフォーマンス分析をよくするんだけど、1秒あたりのbytes allocatedを減らすのは常に有効だよ。 +1。特に[]byte sliceのallocationはGCのペースを左右することが多いし、sync.Poolとかで簡単に最適化できることが多いよね。 pprofだけじゃ全部見れないから、システムプロファイラも見てみるといいかも。 allocation自体、GCに関係なく結構コストかかるんじゃない? Goのallocationはそんなに悪くないよ。数年前にベンチマークしたとき、bump allocationの4倍くらいのコストだった。だから、allocationが多い状況ではarenaが有効だけど、ほとんどの場合はそこまでやる価値ないかな。 $COMPANYのかなり最適化されたmallocと比較すると、Goのallocatorは(相対的なサイクル数でも、Goプログラム全体のサイクル数でも)C/C++のallocatorよりかなりコストが高い(3~4倍だったと思う)。GCのmetadataの設定とか、zeroingとか、やることが多いからね。最近 それ本当?mallocより3~4倍も遅いってことは、そっちのmallocはbump allocatorってことになっちゃうけど、そんな実装はありえない(普通はmodified free list allocatorだよね)。 訂正と明確化: 違うよ。moving multi generational GCなら、allocationは短命なオブジェクトに対してはincrementするだけだよ。 movingでgenerationalなGCだと、高速allocationのメリットはデータの移動とコストのかかるメモリバリアで全部なくなっちゃうんだって。 そんなことないって。ほとんどのオブジェクトはすぐ死ぬから移動しないし。移動するまでの時間もCPUの処理に比べたらめっちゃ長いから、統計的にしか影響ないよ(スループットは高いけど、たまにレイテンシが長くなる)。write-only barrierもそんなにオーバーヘッド大きくないし。 オブジェクトがすぐ死ぬかどうかは関係なくて、ヒープにある他のオブジェクトが定期的に移動するせいでパフォーマンスが落ちるんだよ。moving GCを使うと、movingじゃないGCにはないread barrierも必要になるし。 OSがスレッドをcontext switchするのに比べたら、その周期ってそんなに気になる?CPUのタイムラインで見たら、全然珍しいことじゃないよね。それに、ハイパフォーマンスなGC runtimeは全部movingでgenerationalな方式を選んでるんだから。 OSがcontext switchするからって、その時間が無視できるわけじゃないよ。その間、コードは本来の処理をしてないんだから。Generational GCはmoving GCでデータの移動によるパフォーマンス低下を減らすために使われてるんだ。movingじゃないGCは侵略性が低いから、generational GCが必要なくて、完全にconcurrentにできる。 Generational GCはmoving collectorをさらに改善して、メモリの使用効率を上げてmarkフェーズを短くするものだと思うな。完全にconcurrentなGCってある?ZGCが一番concurrentらしいけど、read barrierとかポインタのトリックでstop-the-world時間をヒープサイズに依存させないようにしてるんだって。 Javaにはまだ完全にconcurrentなGCはないし、管理するgarbageの量とかオブジェクトを移動させるってことを考えると、完全にconcurrentなGCができる可能性は低いと思う。でも、movingじゃないGCなら完全にconcurrentにできるよ。C++のSGCLプロジェクトがそう。GoのGCが将来的に完全にconcurrentになる可能性が一番高いと思う。 SGCLってあんたのプロジェクト?もしそうなら、managed pointerとかread flagにatomic writeしてる?Redditでいくつかコメント読んだけど、flagはメモリページ単位っぽいね。でも、同期のオーバーヘッドが他の方法より大きいかもしれないし、JavaみたいにGC研究の最先端と比較できないから、本当にメリットがあるかどうかはわかんないね。TLA+で設計をモデル化してみた? へー、それってGoだけじゃなくて、他のmark-and-sweep GC(JavaとかC#)でも同じなんじゃない?ってことは、短い間しか生きないオブジェクト(loopのiteratorとかwrapperとか)を作るのは全然ありなんだね。 GCのために短い間しか生きないallocationをする価値ってある?allocationが増えすぎて、逆に遅くなるかもしれないじゃん。 最初の例のobject poolsだけど、こんなのありなんだって驚いたよ。しかも警告も出ないし。 これはstatic typingじゃないけど、strong typingだよ。static vs dynamicとstrong vs weakの話。 strongでstaticでstructuralだね。でもstructural typingはコンパイル時のduck typingみたいなものだから、dynamic typingと混同する人がいるのも理解できる。 Ggpが言ってるのはstructural typingじゃなくて、 >Ggp is not talking about structural typing、 たしかにgenericsは便利かもね。でも upstreamもtype-saferな >While I think you’re right (generics might be useful there), it’s fairly easy to wrap the 誤解しないでほしいんだけど、I agreeだよ!performance的にも、want whatever I wantをbuildできるprimitiveが欲しい。generic primitiveはperformanceがちょっと悪くて、自分をshoot in the footしないようにtuneしなきゃいけない。 ケースバイケースだね。 型システムには、型ルールを破れる抜け道があるのが普通だよね。例えば、OCamlのObjモジュールにある”magic”関数とか。 抜け道がない型システムなんてないけど、Goみたいに頻繁に型システムを破る必要がある言語は見たことないな。 ジェネリクス導入前のJavaやC#と似たようなもんだよ。理由はほぼ同じ。 多くの言語にトップ型って概念があるよね。 Goでプログラミングしたことないでしょ? >New関数がそうじゃないの? >コンパイルできれば正しいってわけじゃないんだ。 レベルによってコストとメリットがあるよね。窓に鉄格子はないけど、玄関には鍵をかける。Golangはコンパイルで多くのエラーを検出できるし、テストで残りを検出できる。PythonやPerlみたいにテストだけが頼りって状態よりマシ。 >コンパイルで多くのエラーを検出できる GPが期待してるのは、Golangのallocationみたいにsync.Pool()が常にゼロ構造体を返すことだと思うんだよね。Pool[T]型を新しく定義して、この保証を得ることもできるよ。intpoolとかboolpoolの例もあるよ。 >One could define a new “Pool[T]” type (extending sync.Pool) to get these guarantees:” >You completely forgot to address the expectation >I don’t get what else one expects when pooling pointers to a type? >And、unfortunately、doesn’t even get the generic constraints right、as demonstrated with the int and bool examples. >If those constraints don’t hold (like you say) it should manifest as runtime panic、no? それは設計図だよ。Embeddingとかtypedefは、その保証を実現する方法。ジェネリックなpoolライブラリを書くのが目的じゃないんだ。 >What does it even mean? >Values are copied in Go >then the code was to demonstrate that sync.Pool could be “extended” with other types and custom logic. その保証はどこにあるのさ?あんたの書いたことを読み直した方がいいんじゃない?俺がsync.Poolでみんなの問題を解決しなかったって怒ってるみたいだけど、そんなこと言ってないし。Pool[T]型を新しく定義(sync.Poolを拡張)すれば、その保証が得られるかもね。まあ、sync.Poolを拡張してカスタムタイプ向けに保証を得るってこと。intとかboolの例もあるけど、コピーされるからpoolingは効果ないって言ってる通り。でも、sync.Poolの拡張方法を示したかっただけで、他意はないんだ。 >あんたの書いたことを読み直した方がいいんじゃない? GPが期待してるのは、sync.Pool()が常にゼロ構造体を返すってことかもね。でもそれって型とかジェネリクスとは関係ない設計判断だよね?プールのドレイン関数が必要ないって言ってるみたいだけど、それって珍しいよね。 >Not in the slightest. >But it’s simply a fact that using the anyは特別な型じゃないよ。interface{}のエイリアスだよ。空集合はすべての型で満たされるけど、必要に応じて絞り込める。 Pythonの型がないのと、anyから値を取り出すのを比べるのはおかしいでしょ。 Pythonには"total absense of typing"はないよ。静的型付けがないから、コンパイル時のチェックができないだけ(最近は擬似的な静的型付けもあるけど)。 >There’s no way in Java, Rust or C++ to express this either ゼロコピーってマジで過小評価されてるよねー。このサイトでも言ってるように、Go のインターフェースのおかげでゼロコピーのコードが比較的書きやすいんだけど、それでも工夫は必要だよね。でも、それに見合うだけの価値はあると思う。メモリの割り当てとか、データのやり取りにめっちゃ時間使ってることに気づいてびっくりすることがよくあるんだよねー。もっとコメントを表示(1)
runtime.mallocgc
の最適化があったから、3~4倍っていうのは少し減ってるかも。mallocgc
が速くないのはわかるけど、そんなに遅いとは思わなかったな。普通のmalloc関数と同じくらい速いと思うけど、測ったことはないし、比較するのは難しいよね(CGo経由でmallocを呼ぶか、CとGoで書いてループのコストが同じになるようにしないといけないし)。
相対的な意味で3〜4倍高価であるという意味でした。
C + +プログラムの場合、アロケーター(割り当て+解放)はサイクルのおおよそ5%を消費します。
Goプログラムの場合、アロケーター(runtime.mallocgc)は〜20%のサイクルを消費していました(これは私が参照したデータです)。確認したところ、最近では最適化のおかげで15%近くになっています。
バイトレベルでのパフォーマンスの差はテストしていません(Goのオブジェクト構造によっても異なります)。もっとコメントを表示(2)
sync.Pool
ってgenerics以前からあるからany
を受け取って返すんだよね。Goって強い型付けが原則だけど、型システムから抜け出すAPIが多くて、メリットがなくなってる気がする。
ちょっと面白いことをしようとすると型システムを切らないといけないなら、意味なくない?
それと、初期値に戻すAPIがないのも気になる。Clear
みたいなコールバックがあっても良くない?
https://stackoverflow.com/a/11889763sync.Pool
の型消去のことだよ(any
の値を受け取って返す)。だから、変なgarbageが入る可能性がある。
いや、返信先はtypingがstaticかどうかをquestionしてたんだよ。structural typingによってstaticなんだ。コンパイラがdacksがcompatibleかどうかを強制する。でもempty interfaceにはconstraintsがないから、全てのtypeはcompatibleになる。他のcommentが何を言ってたかはirrelevant。
>but about sync.Pool type erasing
typeはeraseされてないよ…?p := sync.Pool{New: func() interface{} { return 1 }}
fmt.Printf(”%T”, p.Get())
// Prints: int
>So you can put (and will retrieve) random garbage from it.
それがdynamic typeだってこと?違うでしょ。sync.Pool
とかsync.Map
みたいなprimitiveはuse caseに合わせてwrapするのが簡単だよ。
Goはbreaking changesに厳しいから、今の実装は変わらないと思う。v2が出るかもしれないけど、どうだろうね。codeが多いほどmaintainが大変だし。sync.Pool
はgood ideaだと思ってるみたい。
https://go.dev/issue/71076
でdiscussionされてるよ。sync
primitives such as sync.Pool
and sync.Map
into your specific use case。”
それってstrong argumentじゃないよね。どんなAPIでも、typeをrestrictするwrapは簡単にできる。Genericsならそのworkをしなくて済むし、書かないcodeにはerrorsがない。Sync.map
ってperformanceがbadなはず。
https://github.com/golang/go/issues/21031
Godocによると、Map型は2つのユースケースに最適化されてるんだ。(1)キーに対する書き込みが1回だけで読み込みが多い場合、例えばキャッシュとか。(2)複数のgoroutineがバラバラのキーに対して読み書きする場合。この2つならMutexよりlockの競合が減るかも。
https://pkg.go.dev/sync#Map
あと、書き込みが遅い問題はGo 1.24で改善されたみたい。
https://go.dev/doc/go1.24#minor_library_changes
これは、型システムの制限を回避するための手段なんだ。
wrapperを作れば、コードの安全性を保てるしね。
関数型インターフェースで命令型の実装を包むのも同じ考え方だ。表面的には関数型だけど、内部では命令型コードを使ってる場合もある(効率のため)。
簡単なことならGoの型システムは役立つけど、複雑なことをしようとすると、すぐ投げ出されるんだよ。そんな時にこそ型システムに頼りたいのに。
データベース接続のプールが突然文字列を返すなんて心配したくない。pool.Get()
の型はany
で、Goのワイルドカード型なんだよ。値を取り出すには、正しい型をアサートする必要がある。これはジェネリクスでは解決できない。JavaやRust、C++でも無理だよ。Goが後方互換性を持ってるから、この構造は残るんだ。
>初期化されたデフォルト値にリセットするAPIがないって?
New関数がそうじゃないの?
あと、コードにコンマがないよ。
New関数はプールがスペースを確保する必要がある時にしか呼ばれないじゃん。sync.Pool()が常にゼロ構造体を返すと思ってるみたいだけど、Golangのallocationと同じようにね。
sync.Pool()はパフォーマンスが重要な場合に使う最適化だから、必要な部分だけ初期化するのは理にかなってる。でも、そう思うのも無理はないかも。
>[any]は型
Pythonみたいな型付けで、RustやCとは違う。コンパイルできれば正しいってわけじゃないんだ。
sync.Poolは使わないけど、ジェネリクスがあるなら型付きのプールの方が良いと思う。
みんながそう思ってるなら、Coqとかがもっと有名になってるはずだよ。普通に使われてる言語はテストが必要で、その過程で型も検証される。”機械がコードをリファクタリングしてくれる”ってのが静的型付き言語の魅力で、”ネットで記事を書ける”ってのが二番目の魅力。
リファクタリングにはすごく便利だよね。でも、プログラムの正確さについては、あまり意味がない。もっとコメントを表示(3)
https://go.dev/play/p/-WG7E-CVXHR
それって、自分がそうじゃない限り?Foo構造体の例で、期待を完全に忘れちゃってるよ。pool.Get()で10を設定してpool.Put(a)したあと、もう一度pool.Get()すると10が表示されちゃう。0が期待されてるのに。
>fmt.Println(b.V) // Prints: 10; 0 was expected.
ポインタをpoolingするときに何を期待してるのか、正直よくわかんないんだよね。[]uint8とか[]byteをpoolingするのはよくあることだし、Pool.Put()とかPool.Get()で中身をゼロにする必要があるのかな?
前のコメントにもあるように、期待されてるのは常にゼロ値が返ってくることだよ。“GPが期待してるのはsync.Pool()が常にゼロ構造体を返すこと”なんだから。それを保証するって言ったんだよね?Pool.Put()とかPool.Get()で中身をゼロにしないと。それなのに、そうしてないじゃん。sync.Poolと全く同じ動きしかしないし…ジェネリクスの制約も間違ってるし。
もし制約が正しくないなら、runtime panicになるんじゃないの?
>What GP seems to expect is that sync.Pool() would always return a zeroed structure
アドレスをPoolするときは気をつけないとね。
>But you completely forgot to do it、which questions what your code was for?
ポインタにゼロ値を期待するなら、New funcはnilを返すか、Pool.Get/Putでゼロにする必要があるね。レビューありがと。
intとかboolのpoolは問題なく動くよ。panicしたらコードを投稿しないよね。でも、正しくないんだよ。
>I did not forget?
じゃあ、保証はどうなってるの?“Pool[T]型を新しく定義して、この保証を得ることもできる”って言ったよね?なんで守れない約束をするの?
>but are not correct.
意味がわからない。制約が正しくないって言ったよね?どういう意味?
Goでは値がコピーされるんだよ。コードは動くけど、期待通りには動かない。poolのユーザーがミスしないように任せっきりだよね。それはある程度は良いけど、sync.Poolだけでも同じことができるから、あなたのコードは何のためにあるの?
なるほど、ありがとう。
>so what is your code for?
それが修辞的な質問じゃないなら、sync.Poolをtypedefとかembedで拡張できることを示すためだよ。pooling自体が正しいかどうかは焦点じゃないんだ。
その保証はどこにあるの?コードはゼロ値も保証しないし、型安全ですらない。そもそも、Goに詳しい人が、そんな標準的な機能を unawareなわけないじゃん。
全角の>元の文からコロンを”忘れ”てコピーしてるじゃん。意味を考えると、わざと省いたのかな?俺が元のコメントを読んでないと思ってた?
全角の>怒ってるみたい
全角の>ネットで怒るなんてありえないでしょ。もし怒りそうになっても、その前にパソコン消すって。楽しくないなら使う意味ないし。
全角の>One could define / extend sync.Pool to get those guarantees [for their custom types] …
全角の>誰が興味あるの?sync.Poolの複雑さを理解してて、型定義や関数も書けない人なんていないでしょ。
>It’s typed the way Python is typed
全然違う。
>”if it compiles there’s a good chance it’s correct”
Rustのunwrap()みたいかも。間違った結果に適用するとpanicするやつ。
それってUSENETでよくある、議論を呼ばないコメントだよね。Goは好きでよく使うし、any
型にフォールバックできるのも良いと思う。でもany
型を使うってことは、コンパイル時にチェックできない性質があるってこと。Pythonも同じ。
>If you want to compare it to something, it’s more like Rust’s unwrap(), which will panic if you apply it to the wrong result.
Rustのunwrap()は、型が2つしかない時に使うんだよね(だから型指定がない)。any
型は、型が何でもありってこと。例にあるように、int、string、空のstructをプールに入れられる。多分それは求めてることじゃないけど、コンパイラは止めないよ。any
type means that certain properties of the program can’t be checked at compile time
構造的型付けは、コンパイル時にチェックできる性質を減らすよね。でも、だからってPythonみたいになるわけじゃない。
>certain properties of the program can’t be checked at compile time
数字がプラスかマイナスか、stringが空かどうかはコンパイル時にチェックできないけど、だからってGoがCOBOLやForthみたいになるわけじゃない。var v any
は、vがany型だって宣言してるんであって、任意の型ってわけじゃない。Pythonとは違う。v + 1
はコンパイルエラーになるけど、Pythonはランタイムエラーになるかも。特にインターフェースを考えると、全然違う。Pythonでintegerを宣言しても保証はないけど、Goでは保証がある。例えばjsonを扱う時に重要になってくる。
>the compiler won’t prevent you from doing it.
stringの配列をintの配列として使うことはできないよ。Pythonはできる。全然違う。+
を呼べるオブジェクトと呼べないオブジェクトがあるのは、型が違うからだよ。
本当に型のない言語(またはすべてが単一の型)はASM。レジスタのビットフィールドにどんな操作もできる。文字配列からロードされた64ビットがstringだって?OK、他のレジスタとbitwise andできる。u64?ポインタ?u32のペア?同じこと。意味は変わらない。
それを表現できるのが良いことみたいに言うなよ。Java、Rust、C++でそれが表現できないのは、言語設計者のおかげだ。
any型とかキャストを使わずに、複数の型を持つpoolの値を表現したいなら、Rustのunion型とか、Javaのinterfaceを実装した複数の具象オブジェクトを使えばいい。どちらも、チェックされないduck typingなしに、明示的に値を確認する必要がある。