2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

2
Last updated at Posted at 2025-07-06

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

この記事は「Goのポインタがよく分からない...」という方向けの記事です。実際のコードを動かしながら、段階的に理解していきましょう!

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

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 Playgroundでコードを実行すると、住所(メモリアドレス)は実行するたびに変わります。これは正常な動作です。

🔧 ポインタの演算子

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

🎯 値渡し 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: メモリ確保と初期化を同時に行う(使用頻度は少ない)

nilポインタは危険!
初期化されていないポインタは nil です。nil ポインタを参照するとプログラムが停止(panic)します。

🎯 実践的な使用例

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に変更
}

Goの糖衣構文(シンタックスシュガー)
u.Age は実際には (*u).Age の省略形です。Goが自動的にデリファレンスしてくれます!

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

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

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

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チェックで安全に使用
  • ✅ スライス・マップは既に参照型なのでポインタ不要

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

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

実際に手を動かしてみよう!
この記事のコードは全てGo Playgroundで実行できます。コピペして試してみてください!


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

📝 更新履歴・謝辞

2025/07/07 更新
「スライス型とマップ型は既に参照型」の章を修正しました。「参照渡し」と「参照の値渡し」の用語の違いについて正確な表現に修正し、より理解しやすい内容にアップデートしました。

間違いや改善点がございましたら、お気軽にコメントでお知らせください。

2
1
1

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?