🧐

なぜ `[]T` は `[]any` に変換できないのか?~Goの仕様を考える~

2023/05/10に公開1

導入

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 を使って、変数 isiis のメモリ上の表現を調査してみました。

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のここの話かなと思います
https://github.com/golang/go/wiki/InterfaceSlice

Each interface{} takes up two words (one word for the type of what is contained, the other word for either the contained data or a pointer to it). As a consequence, a slice with length N and with type []interface{} is backed by a chunk of data that is N*2 words long.

1度読んだことがあって覚えていたのですが、実際に確かめたことは無かったので勉強になりました!ありがとうございます!