Go 構造体はポインター型にするべき？

この記事はエイチーム引越し侍/エイチームコネクトの社員による、[Ateam Hikkoshi samurai Inc.× Ateam Connect Inc. Advent Calendar 2021](https://qiita.com/advent-calendar/2021/hikkoshizamurai-connect) 12日目の記事です。
本日は**1歳児の育児でちょくちょく離席が多いパパエンジニア**@lostfindが担当します。

:::note info
誤った内容や日本語の指摘は大歓迎です。とても助かります。
:::

## はじめに😀
11月に**「引越し侍　ネット見積もりサービス」**をリリースしました。
こちらのBackend ビジネスロジックは`Go`による`Clean Architecture`の概念をベースで開発しました。

( 当サービスの詳細な技術の構成については[1日目の記事](https://qiita.com/sho-hata/items/054c77392cc61bc2f3ac)と、[2日目の記事](https://qiita.com/anneau/items/3936c954690a28c51c8f)をご覧ください )

`Clean Architecture`の特性上、構造体(モデル)が複数の層を渡って処理されるパターンが多いです。
その時に、引数としての構造体とそのスライスの型はほとんど構造体のポインタ型で扱っています。

ポインタ型だから値コピーされないので、早い、メモリ効率が良いと思い込んでいましたが、
「それ、本当か?」と思いましたので、軽く浅く検証してみました。

## Benchmark⏱
:::note info
go version 1.17.4
:::

### 検証の準備
```go
const sliceLen = 25
const maxNestedLevel = 4

type TestStr struct {
	ID     int
	Name   string
	Groups []string
	Steps  []string
	Child  *SubStr
}

type SubStr struct {
	ID      int
	SubName string
}

func NewTestStr() TestStr {
	rand.Seed(time.Now().UnixNano())
	id := rand.Intn(10000)
	name := fmt.Sprintf("TesterValueName_%d", id)
	child := SubStr{
		ID:      id + 1000,
		SubName: name,
	}

	return TestStr{
		ID:     id,
		Name:   name,
		Groups: []string{name, name, name, name, name, name, name},
		Steps:  []string{"init"},
	}
}
```

### 構造体のスライス🥒
リポジトリからデータを取得する時はだいたい構造体のスライスを返しているので、
その結果を持って各層で何かをする想定の検証用コードです。

```go
func RecursionPointerSlice(input []*TestStr, lv int) []*TestStr {
	for i := range input {
		input[i].Steps = append(input[i].Steps, strconv.Itoa(lv))
	}

	nextLevel := lv + 1
	if nextLevel > maxNestedLevel {
		return input
	}

	return RecursionPointerSlice(input, nextLevel)
}

func RecursionValueSlice(input []TestStr, lv int) []TestStr {
	// 中身は上の関数と同じ
	... 
}
```

```go
func Benchmark_Pointer_NestedFunc_PointerSlice(b *testing.B) {
	input := PointerSlice()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		RecursionPointerSlice(input, 1)
	}
}

func Benchmark_Value_NestedFunc_ValueSlice(b *testing.B) {
	...
}
```

#### 結果
ベンチマークの結果は構造体のポインタではない方早かったです。差は10%程度。
ただ、sliceの内部ではポインタになっているので、何回ベンチマークして見たら両方のパフォーマンスの差はないようでした。

```
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz

Benchmark_Pointer_NestedFunc_PointerSlice-4       160000              7877 ns/op            8831 B/op          0 allocs/op
Benchmark_Value_NestedFunc_ValueSlice-4           160000              6938 ns/op            8831 B/op          0 allocs/op
```

### 構造体の単体🍅
構造体の単体で扱う時はどれぐらい差があるか確認してみました。
引数で受け取った構造体の中身でなにかをすることを想定して`JustReturn()`という関数を呼び出しています。

```go
func RecursionPointer(input *TestStr, lv int) *TestStr {
	nextLevel := lv + 1
	JustReturn(input.Name)
	if nextLevel > maxNestedLevel {
		return input
	}

	return RecursionPointer(input, nextLevel)
}

func RecursionValue(input TestStr, lv int) TestStr {
	...
}

//go:noinline
func JustReturn(name string) string {
	return name
}
```
:::note warn
JustReturn()関数はinline展開されて何も影響しなくなるので go:noinline というpragmaをつけます
:::

> [cmd/compile go:noinline](https://pkg.go.dev/cmd/compile#:~:text=be%20avoided%20otherwise.-,//go%3Anoinline,-The%20//go%3Anoinline%20directive)


#### 結果
ポインタ型のほうが4倍ぐらい早い結果となりました。
テスト対象である構造体を大きくしたり、再帰をもっと深くすると、差はもっと広がりポインタ型の方が10倍ぐらい早い結果となりました。

```
Benchmark_Pointer_NestedFunc_Pointer-4          17000000                17.35 ns/op            0 B/op          0 allocs/op
Benchmark_Value_NestedFunc_Value-4              17000000                66.96 ns/op            0 B/op          0 allocs/op
```

## まとめ
:::note info
Slice、Mapのときはポインタ型にしなくても良い。
単体の構造体インスタンスを複数の関数やメソッドに引き渡すときはポインタ型が良い。
:::

:::note warn
Immutabilityが重要なところではポインタ型は扱いに注意するべき。
:::

これ以外にも、コードを色々変えながらベンチマークしてみましたが、もっと得られる情報はありませんでした。
構造体が小さい場合や、メソッド呼び出しのネストが深くない場合はポインタ型でなくても良いかと思います。

### すこしの心残り
goのbenchmarkだけで判断しているため、偏ったまとめになってしまいました。
garbage collectorの動き方によっては実際に動くアプリケーションでは異なる結果になるかもしれないなと思いました。

## 明日 😉
明日は@kekiさんの記事です！お楽しみに :loudspeaker: