メインコンテンツまでスキップ

GOのポインタを利用しよう

Go言語では、ポインタを使用して変数のメモリアドレスを参照および操作することができます。 C言語やC++などの他の言語と同様に、効率的なメモリ管理やデータの共有に役立ちます。 つまり、データそのものではなく、そのデータが「どこにあるか」の住所を指し示す役割を果たします。

ポインタの基本

ポインタは、変数のメモリアドレスを格納するための特別な変数です。

ポインタ用語集

用語意味
x := 10データそのもの
アドレス&xデータの場所
ポインタ変数p := &xアドレスを持つ変数
デリファレンス*p = ...ポインタ先の値を操作

ポインタを使って変数の値を変更するには、デリファレンス(*)演算子を使用します。 デリファレンスdereferenceを使うと、ポインタが指し示すアドレスの値にアクセスできます。

package main
import "fmt"
func main() {
x := 10
p := &x

fmt.Println("Before:", x) // Before: 10

*p = 20 // ポインタを使ってxの値を変更

fmt.Println("After:", x) // After: 20
}

ポインタを出力してみましょう:

package main

import "fmt"

func main() {
x := 10 // 普通の変数
p := &x // & を付けると「xの住所」を取得

fmt.Println(x) // 10
fmt.Println(p) // 0x14000122018 みたいなアドレス
}

ポインタの操作

(&)演算子を使用して変数のアドレスを取得し、ポインタで値を変えるには、デリファレンスdereference演算子(*)を使用します。

package main

import "fmt"

func main() {
x := 10 // 普通の変数
p := &x // & で「xの住所」を取得し、ポインタ変数pに代入

*p = 20 // * で「住所先の中身」を触る

fmt.Println(x) // 20
}
記号意味
&xxの住所
*p住所にある値(中身を読む・書く)

ポインタがどこも指してない状態 = nil 家の住所が「不明」みたいな状態。

func PrintName(user *User) {
if user == nil {
fmt.Println("User is nil")
return
}
fmt.Println(user.Name)
}

ポインタと通常の変数の違い

通常の変数を別の変数に代入すると、その値がコピーされます。 一方、ポインタを別のポインタに代入すると、同じアドレスを指すことになります。

通常の変数を使った場合のポインタの値の変化を確認してみましょう:

func main() {
// 普通の変数として宣言
var value1 int = 10
var value2 int = value1

// アドレスを出力してみる
fmt.Println("---アドレスの出力---")
fmt.Println("value1:", &value1)
fmt.Println("value2:", &value2)

// まずは現時点の値を出力
fmt.Println("---初期状態の値---")
fmt.Println("value1:", value1)
fmt.Println("value2:", value2)

// valueの内容を変更
value1 = 20
fmt.Println("---valueの変更後---")
fmt.Println("value1:", value1)
fmt.Println("value2:", value2)

// pointerの内容を変更
value2 = 30
fmt.Println("---pointerの変更後---")
fmt.Println("value1:", value1)
fmt.Println("value2:", value2)
}

出力結果:

---アドレスの出力---
value1: 0x140000100d0
value2: 0x140000100d8
---初期状態の値---
value1: 10
value2: 10
---valueの変更後---
value1: 20
value2: 10
---pointerの変更後---
value1: 20
value2: 30

ポインタを使った場合の値の変化を確認してみましょう:

func main() {
// ポインタ変数として宣言
var value1 int = 10
var value2 *int = &value1

// アドレスを出力してみる
fmt.Println("---アドレスの出力---")
fmt.Println("value1:", &value1)
fmt.Println("value2:", value2)

// まずは現時点の値を出力
fmt.Println("---初期状態の値---")
fmt.Println("value1:", value1)
fmt.Println("value2:", *value2)

// valueの内容を変更
value1 = 20
fmt.Println("---valueの変更後---")
fmt.Println("value1:", value1)
fmt.Println("value2:", *value2)

// pointerの内容を変更
*value2 = 30
fmt.Println("---pointerの変更後---")
fmt.Println("value1:", value1)
fmt.Println("value2:", *value2)
}
出力結果:
---アドレスの出力---
value1: 0x14000108020
value2: 0x14000108020
---初期状態の値---
value1: 10
value2: 10
---valueの変更後---
value1: 20
value2: 20
---pointerの変更後---
value1: 30
value2: 30

ポインタを使う場面の例

構造体のフィールドがオプション(任意)である場合、ポインタを使うことがあります。 例えば、商品の割引があるかどうかが不明な場合などです。 nilを使って「割引なし」を表現できます。

type Product struct {
Name string
Discount *int // 割引があるかは不明なのでポインタ
}

func main() {
p := Product{Name: "Apple", Discount: nil}

if p.Discount != nil {
fmt.Println("Discount:", *p.Discount)
} else {
fmt.Println("No discount")
}
}

Goポインタのよくある用途

  • 関数で値を書き換える
  • 大きなデータをコピーせずに渡す(性能)
  • 構造体のメソッドで内部状態を変える

関数引数でポインタを使う

関数にポインタを渡すことで、呼び出し元の変数を直接変更できます。

方法説明動作
値渡しデータをコピーして渡す元の値は変わらない
ポインタ渡し“住所”を渡す元の値が変わる
package main

import "fmt"

// 値渡し
func updateValue(val int) {
val = 10
}

// ポインタ渡し
func updateValueByPointer(val *int) {
*val = 100
}

func main() {
x := 0
// 値渡し
updateValue(x)
fmt.Println(x) // 0
// &x でxのアドレスを渡す
// ポインタ以外の型を渡すとコンパイルエラーになる
updateValueByPointer(&x)
fmt.Println(x) // 100
}

値レシーバ vs ポインタレシーバ

Goの構造体メソッドには、値レシーバとポインタレシーバの2種類があります。

  • 値レシーバ: メソッドが呼び出されたときに、構造体のコピーが渡される。
  • ポインタレシーバ: メソッドが呼び出されたときに、構造体のアドレスが渡される。 ポインタレシーバを使用すると、メソッド内で構造体のフィールドを変更できます。
// 値レシーバ(コピー)
func (u User) Change() {
u.Name = "Taro" // コピーなので本物は変わらない
}
// ポインタレシーバ(アドレス)
func (u *User) ChangePointer() {
u.Name = "Taro" // ポインタなので本物が変わる
}

具体例:

type Counter struct {
N int
}

// 値レシーバ
func (c Counter) AddViaValueReceiver() {
c.N++
}

// ポインタレシーバ
func (c *Counter) AddViaPointerReceiver() {
c.N++
}

func main() {
cnt := Counter{N: 1}
cnt.AddViaValueReceiver()
fmt.Println(cnt.N) // 1 (変わらない)

cnt.AddViaPointerReceiver()
fmt.Println(cnt.N) // 2 (変わる)
}