Goの構造体とインターフェースを調べる
はじめに
特に込み入ったことを予習することなくGoを使い始めたが、構造体のポインタ型と値型のレシーバや、インターフェースに関する挙動については正直理解できなかったので、ちょっと調べてみた。
しかしA Tour of Goを見ればだいたい解決する気がするのでそっちを見てわかった人はこれを読む必要が無いかもしれない。
思ったことを書いているが厳密な仕様などを調べているわけではないので、雰囲気でふわっと理解するような感じで。
ちなみにいくつかのサイトを検索して調べたり、Google Geminiさんに聞いたりして勉強した。Github CopilotさんはGithub Actionsのエラーを質問していたら速攻で制限きて1か月待たれよって言われたので放置プレイ中。無料のコーディング支援もそろそろ尽きそうだがいらんかなって思っている。
構造体のレシーバについて
わからないところ
Goの構造体のメソッドはレシーバとしてポインタ型で書いたり値型で書いたりできる。呼ぶときはレシーバ.メソッドという書き方をするが、レシーバがポインタでも値でも関係なく呼べる。何が何だかわからない。
調べた結果
つってもA Tour of Goの6ページと7ページに書いてあった話なのだが。
これを雰囲気的解釈すると、
- 構造体の定義はポインタ型と値型の2つがセットになっていて、レシーバの書き方によりどっちに定義されるかが決まる。
- 呼ぶときにレシーバの型によってメソッドを探すが、なければもう片方の定義を自動的に探して、見つかったらキャストして呼ぶ
- この処理はコンパイラがやっていて、自動解決と呼ぶらしい。
という感じではないかと思っている。
値型のレシーバを呼んだ場合は構造体がコピーされることになるから、構造体のサイズや処理内容によってメソッド実装時に選択することになる。このことからGoでは、構造体をコピーするかポインタ渡しで処理するかは呼び出し側ではなく、定義側で選択すべきである、と主張をしていると言える。
なるほど?
インターフェースについて
わからないところ
インターフェース型の変数に値型を入れたりポインタ型を入れたりできる。どういう違いがあるのかわからない。あと、mapのキーにインターフェースを入れたときの挙動がわからない。
調べた結果
インターフェースの中身について
つってもA Tour of Goの11ページに書かれているわけだが。
インターフェースの値は(value, type)の形になっていて、代入したものがvalueに、代入したものの型がtypeに格納される。これはかなり特殊で、変数の中身がこういう形になっているのだな。
値型の構造体値を代入するとValue部分に構造体値がコピーされるし、ポインタ型を代入するとアドレスがコピーされる。つまり何がコピーされるか、が違う。必然的に処理負荷と、中身の更新が可能かどうかが変わる。
また、インターフェース型はImmutableのようで、値型を代入した場合に、レシーバがポインタ型になっているメソッドを呼ぶことができない。エラーになる。これはインターフェース型の(value, type)のvalue部分が更新されてしまうことになるからだと思われる。同様に、値型を代入したインターフェース型をアサーションで構造体に見えるようにして、そのまま更新しようとしてもエラーになる。理由は同じだ。
このあたりはこちらの記事に書いてあったお話。
mapのキーについて
GoのmapというのはPythonや.netで言うDictionary、Javaで言うHashMap、Rubyで言うHash、Perlで言う連想配列、Luaで言うTable、JavaScriptで言うObjectの事である。最初mapって関数型言語のmapかな?変わったモノがあるね?って思ってたので調べてビックリ。
ともあれ、ここまでの経緯でmapのキーについての挙動はだいたい想像できるし、Geminiさんに聞いて勉強した。
Goのmapはキーの部分に入れられないものが一部あり、スライスだったりfuncだったりmapだったりが入れられない。これらを含む構造体も入れられない。ハッシュ値を計算して比較することができないものはキーにできない、ということだそうだ。
逆に、これらを含まない構造体やインターフェースであれば入れることができる。構造体値であればそれぞれの値を比較することができるし、インターフェースであればvalueとtypeで比較ができる。ポインタ型でもアドレスで比較できる。
なるほど?
おしまい
だいたいわかってきたような気がする。自分のコードを見直しても大丈夫そうな気がする。たぶん。
Discussion