🕶️

【保存版】Goのポインタを完全攻略!初心者から実践まで

に公開
2

🚀 なぜポインタを学ぶべきなのか?

Goを書いていて、こんな経験はありませんか?

// 関数で値を変更したいのに...
func updateUser(user User) {
    user.Name = "新しい名前"
}

func main() {
    user := User{Name: "太郎"}
    updateUser(user)
    fmt.Println(user.Name) // まだ"太郎"のまま😱
}

この問題をポインタで解決できます!

さらに、Goの標準ライブラリでもポインタは頻繁に使われています:

// JSONの変換でよく見るパターン
json.Unmarshal(data, &user) // ←この&って何?

// HTTPハンドラでもよく見る
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    // ←この*http.Requestって何?
})

この記事を読めば、これらの疑問が全て解決します!

📍 ポインタの基本概念

そもそもポインタとは?

ポインタを理解するために、まずコンピュータのメモリを想像してみましょう。

メモリ = 巨大なマンション
各部屋 = 変数
住所 = メモリアドレス
ポインタ = 住所を書いた紙

実際のコードで見てみましょう:

package main

import "fmt"

func main() {
    // 値を格納する「部屋」を作る
    age := 25
    
    // その「部屋の住所」を取得する
    agePointer := &age
    
    fmt.Printf("値: %d\n", age)                    // 25
    fmt.Printf("住所: %p\n", &age)                 // 0xc000014098
    fmt.Printf("ポインタ: %p\n", agePointer)       // 0xc000014098
    fmt.Printf("ポインタが指す値: %d\n", *agePointer) // 25
}

🔧 ポインタの演算子

Goでポインタを扱うには2つの演算子を覚えるだけです:

演算子 名前 用途 読み方
& アドレス演算子 変数の住所を取得 アンパサンド &variable
* デリファレンス演算子 住所の値を取得/変更 アスタリスク *pointer
func main() {
    score := 100
    
    // & = 住所を教えて
    scoreAddr := &score
    fmt.Printf("scoreの住所: %p\n", scoreAddr)
    
    // * = その住所にある値を教えて/変更して
    fmt.Printf("住所にある値: %d\n", *scoreAddr) // 100
    
    *scoreAddr = 200 // 住所にある値を変更
    fmt.Printf("scoreの新しい値: %d\n", score)   // 200
}

重要ポイント:

  • *scoreAddr = 200score の値も変わる理由は、同じメモリ領域を指しているから
  • アドレスは実行するたびに変わるが、これは正常な動作

🎯 値渡し vs ポインタ渡し(最重要!)

これがポインタの最も重要な使い道です。

❌ 値渡し(変更されない)

func tryToDouble(num int) {
    num = num * 2
    fmt.Printf("関数内: %d\n", num) // 20
}

func main() {
    value := 10
    tryToDouble(value)
    fmt.Printf("関数外: %d\n", value) // まだ10😱
}

なぜ変更されない?
→ 関数に渡されるのは値のコピーだから

これは「コピー用紙に書いて渡すようなもの」です。コピーに何を書いても、元の紙は変わりません。

✅ ポインタ渡し(変更される)

func actuallyDouble(num *int) {
    *num = *num * 2
    fmt.Printf("関数内: %d\n", *num) // 20
}

func main() {
    value := 10
    actuallyDouble(&value)
    fmt.Printf("関数外: %d\n", value) // 20に変更された🎉
}

なぜ変更される?
→ 関数に渡されるのは値の住所だから

これは「家の住所を教えて、直接家に来てもらうようなもの」です。

🏗️ ポインタの宣言と初期化

// 方法1: 明示的な宣言
var p1 *int        // ゼロ値はnil(何も指していない状態)
x := 42
p1 = &x

// 方法2: 短縮宣言(最も一般的)
y := 100
p2 := &y // var p2 *int = &y と同じ

// 方法3: new関数を使用
p3 := new(int)     // int型のゼロ値を作成し、そのアドレスを返す
*p3 = 300

fmt.Println(*p1)   // 42
fmt.Println(*p2)   // 100
fmt.Println(*p3)   // 300

各方法の特徴:

  • 方法1: 最初はnilで、後から実際のアドレスを代入
  • 方法2: 型推論で自動的にポインタ型になる(実際の開発で最もよく使われる)
  • 方法3: メモリ確保と初期化を同時に行う(使用頻度は少ない)

🎯 実践的な使用例

1. 構造体の更新

type User struct {
    Name  string
    Email string
    Age   int
}

// ❌ 更新されない版
func updateUserBad(u User, newAge int) {
    u.Age = newAge // コピーを変更するだけ
}

// ✅ 更新される版
func updateUserGood(u *User, newAge int) {
    u.Age = newAge // 元の構造体を変更
}

func main() {
    user := User{Name: "田中", Email: "tanaka@example.com", Age: 25}
    
    updateUserBad(user, 30)
    fmt.Printf("Bad: %+v\n", user) // Age: 25のまま
    
    updateUserGood(&user, 30)
    fmt.Printf("Good: %+v\n", user) // Age: 30に変更
}

2. 大きな構造体の効率的な処理

type LargeData struct {
    Matrix [1000][1000]int // 4MB のデータ
    Name   string
}

// ❌ 毎回4MBをコピー(遅い)
func processDataSlow(data LargeData) {
    fmt.Printf("Processing %s\n", data.Name)
}

// ✅ 8バイトのポインタのみ(高速)
func processDataFast(data *LargeData) {
    fmt.Printf("Processing %s\n", data.Name)
}

func main() {
    data := LargeData{Name: "BigData"}
    
    start := time.Now()
    for i := 0; i < 1000; i++ {
        processDataSlow(data) // 4MB × 1000回 = 4GB のコピー
    }
    fmt.Printf("Slow: %v\n", time.Since(start))
    
    start = time.Now()
    for i := 0; i < 1000; i++ {
        processDataFast(&data) // 8バイト × 1000回 = 8KB のコピー
    }
    fmt.Printf("Fast: %v\n", time.Since(start))
}

3. オプショナルなデータの表現

これは実際の開発でよく使うパターンです!

type UserProfile struct {
    ID       int     // 必須
    Name     string  // 必須
    Age      *int    // 任意:年齢非公開可能
    Website  *string // 任意:ウェブサイトなし可能
    Bio      *string // 任意:自己紹介未記入可能
}

func createProfile(name string, age *int, website *string) UserProfile {
    return UserProfile{
        ID:      rand.Intn(10000),
        Name:    name,
        Age:     age,
        Website: website,
    }
}

func displayProfile(p UserProfile) {
    fmt.Printf("名前: %s (ID: %d)\n", p.Name, p.ID)
    
    // nilチェックが重要!
    if p.Age != nil {
        fmt.Printf("年齢: %d\n", *p.Age)
    } else {
        fmt.Println("年齢: 非公開")
    }
    
    if p.Website != nil {
        fmt.Printf("ウェブサイト: %s\n", *p.Website)
    } else {
        fmt.Println("ウェブサイト: 未設定")
    }
    
    if p.Bio != nil {
        fmt.Printf("自己紹介: %s\n", *p.Bio)
    } else {
        fmt.Println("自己紹介: 未記入")
    }
}

func main() {
    age := 28
    website := "https://example.com"
    
    // 全ての情報が揃っている場合
    profile1 := createProfile("山田太郎", &age, &website)
    displayProfile(profile1)
    
    fmt.Println("---")
    
    // 一部の情報のみの場合
    profile2 := createProfile("佐藤花子", nil, nil)
    displayProfile(profile2)
}

なぜポインタを使うのか?

  • 通常の値型では「ゼロ値」と「未設定」の区別ができない
  • Age int では 0 が「0歳」なのか「未設定」なのか分からない
  • Age *int なら nil で「未設定」を明確に表現

4. 外部とのデータやり取り

実際のWeb開発でよく見るパターンです:

type APIResponse struct {
    Success bool        `json:"success"`
    Data    *UserData   `json:"data,omitempty"`    // 成功時のみ
    Error   *ErrorInfo  `json:"error,omitempty"`   // エラー時のみ
}

type UserData struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

type ErrorInfo struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

// HTTPリクエストから値を取得
func handleUser(w http.ResponseWriter, r *http.Request) {
    // 📥 STEP 1: クライアントからJSONを受信
    // HTTPリクエストのボディには以下のようなJSONが含まれる
    // {"name":"山田太郎","age":28}

    var user UserData

    // 🔄 STEP 2: Decode - JSONを構造体に変換
    // なぜ&userが必要?→ json.Decoderが構造体の中身を変更するため
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        // エラーレスポンス
        response := APIResponse{
            Success: false,
            Error:   &ErrorInfo{Code: 400, Message: "Invalid JSON"},
        }
        json.NewEncoder(w).Encode(&response)
        return
    }

    // 💼 STEP 3: 何らかの処理(例:データベースに保存)
    fmt.Printf("受信したユーザー: %s(ID: %d\n", user.Name, user.ID)

    // 🔄 STEP 4: Encode - 構造体をJSONに変換してレスポンス
    response := APIResponse{
        Success: true,
        Data:    &user,
    }
    json.NewEncoder(w).Encode(&response)
}

なぜポインタが必要なのか?

  • json.UnmarshalDecode は「どこに結果を書き込むか」を知る必要がある
  • 関数が変数の中身を変更するには、その変数のメモリアドレス(ポインタ)が必要
  • 値渡しでは元の変数は変更されない

🚫 スライス型とマップ型は内部で参照を使用

スライス型(配列のようなもの)

func modifySlice(s []int) {
    s[0] = 999  // 参照先の配列が変更される
    s = nil     // ただし、元の変数sliceは変更されない
}
func main() {
    slice := []int{1, 2, 3}
    modifySlice(slice)
    fmt.Println(slice)  // [999 2 3] - 内容は変更された
    fmt.Println(slice == nil)  // false - 変数自体は変更されない
}

マップ型(辞書のようなもの)

func modifyMap(m map[string]int) {
    m["key"] = 100  // 参照先のマップが変更される
    m = nil         // ただし、元の変数myMapは変更されない
}
func main() {
    myMap := map[string]int{"key": 1}
    modifyMap(myMap)
    fmt.Println(myMap)  // map[key:100] - 内容は変更された
    fmt.Println(myMap == nil)  // false - 変数自体は変更されない
}

重要ポイント:

  • スライスとマップは内部で参照を使用している
  • 「参照の値渡し」または「共有渡し(Call by Sharing)」と呼ばれる仕組み
  • 参照先のデータは共有されるが、参照自体は独立したコピー
  • 明示的に &* を使わなくても参照先のデータを変更可能

補足説明:
厳密には「参照渡し」ではなく、参照値(ポインタ)を値として渡している。これにより:

  • ✅ 参照先のオブジェクト(配列やマップの実体)は変更できる
  • ❌ 関数内で参照自体を変更しても、元の変数には影響しない

🛡️ 安全なポインタの使い方

❌ nilポインタは危険!

func dangerousFunction() {
    var p *int
    fmt.Println(*p) // panic: runtime error ⚠️
}

✅ 防御的プログラミング

// 安全なバージョン
func safeFunction(p *int) {
    if p != nil {
        fmt.Printf("値: %d\n", *p)
    } else {
        fmt.Println("ポインタがnilです")
    }
}

// 早期リターンパターン
func processUser(user *User) {
    if user == nil {
        fmt.Println("ユーザーがnilです")
        return
    }
    
    // 安全にユーザーを処理
    fmt.Printf("ユーザー: %s\n", user.Name)
}

🔧 便利なヘルパー関数

// 値からポインタを作成するヘルパー関数
func IntPtr(v int) *int { return &v }
func StringPtr(v string) *string { return &v }
func BoolPtr(v bool) *bool { return &v }

// ポインタから値を安全に取得するヘルパー関数
func IntValue(p *int) int {
    if p != nil {
        return *p
    }
    return 0 // デフォルト値
}

func StringValue(p *string) string {
    if p != nil {
        return *p
    }
    return "" // デフォルト値
}

func main() {
    // 使いやすくなった!
    profile := UserProfile{
        Name:    "田中",
        Age:     IntPtr(30),
        Website: StringPtr("https://tanaka.dev"),
    }
    
    fmt.Printf("年齢: %d\n", IntValue(profile.Age))
    fmt.Printf("サイト: %s\n", StringValue(profile.Website))
}

🤔 よくある質問と回答

Q1. いつポインタを使うべき?

A. 以下の場合に使用を検討しましょう:

  1. 値を変更したい時

    • 関数内で引数の値を変更する必要がある
    • 複数の戻り値の代わりに使う
  2. 大きな構造体を効率的に渡したい時

    • メモリ使用量とパフォーマンスの最適化
    • 1KB以上の構造体は要検討
  3. オプショナルな値を表現したい時

    • 「設定なし」「未入力」「非公開」の状態
    • データベースのNULL値に相当
  4. 外部ライブラリとの連携時

    • JSON、XML、データベースライブラリ
    • 標準ライブラリの多くの関数

Q2. スライスやマップにポインタは必要?

A. 不要です! スライスとマップは既に参照型です:

// ポインタ不要(これで十分)
func addElement(s []int, val int) []int {
    return append(s, val)
}

func updateMap(m map[string]int, key string, val int) {
    m[key] = val // 元のマップが変更される
}

Q3. C/C++のポインタと何が違う?

A. Goの方が安全です:

項目 C/C++ Go
メモリ管理 手動(malloc/free) 自動(ガベージコレクション)
ポインタ演算 あり(ptr++) なし
ダングリングポインタ 発生する 発生しない
デリファレンス 手動 構造体は自動

Q4. TypeScriptとの比較は?

A. 似たような概念があります:

// TypeScript
interface User {
  name: string;
  age?: number;  // オプショナル
}

// Go
type User struct {
    Name string
    Age  *int    // オプショナル(nilで未設定)
}

🎯 ポインタを使う判断基準

以下のフローチャートで判断してみましょう:

関数内で値を変更する必要がある?
├─ Yes → ポインタを使う
└─ No ↓

大きな構造体(1KB以上)?
├─ Yes → ポインタを使う(パフォーマンス向上)
└─ No ↓

「未設定」状態を表現したい?
├─ Yes → ポインタを使う(nilで表現)
└─ No ↓

外部ライブラリが要求している?
├─ Yes → ポインタを使う
└─ No → 値渡しで十分

📚 さらに学習を深めるために

関連する重要なGoの概念

  1. インターフェースポインタレシーバー

    type Writer interface {
        Write([]byte) (int, error)
    }
    
    func (u *User) Write(data []byte) (int, error) {
        // ポインタレシーバー
    }
    
  2. エラーハンドリングとポインタ

    func getUser(id int) (*User, error) {
        // ユーザーが見つからない場合: (nil, error)
        // 見つかった場合: (*User, nil)
    }
    
  3. 並行処理での値の受け渡し

    func processUser(userChan chan *User) {
        for user := range userChan {
            // ポインタで効率的に処理
        }
    }
    

実践的な学習方法

  1. 小さなプログラムから始める

    • 値の変更を試す
    • nilチェックを習慣化する
  2. 標準ライブラリのコードを読む

    • encoding/json パッケージ
    • net/http パッケージ
  3. 実際のプロジェクトで使う

    • REST API の作成
    • データベース操作

おすすめの学習リソース

🎉 まとめ

この記事で学んだこと:

  • ✅ ポインタはメモリアドレスを格納する仕組み
  • &で住所取得、*で値アクセス
  • 値を変更したい時に必須
  • パフォーマンス向上に効果的(大きな構造体)
  • オプショナルデータの表現に便利(nil で未設定)
  • 外部ライブラリとの連携で頻繁に使用
  • nilチェックで安全に使用
  • ✅ スライス・マップは既に参照型なのでポインタ不要

ポインタの本質は「効率的で安全なデータ共有の仕組み」です。

まずは小さなプログラムから始めて、徐々に慣れていきましょう。きっと「ポインタって実は簡単だった!」と感じるはずです 🚀


この記事が役に立ったら、いいね👍やフォローをお願いします!
質問やコメントもお気軽にどうぞ 😊

📝 更新履歴・謝辞

2025/07/07 更新
junerさんからのご指摘により、「スライス型とマップ型は既に参照型」の章を修正しました。「参照渡し」と「参照の値渡し」の用語の違いについて正確な表現に修正し、より理解しやすい内容にアップデートしました。貴重なフィードバックをありがとうございました!

技術記事は読者の皆様からのフィードバックにより、より良い内容に成長していきます。間違いや改善点がございましたら、お気軽にコメントでお知らせください。

Discussion

junerjuner

明示的に &* を使わなくても参照渡しになる

それはいわゆる 参照の値渡し や 共有渡しとよばれる 参照値を渡す渡し方ではないでしょうか?

参照渡しは 変数(の参照)を渡すことであって 参照値を値渡しすることを指していません。

ミカヅキミカヅキ

junerさん、貴重なご指摘をありがとうございます!

おっしゃる通りです。私の記事で「参照渡し」と表現した部分は、正確には「参照の値渡し」や「共有渡し」と呼ぶべきでした。

参照先のデータは共有されますが、参照自体は独立したコピーなので、厳密には「参照渡し」ではありませんね。記事の該当部分を修正させていただきます。

このような建設的なフィードバックをいただけて本当に助かります。ありがとうございました!🙏

1