1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ポインタってどういう時に使うべき?

Goにおけるポインタの使い所は難しいと思います. というのも,Goはスライスやマップなど値かポインタかが初見では分かりづらいようなデータ構造があります.直感的に分かりづらく,ポインタでなければうまく動かない処理があったりします.Goのポインタ周りは感覚でコードを書くと,意外なエラーや,予想外の挙動に直面します.

具体的には,以下のような状況において,私はポインタを使うかどうかよく迷います.

  • 関数の引数(配列,スライス,マップ)は,ポインタと値どちらが適切か?
  • 構造体をマップに格納する時,よくポインタが格納されているが値ではダメなのか?
  • メソッドのレシーバ引数は値とポインタどちらが良いのか?
  • Rangeループは配列やスライスの要素を書き換えられるか?

今回の記事では,Goのポインタについて,宣言や値のポインタ化などの基礎構文から,ポインタの使い所を具体例を交えつつ,解説していきたいと思います.

ポインタの基礎構文

宣言

cなどは変数名の前に*をつけますが,Goでは型の前に*をつけてポインタを定義します.

// ポインタの宣言
var a *int

デリファレンス (dereference), アドレス(address)

デリファレンスとは「逆参照」という意味を持つ英単語です.ポインタからポインタが指す値を取り出すとき,値から値を指すポインタを取り出すとき,両方ともデリファレンスと言います.また,値から値を指すポインタを取り出すとき,アドレスするとも言います.

具体的な記法は次のようなものです.値からポインタが指す値を取り出すときは,値の前に&をつけます.ポインタから値を取り出すときは,ポインタの前に*をつけます.

var a *int

// 値からポインタをデリファレンス (アドレス)
var b int = 10
a = &b 

// ポインタから値をデリファレンス
fmt.Println(*a) // 10

使い所,使わなくていい所

ここからは個人的にポインタを使うかどうか迷うケース4パターンを解説していきます.
Goを書く上ではどのパターンも重要で,全て抑えておいた方が良いと個人的には思います.
では一つ一つ見ていきましょう.

(1) 配列・スライス・マップを関数引数にするケース

最初に紹介するものは最も頻出の配列やスライス,マップを関数に渡すケースです.
配列を渡した場合,スライスを渡した場合,マップを渡した場合の三つのケースでそれぞれポインタの使い方を見ていきましょう.

配列

まず,配列を関数に渡したケースです.このケースはポインタを渡さない限り,値渡しとして認識されます.c言語では,配列を関数に渡すと自動で参照渡しとなるが,Goは違います.ここがややこしいポイントです.

したがって,関数の中で配列の値を書き換えたい場合はポインタを明示的に渡す必要があります.以下にコード例を書いておきます.

func main() {
	var a [3]int = [3]int{1, 2, 3}
	fmt.Println(a) // 1,2,3
 
	changeByValue(a)
	fmt.Println(a) // 1,2,3
 
	changeByReference(&a)
	fmt.Println(a) // 2,3,4
}

func changeByValue(a [3]int) [3]int { // 値渡し
	for i := 0; i < 3; i++ {
		a[i]++
	}
	return a
}

func changeByReference(a *[3]int) *[3]int { // 参照渡し
	for i := 0; i < 3; i++ {
		a[i]++
	}
	return a
}

スライス

次にスライスを関数に渡したケースです.このケースは自動でスライス構造体の値渡し(注意!) となります.スライス構造体とは次に示すような,Data(配列へのポインタ),Len(スライスの長さ),Cap(スライスの容量,最大長)で構成されるものです.つまり,スライスとは内部に配列を持ち,自動で配列長を伸ばせるようにした構造体です.したがって,フィールドとして配列のポインタが渡されているので,基本的には参照渡しと同じように,関数の中でスライスの値が書き換えられると,元のスライスの値も書き変えられます.

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}
// (go doc reflect.SliceHeader より引用)

ここで,前述した注意とは関数の内部でスライスにappendする場合は長さに注意が必要 ということです.スライスが関数に渡された時,構造体内部の配列はポインタが渡されているので,参照渡しになります.しかし,スライス容量は値渡しされています.したがって,関数内部でappendして元の容量以上に配列を拡大しても,元のスライス容量を超えた分の値は参照できません.以下に具体例を示します.

func main() {
	a := []int{1, 2, 3, 3} // aはlen = 4, cap = 4
	fmt.Println("capacity:", cap(a), "len:", len(a), a)
	changeValue(a)
	fmt.Println(a)
	appendValue(a)
	fmt.Println(a)
}

func changeValue(a []int) {
	a[3] = 4
	fmt.Println("changeValue:", a)
}

func appendValue(a []int) { // 配列に要素を追加
	a = append(a, 5)
	fmt.Println("appendValue:", a)
}

出力

capacity: 4 len: 4 [1 2 3 3]
changeValue: [1 2 3 4]
[1 2 3 4]
appendValue: [1 2 3 4 5]
[1 2 3 4]

結論として,基本的にスライスを参照渡しする必要はありません.関数にスライスを渡した際,スライスの要素を書き換えると元のスライスの要素も書き換えられます.

しかし,関数の中でスライスがappendされる時は,以下の二点のどちらかが必要となってきます.

  • スライスに十分な容量を保証する
  • スライスのポインタを渡す.

参考- GoのSliceの落とし穴

マップ

最後にマップを関数に渡すケースです.このケースは擬似的な参照渡しとなります.理由は,マップ自体が変数へのポインタを格納したような構造だからです.したがって,関数の中でマップの値が書き換えられると,元のマップの値も書き変えられます.したがって,マップに関してはポインタを渡す理由は特にありません.以下がコード例です.

func main() {
	m := make(map[string]int)
	m["key"] = 100
	fmt.Println(m)
	changeMapValue(m, "key")
	fmt.Println(m)
}
func changeMapValue(m map[string]int, key string) {
	m[key] = 0
}

出力

map[key:100]
map[key:0]

(2) マップのvalueに構造体を格納するケース

次に紹介するものは,マップのvalueに構造体を格納するケースです.このケースは,感覚的には構造体の値を格納しても,ポインタを格納しても,どちらも大差ないように思います.

しかし,マップに構造体の値を格納した場合には,構造体のフィールドを書き換えることができません(UnaddressableFieldAssign).これはmap[key]で得られる値がaddressable(アドレス可能,本記事参照)ではないことが理由です.構造体のフィールドを書き換える操作は,構造体のポインタへアクセス→ポインタからフィールドを取り出す→代入という動作です.この「構造体のポインタにアクセス」という部分が実行できず,エラーが出ています.

map[key]のポインタにアクセスできない理由は,Hashmapの設計上,値を大量に格納していくとmap[key]で得られるデータを格納するアドレスが変わるためです.そのため,Goでは,map[key]で得られる値のポインタはアクセスできないようになっています.

参考- Hash table -Wikipedia, Dynamic Resizing

したがって,フィールドの書き換えをする場合は,マップには構造体のポインタを格納しなければなりません.

type MyInt struct {
	x int
}

func main() {
	m := make(map[string]MyInt)
	m["key"] = MyInt{
		x: 100,
	} // 代入はOK
	m["key"].x = 100 // cannot assign to struct field m["key"].x in map, compiler(UnaddressableFieldAssign)
}

このフィールドの書き換えが難しくなってしまうことから,マップに構造体を格納する時は,ポインタの形で格納した方が柔軟に扱うことができるでしょう.ちなみに,値が格納されてしまっている場合は,特定のフィールドを書き換えるのではなく,別の構造体をマップに格納することでも,書き換えと同じ動作はできます.

したがって,私は特に制限がなければ,mapのvalueに構造体を格納するときはポインタを持つようにしています.値を書き換えれないことにより,行数が増えてしまったり,柔軟性がなくなる事を避けるためです.

(3) レシーバ引数

次に紹介するものは,構造体のレシーバ引数です.goの機能で,型の値またはポインタのどちらかにメソッドを定義できます.そのメソッドを定義する際,関数名の前に記載する引数をレシーバ引数と呼びます.この引数もポインタと値のどちらにするか迷います.以下に値レシーバの例と,ポインタレシーバの例を示します.

レシーバ引数に関しても関数に値渡しする場合と参照渡しする場合と同様の差が現れます.つまり,値レシーバの場合は,値がコピーされてメソッドに渡されます.そして,ポインタレシーバの場合は値のコピーがありません.つまり,レシーバ引数に関しては以下のことが言えます.

  • 構造体のサイズが大きい場合は,ポインタレシーバを使う
  • 構造体の値を書き換えたい場合は,ポインタレシーバを使う

したがって,私は基本的に構造体のレシーバ引数はポインタレシーバにするようにしています.

最後に,値レシーバとポインタレシーバの使用例を示します.

type MyInt struct {
	x int
}

func (x MyInt) increment() { // 値レシーバ
    x++
}

func (x *MyInt) increment() { // ポインタレシーバ
    x++
}

func main() {
	var m MyInt
    p := &MyInt

    m.increment()
	fmt.Println(m.x) // 1
    p.increment()
    fmt.Println(p.increment(m.x)) // 2
}

(4)Rangeループ

最後に紹介するものは,Rangeループです.goの機能で,配列,スライス,マップの値をひとつずつ変数に格納し,ループを回すことができます.他の言語(PHP, C#, Java)でいうforeachに当たります.

Rangeループでは,配列,スライス,マップなどのループ対象の要素を書き換えたい場合,ポインタをループ対象に格納しておく必要があります.Rangeループはループ対象の要素を変数に格納して,その変数を使ってfor文内部の処理を要素ごとに繰り返します.したがって,ループで使われる変数に参照渡ししなければ,要素を書き換えることはできません.

したがって,配列,スライス,マップの要素を書き換える場合は以下のようになります.

  • ポインタを格納して,Rangeループで書き換える
  • 値を格納して,通常のforループで書き換える

最後にコード例を示します.

package main

import "fmt"

func main() {
    numbers := []int{1, 2, 3, 4, 5} // スライスの初期化

    // Rangeループで各要素を2倍にしようとする
    for _, value := range numbers {
        value = value * 2
    }
    
    fmt.Println(numbers) // [1, 2, 3, 4, 5] と出力され、変更されていない

    var ptrs []*int // ポインタを格納するための新しいスライスを作成
    for i := range numbers {
        ptrs = append(ptrs, &numbers[i])
    }

    // ポインタを通じて値を書き換える
    for _, ptr := range ptrs {
        *ptr = *ptr * 2
    }

    // 結果の表示
    fmt.Println(numbers) // [2, 4, 6, 8, 10] と出力される
}

これで本記事のGoにおけるポインタの使い所紹介は以上です.

おわりに

今回は,Goにおけるポインタの基礎構文からポインタの使い所までを具体例を交えて紹介しました.
業務でGoを書くときには,今回記事で紹介した内容を暗記できているとコーディングの際予想外の挙動に当たることが減るかと思います.

もし記事へのご意見などあれば,お待ちしております.
Goの使い方に慣れていない部分もあるので,ご意見いただければ嬉しいです.

以上です,ありがとうございました.
ノシ

1
3
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
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?