🧐
なぜ `[]T` は `[]any` に変換できないのか?~Goの仕様を考える~
導入
Goを書いていると以下のような関数を見つけました。
func hoge(vs []any) {
// 何かの処理
}
単純に []int{}
を引数として渡そうとするとコンパイルエラーが起こります。
ints := []int{1, 2, 3}
hoge(ints)
// cannot use ints (variable of type []int) as []any value in argument to hoge
どうやら単純に []int
を []any
に変換できないようです。
Goの公式のFAQにも同じような質問があり、2つの型がメモリ上で同じ表現にならないことが原因と書かれています。
It is disallowed by the language specification because the two types do not have the same representation in memory.
その違いを実際にdelveを使って確認してみました。
この記事で知れること
-
[]T
が[]any
に変換できない理由- 具体的にメモリ上でどういう違いが現れるのか
- delveの簡単な使い方
実際に調査してみた
対象のコード
以下のコードを対象に delve を使って、変数 is
と iis
のメモリ上の表現を調査してみました。
package main
var sum int64
func addUpDirect(s []int64) {
for i := 0; i < len(s); i++ {
sum += s[i]
}
}
func addUpViaInterface(s []interface{}) {
for i := 0; i < len(s); i++ {
sum += s[i].(int64)
}
}
func main() {
is := []int64{0x55, 0x22, 0xab, 0x9}
addUpDirect(is)
iis := make([]interface{}, len(is))
for i := 0; i < len(is); i++ {
iis[i] = is[i]
}
addUpViaInterface(iis)
}
delveの使い方
簡単に使い方を説明しておきます。
-
break
でブレイクポイントをつける -
c
はブレイクポイントまでコードを進める。continue
のエイリアス。 -
p
は式を評価する。print
のエイリアス。 -
x
は与えられたアドレスのメモリを調査する。examinemem
のエイリアス。
is
のメモリ上の表現
まずは変数 is
のメモリ上の表現から見ていきます。
❯ dlv debug main.go
Type 'help' for list of commands.
(dlv) break main.go:27
Breakpoint 1 set at 0x105f8f6 for main.main() ./main.go:27
(dlv) c
> main.main() ./main.go:27 (hits goroutine(1):1 total:1) (PC: 0x105f8f6)
22: iis := make([]interface{}, len(is))
23: for i := 0; i < len(is); i++ {
24: iis[i] = is[i]
25: }
26:
=> 27: addUpViaInterface(iis)
28: }
(dlv) p &is
(*[]int64)(0xc00005e740)
(dlv) x -fmt hex -len 32 0xc00005e740
0xc00005e740: 0x10 0xe7 0x05 0x00 0xc0 0x00 0x00 0x00
0xc00005e748: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xc00005e750: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xc00005e758: 0x00 0x20 0x10 0x00 0xc0 0x00 0x00 0x00
- 1行目は格納されている値の情報が入っています (arrayへのポインタが入っている)
- 2行目と3行目はそれぞれlenとcapの値が入っています。どちらも
0x04
なのでis
の定義と一致しています。
実際に格納されている値を見てみると
(dlv) x -fmt hex -len 32 0xc00005e710
0xc00005e710: 0x55 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xc00005e718: 0x22 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xc00005e720: 0xab 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xc00005e728: 0x09 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x55
, 0x22
, 0xab
, 0x09
の並びになっていて、実際にスライスに指定した値となっていることが確認できました。
iis
のメモリ上の表現
続いて変数 iis
のメモリ上の表現を見てみます。
(dlv) p &iis
(*[]interface {})(0xc00005e758)
(dlv) x -fmt hex -len 32 0xc00005e758
0xc00005e758: 0x00 0xe0 0x08 0x00 0xc0 0x00 0x00 0x00
0xc00005e760: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xc00005e768: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xc00005e770: 0xd0 0xe7 0x05 0x00 0xc0 0x00 0x00 0x00
is
と同様にスライスなので、2行目と3行目にはlenとcapの情報が入っています。1行目の情報 (arrayへのポインタ) をチェックしてみます。
(dlv) x -fmt hex -len 64 0xc00008e000
0xc00008e000: 0x80 0x40 0x06 0x01 0x00 0x00 0x00 0x00
0xc00008e008: 0x48 0xde 0x0b 0x01 0x00 0x00 0x00 0x00
0xc00008e010: 0x80 0x40 0x06 0x01 0x00 0x00 0x00 0x00
0xc00008e018: 0xb0 0xdc 0x0b 0x01 0x00 0x00 0x00 0x00
0xc00008e020: 0x80 0x40 0x06 0x01 0x00 0x00 0x00 0x00
0xc00008e028: 0xf8 0xe0 0x0b 0x01 0x00 0x00 0x00 0x00
0xc00008e030: 0x80 0x40 0x06 0x01 0x00 0x00 0x00 0x00
0xc00008e038: 0xe8 0xdb 0x0b 0x01 0x00 0x00 0x00 0x00
奇数行目の 0xc00008e000
と 0xc00008e010
と 0xc00008e020
と 0xc00008e030
は同じになっているので、多分これが型の情報だと考えられます。
偶数行目の情報を見てみます。
(dlv) x -fmt hex -len 8 0x010bde48
0x10bde48: 0x55 0x00 0x00 0x00 0x00 0x00 0x00 0x00
(dlv) x -fmt hex -len 8 0x010bdcb0
0x10bdcb0: 0x22 0x00 0x00 0x00 0x00 0x00 0x00 0x00
(dlv) x -fmt hex -len 8 0x010be0f8
0x10be0f8: 0xab 0x00 0x00 0x00 0x00 0x00 0x00 0x00
(dlv) x -fmt hex -len 8 0x010bdbe8
0x10bdbe8: 0x09 0x00 0x00 0x00 0x00 0x00 0x00 0x00
上から 0x55
, 0x22
, 0xab
, 0x09
の並びになっていて、実際にスライスに指定した値となっていることが確認できました。
結論
- 確かに
is
(intのスライス) とiss
(anyのスライス) ではメモリの構造が違っていることをdelveで確かめることができました - どうやらスライスが内部で保持しているarrayの構造がintのスライスとanyのスライスで異なっているようでした
Discussion
大変興味深く読ませていただきました!
最後のisとiisのメモリ上の表現の違いについてですが、Goの公式wikiのここの話かなと思います
1度読んだことがあって覚えていたのですが、実際に確かめたことは無かったので勉強になりました!ありがとうございます!