Goの関数内で引数の値を書き換えた時の影響

はじめに

関数の引数の値を書き換えた時、呼び出し元で渡した変数の値にも影響がある場合があります。どのような場合に影響があるのか​​、定期的に忘れるため書き残しておきました。

調査

以下の構造体で試してみます。

type TestStruct struct {
	Value        User
	Pointer      *User
	ValueSlice   ValueUsers
	PointerSlice PointerUsers
}

type User struct {
	ID int
}

type ValueUsers []User

type PointerUsers []*User

関数の引数に渡したとき、各フィールドのポイントインタがどうなるかを確認します。

package main

import (
	"fmt"
	"os"
	"strings"

	"github.com/olekukonko/tablewriter"
)

func main() {
	a := TestStruct{
		Value: User{
			ID: 1,
		},
		Pointer: &User{
			ID: 2,
		},
		ValueSlice:   append(make(ValueUsers, 0, 2), User{ID: 3}),
		PointerSlice: append(make(PointerUsers, 0, 2), &User{ID: 4}),
	}

	table := tablewriter.NewWriter(os.Stdout)
	table.SetAutoFormatHeaders(false)
	table.SetHeader([]string{"", "&a", "&a.Value", "a.Pointer", "&a.Pointer", "a.ValueSlice", "&a.ValueSlice", "a.PointerSlice", "&a.PointerSlice"})

	table.Append(strings.Split(fmt.Sprintf("main %p %p %p %p %p %p %p %p",
		&a, &a.Value, a.Pointer, &a.Pointer, a.ValueSlice, &a.ValueSlice, a.PointerSlice, &a.PointerSlice), " "))

	ValueArg(a, table)
    
	PointerArg(&a, table)
    
	table.Render()
}

func ValueArg(a TestStruct, table *tablewriter.Table) {
	table.Append(strings.Split(fmt.Sprintf("ValueArg %p %p %p %p %p %p %p %p",
		&a, &a.Value, a.Pointer, &a.Pointer, a.ValueSlice, &a.ValueSlice, a.PointerSlice, &a.PointerSlice), " "))
}

func PointerArg(a *TestStruct, table *tablewriter.Table) {
	table.Append(strings.Split(fmt.Sprintf("PointerArg %p %p %p %p %p %p %p %p",
		a, &a.Value, a.Pointer, &a.Pointer, a.ValueSlice, &a.ValueSlice, a.PointerSlice, &a.PointerSlice), " "))
}

結果は以下です。

+------------+--------------+--------------+--------------+--------------+--------------+---------------+----------------+-----------------+
|            |      &a      |   &a.Value   |  a.Pointer   |  &a.Pointer  | a.ValueSlice | &a.ValueSlice | a.PointerSlice | &a.PointerSlice |
+------------+--------------+--------------+--------------+--------------+--------------+---------------+----------------+-----------------+
| main       | 0xc0000aa240 | 0xc0000aa240 | 0xc0000ac1e0 | 0xc0000aa248 | 0xc0000ac1d0 | 0xc0000aa250  | 0xc000096220   | 0xc0000aa268    |
| ValueArg   | 0xc0000aa280 | 0xc0000aa280 | 0xc0000ac1e0 | 0xc0000aa288 | 0xc0000ac1d0 | 0xc0000aa290  | 0xc000096220   | 0xc0000aa2a8    |
| PointerArg | 0xc0000aa240 | 0xc0000aa240 | 0xc0000ac1e0 | 0xc0000aa248 | 0xc0000ac1d0 | 0xc0000aa250  | 0xc000096220   | 0xc0000aa268    |
+------------+--------------+--------------+--------------+--------------+--------------+---------------+----------------+-----------------+

実体として渡した場合(`ValueArg`)、内部的には値をコピーします。そのため、構造体特有(`&a`)や各フィールド(`&Value`, `&Pointer`, `&ValueSlice`, `&PointerSlice`内部的に参照をもつフィールド(`Pointer`, `ValueSlice`, `PointerSlice`)の場合、その参照先は同じアドレスを向きます。

ポインタとして渡した場合(`PointerArg`)、内部的には渡したポインタの値をコピーします。そのため、すべての項目が `main` での実行結果と同じになります。

関数の中引数の構造フィーノルを書き換えた時、元の値にどのような影響があるかを確認します。

構造のフィールドの値を書き換える

func ValueArgUpdateStructField(a TestStruct) {
	a.Value.ID = a.Value.ID + 10
	a.Pointer.ID = a.Pointer.ID + 10
}

func PointerArgUpdateStructField(a *TestStruct) {
	a.Value.ID = a.Value.ID + 10
	a.Pointer.ID = a.Pointer.ID + 10
}

	a := TestStruct{
		Value: User{
			ID: 1,
		},
		Pointer: &User{
			ID: 2,
		},
	}

	table := tablewriter.NewWriter(os.Stdout)
	table.SetAutoFormatHeaders(false)
	table.SetHeader([]string{"", "a.Value", "a.Pointer"})
	table.Append(strings.Split(fmt.Sprintf("main %#v %#v",
		a.Value, a.Pointer), " "))

	ValueArgUpdateStructField(a)
	table.Append(strings.Split(fmt.Sprintf("ValueArgUpdateStructField %#v %#v",
		a.Value, a.Pointer), " "))

	a = TestStruct{
		Value: User{
			ID: 1,
		},
		Pointer: &User{
			ID: 2,
		},
	}
	PointerArgUpdateStructField(&a)
	table.Append(strings.Split(fmt.Sprintf("PointerArgUpdateStructField %#v %#v",
		a.Value, a.Pointer), " "))
	table.Render()

結果は以下です。

+------------------+------------------+-------------------+
|                  |     a.Value      |     a.Pointer     |
+------------------+------------------+-------------------+
| main             | main.User{ID:1}  | &main.User{ID:2}  |
| ValueArgUpdate   | main.User{ID:1}  | &main.User{ID:12} |
| PointerArgUpdate | main.User{ID:11} | &main.User{ID:12} |
+------------------+------------------+-------------------+

実体を渡す場合、ポインタのフィールドは書き換えられますが実体のフィールドは書き換えられません。ポインタを渡す場合は両方とも書き換えられます。

スライスのフィールドの値を書き換える

func ValueArgUpdateSliceField(a TestStruct) {
	a.ValueSlice[0].ID = a.ValueSlice[0].ID + 10
	a.PointerSlice[0].ID = a.PointerSlice[0].ID + 10
}

func PointerArgUpdateSliceField(a *TestStruct) {
	a.ValueSlice[0].ID = a.ValueSlice[0].ID + 10
	a.PointerSlice[0].ID = a.PointerSlice[0].ID + 10
}
	a := TestStruct{
		ValueSlice:   append(make(ValueUsers, 0, 2), User{ID: 3}),
		PointerSlice: append(make(PointerUsers, 0, 2), &User{ID: 4}),
	}

	table := tablewriter.NewWriter(os.Stdout)
	table.SetAutoFormatHeaders(false)
	table.SetHeader([]string{"", "a.ValueSlice[0]", "a.PointerSlice[0]"})
	table.Append(strings.Split(fmt.Sprintf("main %#v %#v",
		a.ValueSlice[0], a.PointerSlice[0]), " "))

	ValueArgUpdateSliceField(a)
	table.Append(strings.Split(fmt.Sprintf("ValueArgUpdateStructField %#v %#v",
		a.ValueSlice[0], a.PointerSlice[0]), " "))

	a = TestStruct{
		ValueSlice:   append(make(ValueUsers, 0, 2), User{ID: 3}),
		PointerSlice: append(make(PointerUsers, 0, 2), &User{ID: 4}),
	}
	PointerArgUpdateSliceField(&a)
	table.Append(strings.Split(fmt.Sprintf("PointerArgUpdateStructField %#v %#v",
		a.ValueSlice[0], a.PointerSlice[0]), " "))
	table.Render()

結果は以下です。

+-----------------------------+------------------+-------------------+
|                             | a.ValueSlice[0]  | a.PointerSlice[0] |
+-----------------------------+------------------+-------------------+
| main                        | main.User{ID:3}  | &main.User{ID:4}  |
| ValueArgUpdateStructField   | main.User{ID:13} | &main.User{ID:14} |
| PointerArgUpdateStructField | main.User{ID:13} | &main.User{ID:14} |
+-----------------------------+------------------+-------------------+

実体でもポインタでも、呼び出し元の値が書き換えられます。実体で渡しても書き換えられるのは、スライス(`a.ValueSlice` や `a.PointerSlice`)が参照している配列の値を書き換えているからです。

スライスのフィールドに追加する

func ValueArgUpdateSliceAppend(a TestStruct) {
	a.ValueSlice = append(a.ValueSlice, User{ID: 5})
	a.PointerSlice = append(a.PointerSlice, &User{ID: 5})
}

func PointerArgUpdateSliceAppend(a *TestStruct) {
	a.ValueSlice = append(a.ValueSlice, User{ID: 5})
	a.PointerSlice = append(a.PointerSlice, &User{ID: 5})
}
	a = TestStruct{
		ValueSlice:   append(make(ValueUsers, 0, 2), User{ID: 3}),
		PointerSlice: append(make(PointerUsers, 0, 2), &User{ID: 4}),
	}

	table = tablewriter.NewWriter(os.Stdout)
	table.SetAutoFormatHeaders(false)
	table.SetHeader([]string{"", "len(a.ValueSlice)", "len(a.PointerSlice)"})
	table.Append(strings.Split(fmt.Sprintf("main %#v %#v",
		len(a.ValueSlice), len(a.PointerSlice)), " "))

	ValueArgUpdateSliceAppend(a)
	table.Append(strings.Split(fmt.Sprintf("ValueArgUpdateStructAppend %#v %#v",
		len(a.ValueSlice), len(a.PointerSlice)), " "))

	a = TestStruct{
		ValueSlice:   append(make(ValueUsers, 0, 2), User{ID: 3}),
		PointerSlice: append(make(PointerUsers, 0, 2), &User{ID: 4}),
	}
	PointerArgUpdateSliceAppend(&a)
	table.Append(strings.Split(fmt.Sprintf("PointerArgUpdateStructAppend %#v %#v",
		len(a.ValueSlice), len(a.PointerSlice)), " "))
	table.Render()

結果は以下です。

+------------------------------+-------------------+---------------------+
|                              | len(a.ValueSlice) | len(a.PointerSlice) |
+------------------------------+-------------------+---------------------+
| main                         |                 1 |                   1 |
| ValueArgUpdateStructAppend   |                 1 |                   1 |
| PointerArgUpdateStructAppend |                 2 |                   2 |
+------------------------------+-------------------+---------------------+

実体を渡す場合は呼び出し元に影響はありませんが、ポインタを渡す場合は呼び出し元にも反映されます。

実体を渡す場合に関して細かい話をすると、スライスは内部的には配列への参照、長さ、容量を持っていて、`a.ValueSlice` が参照している配列の要素は更新されていますが、 `a.ValueSlice` が内部的に持っている長さの値が更新されていないので呼び出し元からは参照できないことが、という状態です。ます。

	ValueArgUpdateSliceAppend(a)
	table.Append(strings.Split(fmt.Sprintf("ValueArgUpdateStructAppend %#v %#v",
		len(a.ValueSlice), len(a.PointerSlice)), " "))
	fmt.Println(a.ValueSlice[0:2]) // 追加
	// main.ValueUsers{main.User{ID:3}, main.User{ID:5}}
    

スライスのフィールドから値を取り出して書き換える

func ValueArgUpdateSliceValue(a TestStruct) {
	v := a.ValueSlice[0]
	v.ID = v.ID + 10
	p := a.PointerSlice[0]
	p.ID = p.ID + 10
}

func PointerArgUpdateSliceValue(a *TestStruct) {
	v := a.ValueSlice[0]
	v.ID = v.ID + 10
	p := a.PointerSlice[0]
	p.ID = p.ID + 10
}
	a = TestStruct{
		ValueSlice:   append(make(ValueUsers, 0, 2), User{ID: 3}),
		PointerSlice: append(make(PointerUsers, 0, 2), &User{ID: 4}),
	}

	table = tablewriter.NewWriter(os.Stdout)
	table.SetAutoFormatHeaders(false)
	table.SetHeader([]string{"", "a.ValueSlice[0]", "a.PointerSlice[0]"})
	table.Append(strings.Split(fmt.Sprintf("main %#v %#v",
		a.ValueSlice[0], a.PointerSlice[0]), " "))

	ValueArgUpdateSliceValue(a)
	table.Append(strings.Split(fmt.Sprintf("ValueArgUpdateSliceValue %#v %#v",
		a.ValueSlice[0], a.PointerSlice[0]), " "))

	a = TestStruct{
		ValueSlice:   append(make(ValueUsers, 0, 2), User{ID: 3}),
		PointerSlice: append(make(PointerUsers, 0, 2), &User{ID: 4}),
	}
	PointerArgUpdateSliceValue(&a)
	table.Append(strings.Split(fmt.Sprintf("PointerArgUpdateSliceValue %#v %#v",
		a.ValueSlice[0], a.PointerSlice[0]), " "))
	table.Render()

結果は以下です。

+----------------------------+-----------------+-------------------+
|                            | a.ValueSlice[0] | a.PointerSlice[0] |
+----------------------------+-----------------+-------------------+
| main                       | main.User{ID:3} | &main.User{ID:4}  |
| ValueArgUpdateSliceValue   | main.User{ID:3} | &main.User{ID:14} |
| PointerArgUpdateSliceValue | main.User{ID:3} | &main.User{ID:14} |
+----------------------------+-----------------+-------------------+

vやpに代入したタイミングでその値がコピーされるため、要素が実体の場合は元のスライスには影響がなく、ポインタの場合は元のスライスにも反映されます。

まとめ

いろいろ書きましたが、結論としては以下のことを覚えていれば理解できると思います。

  • 関数の引数に渡すと値がコピーされる
  • 構造体の実体を渡すと実体がコピーされるので、各フィールドがコピーされる
  • 構造体のポインタを渡すとポインタの値がコピーされるので、同じポインタを指す
  • スライスは「配列への参照、長さ、容量をもつ構造体」と考えれば、構造体のコピーと同じ結果になる

ちなみにメソッドは「レシーバーを引数に持つ関数」と同じなので同じ考え方ができます。