フリーキーズ | 独学プログラミング

Go言語のポインタと値渡し・参照渡しを基礎から徹底解説!初心者でもわかる入門ガイド

リンドくん

リンドくん

たなべ先生、Go言語を勉強してるんですけど、ポインタとか値渡しとか、なんだか難しそうで...

たなべ

たなべ

わかるよ!でも実は、Go言語のポインタは他の言語と比べてシンプルで理解しやすいんだ。
今日は基礎から丁寧に解説するから、一緒に学んでいこう!

プログラミングを学ぶ中で、「ポインタ」や「値渡し・参照渡し」という言葉を聞いたことがあるのではないでしょうか?
特にGo言語を学習している方にとって、これらの概念は避けて通れない重要なテーマです。

しかし、多くの初学者が「難しそう」「理解できない」と感じてしまうのも事実です。
実際、自分も最初にポインタという概念に出会ったときは、「なぜこんな複雑な仕組みが必要なの?」と思ったものです。

でも安心してください。Go言語のポインタは、C言語のポインタと比べて非常にシンプルで安全に設計されています。
そして、これらの概念を理解することで、メモリ効率的なプログラムを書けるようになり、AI時代のエンジニアとして必要な基礎力を身につけることができます。

この記事では、Go言語のポインタと値渡し・参照渡しについて、初心者の方でも理解できるよう段階的に解説していきます。

プログラミング学習でお悩みの方へ

HackATAは、エンジニアを目指す方のためのプログラミング学習コーチングサービスです。 経験豊富な現役エンジニアがあなたの学習をサポートします。

✓ 質問し放題

✓ β版公開中(2025年内の特別割引)

HackATAの詳細を見る

ポインタとは何か?基本概念を理解しよう

リンドくん

リンドくん

そもそも「ポインタ」って何なんですか?

たなべ

たなべ

ポインタは変数の住所を記録する特別な変数なんだ。まるで手紙を送るときの住所のようなものだよ。
実際の値ではなく、その値がメモリのどこにあるかを教えてくれるんだ。

ポインタの基本的な考え方

ポインタとは、メモリ上のアドレス(住所)を格納する変数のことです。
変数には実際の値が入っていますが、ポインタにはその変数がメモリのどこに保存されているかという「場所の情報」が入っています。

例えば、あなたの家に「たなべ」という名前の人が住んでいるとします。この場合、変数とポインタは以下の関係になります。

  • 変数 = 「たなべ」(実際の値)
  • ポインタ = 「福岡県福岡市A区B-C」(住所・アドレス)

このように、ポインタは「実際のデータがどこにあるか」を示すものなのです。

Go言語におけるポインタの特徴

Go言語のポインタには、以下のような特徴があります。

  • 安全性 → ポインタ演算(アドレスの計算)は禁止されているため、メモリ破壊が起きにくい
  • シンプルさ → C言語のような複雑な仕組みはなく、基本的な操作のみ
  • ガベージコレクション → メモリの自動管理により、メモリリークの心配が少ない

これらの特徴により、Go言語のポインタは初心者でも比較的理解しやすくなっています。

Go言語でのポインタの基本的な使い方

ポインタの宣言と基本操作

Go言語でポインタを使う際の基本的な操作を見てみましょう。

package main

import "fmt"

func main() {
    // 通常の変数
    number := 42
    
    // ポインタの宣言(&演算子でアドレスを取得)
    var pointer *int = &number
    
    fmt.Println("変数の値:", number)          // 42
    fmt.Println("変数のアドレス:", &number)    // 0xc0000140a0(例)
    fmt.Println("ポインタの値:", pointer)      // 0xc0000140a0(例)
    fmt.Println("ポインタが指す値:", *pointer) // 42
}

ここで重要なのは以下の演算子です。

  • &演算子: 変数のアドレスを取得する(「アドレス演算子」)
  • *演算子: ポインタが指している値を取得する(「間接参照演算子」)

ポインタを使った値の変更

ポインタの真価は、別の場所から元の変数の値を変更できることにあります。

package main

import "fmt"

func main() {
    number := 10
    pointer := &number
    
    fmt.Println("変更前:", number) // 10
    
    // ポインタ経由で値を変更
    *pointer = 20
    
    fmt.Println("変更後:", number) // 20
}

このように、ポインタを使うことで、元の変数を直接操作することができます。

値渡しと参照渡しの違いを理解しよう

リンドくん

リンドくん

関数に値を渡すときの「値渡し」と「参照渡し」って何が違うんですか?

たなべ

たなべ

これは超重要な概念だね!
値渡しは「コピーを渡す」、参照渡し(ポインタ渡し)は「住所を教える」という違いがあるんだ。

値渡しの仕組み

値渡しでは、関数に値のコピーが渡されます。
そのため、関数内で値を変更しても、元の変数には影響しません。

package main

import "fmt"

// 値渡しの関数
func changeValueByValue(x int) {
    x = 100
    fmt.Println("関数内での値:", x) // 100
}

func main() {
    number := 42
    
    fmt.Println("関数呼び出し前:", number) // 42
    changeValueByValue(number)
    fmt.Println("関数呼び出し後:", number) // 42(変更されない!)
}

この例では、numberの値(42)がコピーされて関数に渡されるため、関数内でxを変更しても元のnumberは変わりません。

参照渡し(ポインタ渡し)の仕組み

参照渡しでは、変数のアドレス(ポインタ)を関数に渡します。
これにより、関数内から元の変数を直接操作できます。

package main

import "fmt"

// ポインタを使った参照渡しの関数
func changeValueByReference(x *int) {
    *x = 100
    fmt.Println("関数内での値:", *x) // 100
}

func main() {
    number := 42
    
    fmt.Println("関数呼び出し前:", number) // 42
    changeValueByReference(&number)
    fmt.Println("関数呼び出し後:", number) // 100(変更された!)
}

この例では、numberのアドレスが関数に渡されるため、関数内から元のnumberを直接変更できます。

どちらを使うべきか?

それぞれの使い分けは以下のような基準で判断します。

  • 値渡し: 元の値を変更したくない場合、小さなデータの場合
  • 参照渡し: 元の値を変更したい場合、大きなデータの場合(メモリ効率のため)

構造体とポインタの実践的な活用法

構造体での値渡しとポインタ渡し

Go言語では構造体もよく使われます。構造体とポインタの組み合わせを見てみましょう。

package main

import "fmt"

// 構造体の定義
type Person struct {
    Name string
    Age  int
}

// 値渡しのメソッド(元の構造体は変更されない)
func (p Person) ChangeAgeByValue(newAge int) {
    p.Age = newAge
    fmt.Println("メソッド内:", p.Age)
}

// ポインタレシーバのメソッド(元の構造体が変更される)
func (p *Person) ChangeAgeByPointer(newAge int) {
    p.Age = newAge
    fmt.Println("メソッド内:", p.Age)
}

func main() {
    person := Person{Name: "田中", Age: 25}
    
    fmt.Println("変更前:", person.Age) // 25
    
    // 値渡しのメソッド
    person.ChangeAgeByValue(30)
    fmt.Println("値渡し後:", person.Age) // 25(変更されない)
    
    // ポインタレシーバのメソッド
    person.ChangeAgeByPointer(30)
    fmt.Println("ポインタ渡し後:", person.Age) // 30(変更される)
}

スライスとマップの特殊な性質

Go言語には、スライスマップといった参照型があります。これらは特殊な性質を持っています。

package main

import "fmt"

func modifySlice(slice []int) {
    slice[0] = 999
}

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    
    fmt.Println("変更前:", numbers) // [1 2 3 4 5]
    modifySlice(numbers)
    fmt.Println("変更後:", numbers) // [999 2 3 4 5](変更される!)
}

スライスは値渡しでも内部のデータが変更されます。これは、スライスが内部的に配列への参照を持っているためです。

メモリ効率とパフォーマンスを考慮した使い分け

リンドくん

リンドくん

ポインタを使うとパフォーマンスが良くなるって聞いたんですけど、本当ですか?

たなべ

たなべ

その通り!特に大きなデータ構造を扱うときは、ポインタを使うことでメモリ使用量を大幅に削減できるんだ。

大きな構造体での比較

大きな構造体を扱う際の、値渡しとポインタ渡しの違いを見てみましょう。

package main

import (
    "fmt"
    "runtime"
    "time"
)

// 大きな構造体
type LargeStruct struct {
    Data [1000000]int // 100万個のint配列
}

// 値渡しの関数
func processByValue(ls LargeStruct) {
    // 何かの処理...
    _ = ls.Data[0]
}

// ポインタ渡しの関数
func processByPointer(ls *LargeStruct) {
    // 何かの処理...
    _ = ls.Data[0]
}

func main() {
    largeData := LargeStruct{}
    
    // 値渡しのテスト
    start := time.Now()
    for i := 0; i < 1000; i++ {
        processByValue(largeData)
    }
    valueTime := time.Since(start)
    
    // ポインタ渡しのテスト
    start = time.Now()
    for i := 0; i < 1000; i++ {
        processByPointer(&largeData)
    }
    pointerTime := time.Since(start)
    
    fmt.Printf("値渡し時間: %v\n", valueTime)
    fmt.Printf("ポインタ渡し時間: %v\n", pointerTime)
    
    var m runtime.MemStats
    runtime.GC()
    runtime.ReadMemStats(&m)
    fmt.Printf("メモリ使用量: %d KB\n", m.Alloc/1024)
}

このテストを実行すると、ポインタ渡しの方が圧倒的に高速で、メモリ使用量も少ないことがわかります。

適切な使い分けの指針

以下のような基準で使い分けを行いましょう。

値渡しを使う場合

  • 小さなデータ(int, string, 小さな構造体など)
  • 元のデータを変更したくない場合
  • 並行処理で安全性を重視する場合

ポインタ渡しを使う場合

  • 大きなデータ構造
  • 元のデータを変更したい場合
  • メモリ効率を重視する場合

よくある間違いと注意点

nilポインタの扱い

Go言語でポインタを扱う際の最も一般的な問題は、nilポインタへの操作です。

package main

import "fmt"

func main() {
    var pointer *int
    
    fmt.Println("ポインタの値:", pointer) // <nil>
    
    // これはランタイムエラー(panic)になる
    // fmt.Println("ポインタが指す値:", *pointer)
    
    // 安全な方法:nilチェック
    if pointer != nil {
        fmt.Println("ポインタが指す値:", *pointer)
    } else {
        fmt.Println("ポインタはnilです")
    }
    
    // ポインタに値を設定
    number := 42
    pointer = &number
    fmt.Println("ポインタが指す値:", *pointer) // 42
}

ポインタのポインタに注意

Go言語では、ポインタのポインタも作成できますが、複雑になりがちなので注意が必要です。

package main

import "fmt"

func main() {
    number := 42
    pointer := &number
    pointerToPointer := &pointer
    
    fmt.Println("値:", number)                        // 42
    fmt.Println("ポインタが指す値:", *pointer)           // 42
    fmt.Println("ポインタのポインタが指す値:", **pointerToPointer) // 42
}

このような複雑な構造は、通常の開発では避けることをお勧めします。

まとめ

リンドくん

リンドくん

ポインタって最初は難しく感じましたけど、実際に使ってみると便利なんですね!

たなべ

たなべ

そうなんだ!ポインタを理解すると、メモリ効率的で高性能なプログラムが書けるようになるよ。

Go言語のポインタと値渡し・参照渡しについて、基礎から実践的な活用法まで解説してきました。
重要なポイントを改めて整理しましょう。

ポインタの本質

  • ポインタはメモリアドレスを格納する変数
  • &演算子でアドレス取得、*演算子で値を参照
  • Go言語のポインタは安全で初心者にも理解しやすい

値渡しと参照渡しの使い分け

  • 値渡し: 小さなデータ、元の値を保護したい場合
  • 参照渡し: 大きなデータ、元の値を変更したい場合
  • メモリ効率とパフォーマンスを考慮した選択が重要

これらの概念を理解することで、より効率的で安全なGo言語プログラムを書けるようになります。
メモリの扱いが効率的なプログラミングスキルは非常に重要です。

ぜひ実際にコードを書いて試してみてください。
最初は慣れないかもしれませんが、練習を重ねることで必ず身につきます。

この記事をシェア

関連するコンテンツ