1
1

More than 5 years have passed since last update.

Go言語 スライス・string・interface{}の内部構造をプログラミングによって調べてみた

Posted at

概要

スライス・string・interface{}の内部構造をプログラミングによって調査していきます。その準備として変数の型の再定義の方法を考えます。最後にGoのソースコードを調べたものも載せました。

環境

$ go version
go version go1.12 windows/amd64

準備

変数が占めているメモリサイズの取得

unsafe.Sizeofを使います

main.go
package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var sliceVal []int
    var stringVal string
    var interfaceVal interface{}
    fmt.Printf("sliceVal:%d, stringVal:%d, interfaceVal:%d\n",
        unsafe.Sizeof(sliceVal), unsafe.Sizeof(stringVal), unsafe.Sizeof(interfaceVal))
}

実行結果

$ go run main.go
sliceVal:24, stringVal:16, interfaceVal:16

変数が占めている領域を定義と違う型で再定義する

var num uint = 0x0102030405060708[8]uint8で再定義して表示を試みます。

main.go
package main

import (
    "fmt"
)

func main() {
    var num uint = 0x0102030405060708
    reNum := (*[8]uint8)(&num)
    fmt.Println(*reNum)
}
$ go run main.go
# command-line-arguments
.\main.go:9:22: cannot convert &num (type *uint) to type *[8]uint8

このままでは型変換できないようです。そこで使うのがunsafe.Pointerです。

main.go
package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var num uint = 0x0102030405060708
    reNum := (*[8]uint8)(unsafe.Pointer(&num))
    fmt.Println(*reNum)
}
$ go run main.go
[8 7 6 5 4 3 2 1]

無事表示できました。数字の順番がnumと逆になっているのはCPUがIntel製でリトルエンディアンのためです。
これでも十分ですが、構造体を使った方が後々便利です。さらにfmt.Printfに変更しています。

main.go
package main

import (
    "fmt"
    "unsafe"
)

type memory struct {
    field1 [4]uint8
    field2 [2]uint16
}

func main() {
    var num uint = 0x0102030405060708
    reNum := (*memory)(unsafe.Pointer(&num))
    fmt.Printf("%#v\n", *reNum)
}
$ go run main.go
main.memory{field1:[4]uint8{0x8, 0x7, 0x6, 0x5}, field2:[2]uint16{0x304, 0x102}}

スライスの調査

スライスは24バイトなので、とりあえずuint型3個として中身を見てみます。

main.go
package main

import (
    "fmt"
    "unsafe"
)

type memory struct {
    field1 uint
    field2 uint
    field3 uint
}

func dumpMemory(s []int) {
    p := (*memory)(unsafe.Pointer(&s))
    fmt.Printf("%+v\n", *p)
    fmt.Printf("len: %d, cap: %d\n", len(s), cap(s))
}
func main() {
    var sliceVal []int
    sliceVal = make([]int, 1, 2)
    sliceVal[0] = 12
    dumpMemory(sliceVal)
    sliceVal = append(sliceVal, 34)
    dumpMemory(sliceVal)
    sliceVal = append(sliceVal, 56)
    dumpMemory(sliceVal)
    sliceVal = append(sliceVal, 78)
    dumpMemory(sliceVal)
    fmt.Println(sliceVal)
}
$ go run main.go
{field1:824633762016 field2:1 field3:2}
len: 1, cap: 2
{field1:824633762016 field2:2 field3:2}
len: 2, cap: 2
{field1:824633795808 field2:3 field3:4}
len: 3, cap: 4
{field1:824633795808 field2:4 field3:4}
len: 4, cap: 4
[12 34 56 78]

結果を見てみるとfield2が要素数(len)で、field3が容量(cap)であることがわかる。field1はアドレスっぽいです。容量(cap)が変わらないときはfield1の値も変わらないようです。つまり容量が増えると配列用のメモリを新たに割り当てていると推測されます。次にfield1uint型の配列ポインタとして中身を見てみます。プログラムはmemorydumpMemoryのみ変更します。

main.go
type memory struct {
    field1 *[10]uint
    field2 uint
    field3 uint
}

func dumpMemory(s []int) {
    p := (*memory)(unsafe.Pointer(&s))
    fmt.Printf("field1: %v\n%+v\n", *p.field1, *p)
    fmt.Printf("len: %d, cap: %d\n", len(s), cap(s))
    fmt.Println()
}
$ go run main.go
field1: [12 0 0 0 0 0 0 0 0 0]
{field1:0xc000066080 field2:1 field3:2}
len: 1, cap: 2

field1: [12 34 2322222865247988070 0 2322222865247988070 2319389199166484827 1 2 7811852430209544791 101]
{field1:0xc000066080 field2:2 field3:2}
len: 2, cap: 2

field1: [12 34 56 0 0 0 0 0 0 0]
{field1:0xc0000640e0 field2:3 field3:4}
len: 3, cap: 4

field1: [12 34 56 78 0 0 0 0 0 0]
{field1:0xc0000640e0 field2:4 field3:4}
len: 4, cap: 4

[12 34 56 78]

34を追加したときfield1のアドレスが変わっていないのにfield1[2]以降が変わっていますが、スライスで用意した領域はfield1[1]までなので、他の処理で使われたのだと思います。

結果、スライスは次のような内部構造になっているようです。

相対アドレス メモリの内容
0~7バイト目 実データの配列があるメモリのポインタ
8~15バイト目 要素数
16~23バイト目 容量

stringの調査

stringのメモリサイズは16バイトなので、uint型2個として中身を見てみます。

main.go
package main

import (
    "fmt"
    "unsafe"
)

type memory struct {
    field1 uint
    field2 uint
}

func dumpMemory(s string) {
    p := (*memory)(unsafe.Pointer(&s))
    fmt.Printf("%+v\n", *p)
}
func main() {
    dumpMemory("abcde")
    dumpMemory("あいうえお")
}
$ go run main.go
{field1:5003377 field2:5}
{field1:5009736 field2:15}

結果を見ると、field1はアドレスのようで、field2は文字数ではなくバイト数のように思われます。そこで、以下のURLを参照してUTF-8の文字コードを調べてみました。ひらがなは3バイトなのでfield2の値と一致します。

文字 UTF-8文字コード
a 61
b 62
c 63
d 64
e 65
E38182
E38184
E38186
E38188
E3818A

そこで、field1をbyteの配列として中身を表示してみます。プログラムはmemorydumpMemoryのみ変更します。

main.go
type memory struct {
    field1 *[20]byte
    field2 uint
}

func dumpMemory(s string) {
    p := (*memory)(unsafe.Pointer(&s))
    fmt.Printf("%+v\n", *p)
    for i := uint(0); i < p.field2; i++ {
        fmt.Printf("%d: %x\n", i, p.field1[i])
    }
    fmt.Println()
}
$ go run main.go
{field1:0x4c69d1 field2:5}
0: 61
1: 62
2: 63
3: 64
4: 65

{field1:0x4c82af field2:15}
0: e3
1: 81
2: 82
3: e3
4: 81
5: 84
6: e3
7: 81
8: 86
9: e3
10: 81
11: 88
12: e3
13: 81
14: 8a

調べたUTF-8のコードと一致しましたのでfield1byte配列のポインタで間違いないようです。

結果、stringは次のような内部構造になっているようです。

相対アドレス メモリの内容
0~7バイト目 実データのbyte配列があるメモリのポインタ
8~15バイト目 byte数

内部構造が分かったところでstringはイミュータブルで変更不可ですが、無理やり変更してみたくなります。次のような先頭の英文字を変更するプログラムを試みましたがpanicが発生しました。

main.go
package main

import (
    "fmt"
    "unsafe"
)

type memory struct {
    field1 *[20]byte
    field2 uint
}

func main() {
    str := "abcde"
    p := (*memory)(unsafe.Pointer(&str))
    fmt.Printf("str: %p, fiedl1: %p\n", &str, p.field1)
    fmt.Println(str, "\n*************************")
    p.field1[0] = '0'
    fmt.Println(str)
}

実行結果

$ go run main.go
str: 0xc0000461d0, fiedl1: 0x4c46ec
abcde
*************************
unexpected fault address 0x4c46ec
fatal error: fault
[signal 0xc0000005 code=0x1 addr=0x4c46ec pc=0x491c76]

以下省略

この結果を見るとstrのアドレスとabcdeが格納されているアドレス(field1)が随分違うことに気づきます。よってabcdeが格納されているメモリは書き込み不可属性ではないかと推測します。そこでstrと同じようなアドレスに割り当てられるようにして、再び変更を試みます。 main()のみ記載します。

main.go
func main() {
    str0 := "abcd"
    str := str0 + "e"
    p := (*memory)(unsafe.Pointer(&str))
    fmt.Printf("str: %p, fiedl1: %p\n", &str, p.field1)
    fmt.Println(str, "\n*************************")
    p.field1[0] = '0'
    fmt.Println(str)
}
$ go run main.go
str: 0xc00005c1c0, fiedl1: 0xc000070080
abcde
*************************
0bcde

field1のアドレスが書き込み可能なところに割り当てられたため変更が可能になったようです。

interface{}の調査

メモリサイズは16バイトなので2個のuintとしてメモリダンプします。中に入れるデータとしてもダンプ時に分かるようにintと同類にします。

main.go
package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

type memory struct {
    field1 uint
    field2 uint
}

func dumpMemory(val interface{}) {
    p := (*memory)(unsafe.Pointer(&val))
    fmt.Printf("%+v\n", *p)
    fmt.Printf("type: %+v\n", reflect.TypeOf(val))
}

func main() {
    dumpMemory(123456)
}
$ go run main.go
{field1:4863776 field2:5102552}
type: int

残念ながら123456は出てきませんでした。データが入っていないときはポインタであると考えます。そこで*[4]uint型にしてポインタ先のメモリを4個分づつダンプします。

type memory struct {
    field1 *[4]uint
    field2 *[4]uint
}

func dumpMemory(val interface{}) {
    p := (*memory)(unsafe.Pointer(&val))
    fmt.Printf("field1: %+v, field2: %+v\n", *p.field1, *p.field2)
    fmt.Printf("type: %+v\n", reflect.TypeOf(val))
}
$ go run main.go
field1: [8 0 9369747855051551226 5672240], field2: [123456 536870951 1 4376800]
type: int

field2の先頭に期待していた数値がでましたのでfield2が実データのポインタでしょう。valに入れるデータを変えてもう少し確かめます。 main()のところで dumpMemory([2]uint{123456, 7890})としてみます。

$ go run main.go
field1: [16 0 10450611741568835945 5672256], field2: [123456 7890 0 0]
type: [2]uint

field2は期待通りでした。field1の先頭は8から16になっているのでデータバイト数ではないかと推測できます。

以前、投稿した記事Go言語 構造体をfmt.Printfで表示するときの書式の不思議reflect.TypeOf%#vで表示したときの構造かも知れないので、reflect.TypeOf%#v%fとしfield1%#vにします。

func dumpMemory(val interface{}) {
    p := (*memory)(unsafe.Pointer(&val))
    fmt.Printf("field1: %#v, field2: %+v\n", *p.field1, *p.field2)
    fmt.Printf("type: %#v\n    : %f\n", reflect.TypeOf(val), reflect.TypeOf(val))
}
$ go run main.go
field1: [4]uint{0x10, 0x0, 0x910808025dbca169, 0x568d40}, field2: [123456 7890 0 0]
type: &reflect.rtype{size:0x10, ptrdata:0x0, hash:0x5dbca169, tflag:0x2, align:0x8, fieldAlign:0x8, kind:0x91, alg:(*reflect.typeAlg)(0x568d40), gcdata:(*uint8)(0x4dcb48), str:8166, ptrToThis:0}
    : &{%!f(uintptr=16) %!f(uintptr=0) %!f(uint32=1572643177) %!f(reflect.tflag=2) %!f(uint8=8) %!f(uint8=8) %!f(uint8=145) %!f(*reflect.typeAlg=&{0x402720 0x402f20}) %!f(*uint8=0x4dcb48) %!f(reflect.nameOff=8166) %!f(reflect.typeOff=0)}

field1typeを較べてみると4個中3個(size,ptrdata,alg)が一致しています。そこでfield1typeであると考えれらます。またfield10x910808025dbca169typeでは5個のフィールドになっています。64ビットを5個に分けるには16,16,16,8,8または32,8,8,8,8が考えられる。hashuint32、必然的に32,8,8,8,8の構成になる。algが二つの項目を持っていて%fで16進表示されているのでポインタと考えられます。また%#vのフォーマットでstrptrToThis%#vで10進表示になっているのは符号付きだからだと思います。rtypeは直接使用できないので、自分で11個の項目myTypeを定義してみます。

main.go
package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

type memory struct {
    field1 *myType
    field2 *[4]uint
}

type myType struct {
    size       uintptr
    ptrdata    uintptr
    hash       uint32
    tflag      uint8
    align      uint8
    fieldAlign uint8
    kind       uint8
    alg        *[2]*uint
    gcdata     *uint8
    str        int
    ptrToThis  int
}

func dumpMemory(val interface{}) {
    p := (*memory)(unsafe.Pointer(&val))
    fmt.Printf("field1: %#v\n     : %f , field2: %+v\n", *p.field1, *p.field1, *p.field2)
    fmt.Printf("type: %#v\n    : %f\n", reflect.TypeOf(val), reflect.TypeOf(val))
}

func main() {
    dumpMemory([2]uint{123456, 67890})
}
$ go run main.go
field1: main.myType{size:0x10, ptrdata:0x0, hash:0x5dbca169, tflag:0x2, align:0x8, fieldAlign:0x8, kind:0x91, alg:(*[2]*uint)(0x569d40), gcdata:(*uint8)(0x4ddeb8), str:8190, ptrToThis:4870240}
     : {%!f(uintptr=16) %!f(uintptr=0) %!f(uint32=1572643177) %!f(uint8=2) %!f(uint8=8) %!f(uint8=8) %!f(uint8=145) %!f(*[2]*uint=&[0x4cf668 0x4cf638]) %!f(*uint8=0x4ddeb8) %!f(int=8190) %!f(int=4870240)} , field2: [123456 67890 0 0]
type: &reflect.rtype{size:0x10, ptrdata:0x0, hash:0x5dbca169, tflag:0x2, align:0x8, fieldAlign:0x8, kind:0x91, alg:(*reflect.typeAlg)(0x569d40), gcdata:(*uint8)(0x4ddeb8), str:8190, ptrToThis:0}
    : &{%!f(uintptr=16) %!f(uintptr=0) %!f(uint32=1572643177) %!f(reflect.tflag=2) %!f(uint8=8) %!f(uint8=8) %!f(uint8=145) %!f(*reflect.typeAlg=&{0x402720 0x402f20}) %!f(*uint8=0x4ddeb8) %!f(reflect.nameOff=8190) %!f(reflect.typeOff=0)}

algの二つのポインタが違っています。ポインタだと思うのでデータポインタでなく関数のポインタにしてみます。また、ptrToThisの値が違うのでstrptrToThisint32にしてみます。

type myType struct {
    size       uintptr
    ptrdata    uintptr
    hash       uint32
    tflag      uint8
    align      uint8
    fieldAlign uint8
    kind       uint8
    alg        *[2]func()
    gcdata     *uint8
    str        int32
    ptrToThis  int32
}
$ go run main.go
field1: main.myType{size:0x10, ptrdata:0x0, hash:0x5dbca169, tflag:0x2, align:0x8, fieldAlign:0x8, kind:0x91, alg:(*[2]func())(0x569d40), gcdata:(*uint8)(0x4ddeb8), str:8190, ptrToThis:0}
     : {%!f(uintptr=16) %!f(uintptr=0) %!f(uint32=1572643177) %!f(uint8=2) %!f(uint8=8) %!f(uint8=8) %!f(uint8=145) %!f(*[2]func()=&[0x402720 0x402f20]) %!f(*uint8=0x4ddeb8) %!f(int32=8190) %!f(int32=0)} , field2: [123456 67890 0 0]
type: &reflect.rtype{size:0x10, ptrdata:0x0, hash:0x5dbca169, tflag:0x2, align:0x8, fieldAlign:0x8, kind:0x91, alg:(*reflect.typeAlg)(0x569d40), gcdata:(*uint8)(0x4ddeb8), str:8190, ptrToThis:0}
    : &{%!f(uintptr=16) %!f(uintptr=0) %!f(uint32=1572643177) %!f(reflect.tflag=2) %!f(uint8=8) %!f(uint8=8) %!f(uint8=145) %!f(*reflect.typeAlg=&{0x402720 0x402f20}) %!f(*uint8=0x4ddeb8) %!f(reflect.nameOff=8190) %!f(reflect.typeOff=0)}

ここで完全に一致しました。algが関数のポインタとは分かりましたが、関数の引数や戻り値の型などまでは分かりません。私がプログラミングで調べられるのはここまでです。
結論としてfield1Typeの構造体のポインタでTypeの先頭項目はデータのバイト数です。field2は実データがある領域のポインタです。最後のほうはreflect.rtypeの構造の調査になってしまいました。

相対アドレス メモリの内容
0~7バイト目 データのタイプ情報(reflect.rtype)のポインタ
8~15バイト目 実データが格納されているポインタ

Appendix

Goソースを調べてみた

string とスライス

スライスとstringの内部構造についてGo言語のソースを探してみましたら、それらしいのが見つかりました。ソース名はreflect/value.goです。

reflect/value.go
// StringHeader is the runtime representation of a string.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type StringHeader struct {
    Data uintptr
    Len  int
}

// stringHeader is a safe version of StringHeader used within this package.
type stringHeader struct {
    Data unsafe.Pointer
    Len  int
}

// SliceHeader is the runtime representation of a slice.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

// sliceHeader is a safe version of SliceHeader used within this package.
type sliceHeader struct {
    Data unsafe.Pointer
    Len  int
    Cap  int
}

interface{}

Go言語のソースからreflect/value.goreflect/type.goに下記のような構造体を見つけました。
memoryemptyInterfaceに対応して、 myTypertypeに対応しているようです。

reflect/value.go
// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
    typ  *rtype
    word unsafe.Pointer
}
reflect/type.go
// rtype is the common implementation of most values.
// It is embedded in other struct types.
//
// rtype must be kept in sync with ../runtime/type.go:/^type._type.
type rtype struct {
    size       uintptr
    ptrdata    uintptr  // number of bytes in the type that can contain pointers
    hash       uint32   // hash of type; avoids computation in hash tables
    tflag      tflag    // extra type information flags
    align      uint8    // alignment of variable with this type
    fieldAlign uint8    // alignment of struct field with this type
    kind       uint8    // enumeration for C
    alg        *typeAlg // algorithm table
    gcdata     *byte    // garbage collection data
    str        nameOff  // string form
    ptrToThis  typeOff  // type for pointer to this type, may be zero
}

tflaguint8で、nameOfftypeOffint32です。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1