👁

Goポインタ

この章では、Goのポインタについて学び、ハンズオン形式で学習します。これにより、&* の意味、関数やメソッドで呼び出し元の値を書き換える書き方の基本が身につきます。

1. 本章の概要

1.1 本章の目的

Goでは、変数の「アドレス」を指し示す ポインタ を扱うことができます。関数やメソッドの中で呼び出し元の値を直接書き換えたいときや、大きな構造体をコピーせずに扱いたいときに使います。前章で触れたメソッドの ポインタレシーバ も、この仕組みの上に成り立っています。

本章では、ポインタの基本(&*)、関数への活用(関数への渡し方・スライス・マップの扱い)、構造体での利用(アドレス取得・フィールドアクセスの省略記法・値レシーバとポインタレシーバ・相互呼び出し)までを扱います。

1.2 ハンズオンの流れ

小さな例で &* の動きを確認したあと、関数の引数として渡した場合の挙動(スライス・マップとの関係も含む)、そして構造体を対象にポインタを使う場面(アドレス取得・フィールドアクセスの省略記法・レシーバの使い分けと相互呼び出し)を順に見ていきます。

1.3 事前準備

必要なツール

この章では、以下のツールを使用します。まだインストールしていない場合は、リンク先の手順に沿って準備をお願いします。

ツール名 関連箇所 理由
Visual Studio Code Visual Studio Codeのインストール Goのコードを記述するエディタとして使用する
Go Goのインストール 本章のサンプルコードを実行する言語処理系として使用する

2. ポインタとは

まずはポインタが必要になる場面を確認し、続けて &* という2つの記号でポインタを扱う基本を見ていきます。

2.1 ポインタが必要な場面

「関数の中で受け取った値を書き換えて、呼び出し元にも変更を伝えたい」場面があります。Goでは関数に引数を渡すと値がコピーされて渡されるため、関数の中で書き換えても呼び出し元には影響しません。

次のコードは、関数 increment の中で受け取った値を1増やそうとする例です。

package main

import "fmt"

func increment(n int) {
    n++
}

func main() {
    x := 5
    increment(x)
    fmt.Println(x)
}

以下のような実行結果が表示されます。

5

increment を呼び出したのに x5 のままです。関数の中で n++ を実行しても、n は呼び出し元の x のコピーなので、書き換えても x には影響しません。

多くの一般的な用途ではこの値渡しの動きで問題ありません。ただし場合によっては、関数の中で書き換えた結果を呼び出し元にも反映させたい、というケースが出てきます。それを実現する仕組みが ポインタ です。変数の「アドレス」を渡すことで、その先にある元の値を直接書き換えられます。

📝 ポインタとは
変数がメモリ上のどこにあるかを指し示す値のことを ポインタ と呼びます。ポインタを通して、変数そのものにアクセスして値を読み書きできます。

2.2 アドレスとポインタ

ポインタを扱うには、&* の2つの記号を覚えます。書き方は以下のとおりです。

p := &変数名    // 変数のアドレスを取得し、p に代入する
*p             // p が指している変数の値を取り出す
*p = 値         // p が指している変数の値を書き換える
  • &変数名 は「その変数のアドレス」を取り出す書き方です。
  • *ポインタ は「そのポインタが指している変数の値」を取り出す(または代入する)書き方です。

実際に試してみましょう。次のコードは、x のアドレスを p に代入し、p を通して x の値を読み書きする例です。main.go を以下の内容に書き換えて保存し、go run main.go で実行します。

package main

import "fmt"

func main() {
    x := 5
    p := &x

    fmt.Println(p)   // xのアドレス
    fmt.Println(*p)  // xの値
    *p = 10
    fmt.Println(x)   // 書き換えられた x
}

以下のような実行結果が表示されます(アドレスは実行環境によって変わります)。

0xc000010090
5
10

p を出力するとアドレス、*p を出力すると x の値(5)、そのあと *p = 10x を書き換えたため、最後の fmt.Println(x)10 が出力されました。

コードを解説します。

p := &x

&x で変数 x のアドレスを取り出し、それを p に代入しています。p の型は *intint を指すポインタ)です。

fmt.Println(*p)

*p は「p が指している変数の値」を意味します。ここでは x の値である 5 が取り出されます。

*p = 10

*p = ... の形で、p が指している変数に値を代入できます。これにより、x そのものが 10 に書き換わります。

なお、var p *int のようにポインタを宣言しただけで初期化しなかった場合、ポインタは nil(まだどこも指していない状態)になります。nil の詳細(判定の書き方、他の型での挙動)は「Go関数」章の「## nil」節で扱いますが、ポインタでは nil*p で参照すると実行時エラーが発生してプログラム全体が停止するため、if p != nil { ... } で判定してから使うのが定石です。

3. 関数への活用

ポインタが実際に活きる代表的な場面の1つが、関数の引数として渡すときです。通常のポインタ引数の書き方と、内部にポインタを持つスライス・マップの動きを順に見ていきます。

3.1 関数への渡し方

冒頭で見たように、値渡しでは関数の中で書き換えても呼び出し元には反映されません。関数の引数をポインタ型で受け取ると、呼び出し元の変数のアドレスを渡す形になり、関数の中から元の値を書き換えられます。書き方は以下のとおりです。呼び出し側は &変数名 でアドレスを渡し、関数側は *型名 で受け取ります。

func 関数名(引数名 *型名) {
    *引数名 = 値    // 呼び出し元の変数を書き換える
}

func main() {
    関数名(&変数名)
}

次のコードは、先ほどの increment をポインタ引数で書き直した例です。

package main

import "fmt"

func increment(n *int) {
    *n++
}

func main() {
    x := 5
    increment(&x)
    fmt.Println(x)
}

以下のような実行結果が表示されます。

6

increment(&x)x のアドレスを渡し、関数の中で *n++ を実行することで、呼び出し元の x6 に書き換わりました。

コードを解説します。

func increment(n *int) {
    *n++
}

引数の型を *intint を指すポインタ)にしています。*n++ は「n が指している変数の値を1増やす」の意味で、呼び出し元の変数を直接書き換えます。

increment(&x)

&xx のアドレスを取り出して関数に渡しています。これにより、関数の中で x そのものが更新されます。

3.2 スライス・マップと関数の関係

これまでの例では、値を書き換えるには明示的にポインタを渡す必要がありました。ただし、スライスマップ は内部にポインタを持つため、そのまま関数に渡しても要素の書き換えは呼び出し元に反映されます。書き方は以下のとおりです。

func 関数名(s []型) {
    s[インデックス] = 値   // ポインタを明示しなくても呼び出し元に反映される
}

次のコードは、スライスを関数に渡してその中で要素を書き換える例です。

package main

import "fmt"

func modify(s []int) {
    s[0] = 100
}

func main() {
    nums := []int{1, 2, 3}
    modify(nums)
    fmt.Println(nums)
}

以下のような実行結果が表示されます。

[100 2 3]

modify の中で s[0]100 に書き換えたところ、呼び出し元の nums にも反映されました。

コードを解説します。

func modify(s []int) {
    s[0] = 100
}

スライスは内部に「要素が並んでいるメモリ領域へのポインタ」を持っています。関数に渡すとそのポインタごとコピーされるため、s と呼び出し元の nums は同じメモリ領域を指し、要素の書き換えが両方から見えるようになります。マップも同じ仕組みです。

なお、append で容量を超える追加が起きた場合、内部で新しいメモリ領域に切り替わるため、その変更は呼び出し元には伝わりません。長さを変える操作を関数の外に反映したい場合は、これまでの例のように結果を戻り値で返すか、*[]int のようにスライスのポインタを渡します。

4. 構造体での利用

これまで int に対して &* を扱ってきましたが、& は変数の型を問わないため、構造体にも同じように使えます。ここでは、構造体のアドレス取得、フィールドアクセスの省略記法、メソッドの値レシーバ・ポインタレシーバ、そしてそれらの相互呼び出しまでを順に見ていきます。

4.1 アドレス取得

構造体の変数に対しても & でアドレスを取れます。書き方は以下のとおりです。得られるポインタの型は *型名 になります。

p := &構造体変数    // 構造体のアドレスを取得。p の型は *型名

次のコードは、User 型の変数 u からポインタ p を作り、その型と中身を確認する例です。

package main

import "fmt"

type User struct {
    Name string
    Age  int
}

func main() {
    u := User{Name: "Taro", Age: 30}
    p := &u

    fmt.Printf("%T\n", p)   // ポインタの型
    fmt.Println(p)          // アドレス
}

以下のような実行結果が表示されます(アドレスは実行環境によって変わります)。

*main.User
0xc0000100a0

p の型は *main.UserUser を指すポインタ)で、p の中身は u のアドレスです。int で扱った p := &x と同じ形で、構造体でもそのままポインタが得られていることが確認できます。

コードを解説します。

p := &u

&u で構造体変数 u のアドレスを取り出し、p に代入しています。int で扱った p := &x と同じ書き方で、構造体に対しても & を付けるだけでアドレスが得られます。

fmt.Printf("%T\n", p)

%T は「渡した値の型」を表示する書式指定子です。pUser のポインタなので *main.User と出力されます。main. はパッケージ名で、パッケージが変われば *パッケージ名.型名 の形になります。

fmt.Println(p)

p をそのまま出力すると、ポインタの中身(u のアドレス)が16進数で表示されます。アドレスの具体的な値はプログラムの実行環境や実行のたびに変わります。

4.2 フィールドアクセスの省略記法

構造体のポインタからフィールドにアクセスするときは、本来 (*p).フィールド名 と書きますが、Go では . を書くだけで自動的にデリファレンスされ、p.フィールド名 の形で直接アクセスできます。書き方は以下のとおりです。

p.フィールド名     // (*p).フィールド名 と同じ意味で、フィールドの読み書きができる

次のコードは、User のポインタ p から Name フィールドを読み書きする例です。

package main

import "fmt"

type User struct {
    Name string
    Age  int
}

func main() {
    u := User{Name: "Taro", Age: 30}
    p := &u

    fmt.Println(p.Name)
    p.Name = "Jiro"
    fmt.Println(u.Name)
}

以下のような実行結果が表示されます。

Taro
Jiro

p.NameTaro が読み取れ、p.Name = "Jiro" で書き換えたところ、元の uNameJiro に変わりました。

コードを解説します。

fmt.Println(p.Name)

p*User 型ですが、p.Name と書けば Go が自動的に (*p).Name として扱ってくれます。毎回 * を書かなくて済むため、構造体ポインタは実務でも扱いやすくなっています。

p.Name = "Jiro"
fmt.Println(u.Name)

p が指しているのは u そのものなので、p.Name を書き換えると u.Name も同じ値になります。ポインタを渡した先で構造体の中身を更新できるのは、この仕組みによるものです。

この省略記法の利点は主に2つあります。1つは記述量が減ることで、(*p).Name の代わりに p.Name と書けるだけでなく、ネストしたフィールド(例: p.Address.City)にも . を続けるだけで到達できます。もう1つは、値(u.Name)とポインタ(p.Name)で書き方が同じになる点で、値を扱うコードをあとからポインタに書き換えても、フィールドアクセスの記述はそのままで済みます。

4.3 値レシーバとポインタレシーバ

前章の Go構造体 でメソッドを扱ったときに、レシーバは (u User) のように書きました。実はこのレシーバも、値で受け取るか、ポインタで受け取るかの2種類があります。書き方は以下のとおりです。ポインタレシーバは型名の前に * を付けます。

// 値レシーバ
func (レシーバ変数 型) メソッド名() { ... }

// ポインタレシーバ
func (レシーバ変数 *型) メソッド名() { ... }

次のコードは、同じ Counter 型に対して、値レシーバの IncByValue とポインタレシーバの IncByPointer を用意し、動きの違いを比較する例です。

package main

import "fmt"

type Counter struct {
    Count int
}

// 値レシーバ: メソッド内での変更は呼び出し元に伝わらない
func (c Counter) IncByValue() {
    c.Count++
}

// ポインタレシーバ: メソッド内での変更が呼び出し元に伝わる
func (c *Counter) IncByPointer() {
    c.Count++
}

func main() {
    c := Counter{Count: 0}

    c.IncByValue()
    fmt.Println(c.Count)

    c.IncByPointer()
    fmt.Println(c.Count)
}

以下のような実行結果が表示されます。

0
1

IncByValue を呼んだあとの c.Count0 のまま(コピーを書き換えただけ)、IncByPointer を呼んだあとは 1 に増えました(本体を書き換えた)。

コードを解説します。

func (c Counter) IncByValue() {
    c.Count++
}

レシーバを (c Counter) と型そのもので受けています。この場合、c は呼び出し元の値のコピーなので、c.Count++ してもコピーの値が変わるだけで、元の Counter には影響しません。

func (c *Counter) IncByPointer() {
    c.Count++
}

レシーバを (c *Counter) とポインタ型で受けています。*Counter は「Counter のポインタ」を意味し、呼び出し元の値そのものを指します。c.Count++ はここでも「フィールドアクセスの省略記法」((*c).Count++)が働いており、本体のフィールドを書き換えるため、呼び出し元の Counter に変更が反映されます。

使い分けの目安として、フィールドを変更するメソッドや、フィールドが多い大きな構造体(呼び出しのたびに値全体をコピーしたくない場面)では、ポインタレシーバを使います。それ以外は値レシーバでも構いませんが、同じ型のメソッド間ではどちらかに統一するのが原則です。

4.4 レシーバの相互呼び出し

値レシーバとポインタレシーバのメソッドは、呼び出し側でわざわざ *& を書かなくても、Go が自動で対応してくれる場面が多くあります。次のコードは、Counter の値レシーバ・ポインタレシーバ両方のメソッドを、値からもポインタからも呼び出す例です。

package main

import "fmt"

type Counter struct {
    Count int
}

func (c Counter) Value() int { return c.Count }   // 値レシーバ
func (c *Counter) Inc()      { c.Count++ }         // ポインタレシーバ

func main() {
    c := Counter{Count: 0}
    p := &c

    c.Inc()                  // 値から ポインタレシーバのメソッドを呼ぶ
    fmt.Println(p.Value())   // ポインタから 値レシーバのメソッドを呼ぶ
}

以下のような実行結果が表示されます。

1

c.Inc()(値からポインタレシーバ)と p.Value()(ポインタから値レシーバ)の両方がそのまま書けて、それぞれ意図通りに動きました。

コードを解説します。

c.Inc()

Inc はポインタレシーバのメソッドですが、変数 c からは c.Inc() の形で呼び出せます。Go が自動的に (&c).Inc() に置き換えてくれるためです。

fmt.Println(p.Value())

Value は値レシーバのメソッドですが、ポインタ p からは p.Value() の形で呼び出せます。こちらも自動的に (*p).Value() に置き換えてくれます。

ただし、マップの要素や式の結果のような一時的な値 からは、Go はアドレスを取れないため、ポインタレシーバのメソッドを直接呼び出せません。その場合は、いったんローカル変数に受けてから呼び出す必要があります。

5. まとめ

この章では、Goのポインタを学びつつ、実際に &* の使い方、ポインタを引数に取る関数、構造体でのポインタ利用(フィールドアクセス・メソッドのレシーバ)を体験しました。

  • 変数のアドレスを指し示す値を ポインタ と呼ぶ
  • &変数名 でアドレスを取得、*ポインタ で参照や代入ができる
  • 未初期化のポインタは nil になり、参照すると実行時エラーで停止するため、使う前に if p != nil { ... } で判定する
  • 関数の引数をポインタ型で受け取ると、呼び出し元の値を直接書き換えられる
  • スライスやマップは内部にポインタを持つため、関数に渡しても要素の書き換えは呼び出し元に反映される
  • & は型を問わないため構造体にもそのまま使え、構造体ポインタは p.フィールド の形で自動デリファレンスされる((*p).フィールド と書かなくてもよい)
  • メソッドの ポインタレシーバ(c *Counter) の形)もこの仕組みの応用で、レシーバの値を書き換えられる
  • 値からポインタレシーバのメソッド、ポインタから値レシーバのメソッドも、Go が自動変換してそのまま呼び出せる
  • フィールドを変更するメソッドや、フィールドが多く値全体をコピーしたくない構造体では、ポインタレシーバを使うのが原則である

次の章では、Goのインタフェースを学びつつ、実際に interface による切り替えの書き方を体験します。

この教材は役に立ちましたか?

いいねをたくさんいただけると、制作者の励みになり、より多くのセクションが作れるようになります。

感想を一言(任意)

いただいたコメントは次の制作のヒントになります。ぜひお気軽にご投稿ください。

このコメントは他の受講生には公開されません。DevOps Camp運営が、教材改善のために確認します。

0 / 2000