Go言語100Tips No.23 - No.29

mizue
·

Go言語100Tips ありがちなミスを把握し、実装を最適化する の No.23 〜 No.29 まで読んだ。

引き続き、スライスの話とマップの話。

No.23 - No.28

スライスの誤りポイントは、長さと容量(キャパシティ)、そして基底配列。

例えば、以下のように、s2はs1を切り出したにも関わらず、同じ基底配列を参照する。なおかつ、容量が2なので、append でもとデータの3を10で上書きしてしまう、

s1 := []int{1, 2, 3} // s1は長さ3, 容量3のスライス

s2 := s1[1:2] // s2は長さ1, 容量2のスライス

s3 := append(s2, 10) // s2は容量が2であるため10を受け入れてs3を作る

このように、簡単にデータを壊してしまうことができる。データを切り出した先で何か変更を与えたい場合は、copyを使うととりあえず安全。または、フルスライス(スライス作成時に容量を指定する)を使う。

スライスは、メモリリークも意外と簡単に起こせてしまう。

スライスの場合、上記のように切り出した先で何かをやっていると、元の基底配列は参照されたままになるので、解放されない。また、ポインタフィールドを持つ構造体も解放されない。リークされないようにするには、スライスのコピーを使うか、残りのスライスを明示的にnilにする。

マップは、スライスでぐるぐる回すより効率が良いため、やや大きめのデータを扱う場合はマップを使うことがある。

マップは内部的にはハッシュテーブルのデータ構造で、拡大する時に大きさが2倍になる。そして、全てのキーが全てのバケットへ再配置される。そのため、最悪の場合、キーの挿入はマップの要素の総数をnとした時、O(n) の処理になる可能性がある。これを回避するには、マップ作成時にサイズを指定すること。

マップもメモリリークを起こす場合がある。大きいデータと多くの要素をマップに追加した後、すべての要素を取り除いても、マップ内のバケットは縮小できないため、期待したほどメモリは解放されない。

回避方法としては、データをそのままマップにいれず、ポインタを使うこと。そうすると、マップに保持するデータサイズはポインタのサイズしか必要なくなり、メモリを節約できる。

No.29: 値の比較の誤り

Goでは、値を比較す流場合、オペランドは比較可能(Comparable)である必要がある。スライスやマップはComparableではない。

Comparableではないものを比較する場合は、リフレクション(refrection)を使うと良いが、パフォーマンスが重要な場合は問題になることがある。そんな時は、自前で比較メソッドを実装すると良い。

こうしてみると、パフォーマンスに影響を及ぼす処理やメモリリークを起こすコードは意外と身近にあることがわかるので、気をつけなければいけない。特に、こう言ったものは、テストでは見つけにくい。

これで3章が終わった。

@mizue
Healthy Programmer