【保存版】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でポインタを扱うには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: メモリ確保と初期化を同時に行う(使用頻度は少ない)
🎯 実践的な使用例
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.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. 以下の場合に使用を検討しましょう:
-
値を変更したい時
- 関数内で引数の値を変更する必要がある
- 複数の戻り値の代わりに使う
-
大きな構造体を効率的に渡したい時
- メモリ使用量とパフォーマンスの最適化
- 1KB以上の構造体は要検討
-
オプショナルな値を表現したい時
- 「設定なし」「未入力」「非公開」の状態
- データベースのNULL値に相当
-
外部ライブラリとの連携時
- 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の概念
-
インターフェースとポインタレシーバー
type Writer interface { Write([]byte) (int, error) } func (u *User) Write(data []byte) (int, error) { // ポインタレシーバー } -
エラーハンドリングとポインタ
func getUser(id int) (*User, error) { // ユーザーが見つからない場合: (nil, error) // 見つかった場合: (*User, nil) } -
並行処理での値の受け渡し
func processUser(userChan chan *User) { for user := range userChan { // ポインタで効率的に処理 } }
実践的な学習方法
-
小さなプログラムから始める
- 値の変更を試す
- nilチェックを習慣化する
-
標準ライブラリのコードを読む
- encoding/json パッケージ
- net/http パッケージ
-
実際のプロジェクトで使う
- REST API の作成
- データベース操作
おすすめの学習リソース
- A Tour of Go - 公式チュートリアル
- 初めてのGo言語 - おすすめ書籍
🎉 まとめ
この記事で学んだこと:
- ✅ ポインタはメモリアドレスを格納する仕組み
- ✅
&で住所取得、*で値アクセス - ✅ 値を変更したい時に必須
- ✅ パフォーマンス向上に効果的(大きな構造体)
- ✅ オプショナルデータの表現に便利(nil で未設定)
- ✅ 外部ライブラリとの連携で頻繁に使用
- ✅ nilチェックで安全に使用
- ✅ スライス・マップは既に参照型なのでポインタ不要
ポインタの本質は「効率的で安全なデータ共有の仕組み」です。
まずは小さなプログラムから始めて、徐々に慣れていきましょう。きっと「ポインタって実は簡単だった!」と感じるはずです 🚀
この記事が役に立ったら、いいね👍やフォローをお願いします!
質問やコメントもお気軽にどうぞ 😊
📝 更新履歴・謝辞
2025/07/07 更新
junerさんからのご指摘により、「スライス型とマップ型は既に参照型」の章を修正しました。「参照渡し」と「参照の値渡し」の用語の違いについて正確な表現に修正し、より理解しやすい内容にアップデートしました。貴重なフィードバックをありがとうございました!
技術記事は読者の皆様からのフィードバックにより、より良い内容に成長していきます。間違いや改善点がございましたら、お気軽にコメントでお知らせください。
Discussion
それはいわゆる 参照の値渡し や 共有渡しとよばれる 参照値を渡す渡し方ではないでしょうか?
参照渡しは 変数(の参照)を渡すことであって 参照値を値渡しすることを指していません。
junerさん、貴重なご指摘をありがとうございます!
おっしゃる通りです。私の記事で「参照渡し」と表現した部分は、正確には「参照の値渡し」や「共有渡し」と呼ぶべきでした。
参照先のデータは共有されますが、参照自体は独立したコピーなので、厳密には「参照渡し」ではありませんね。記事の該当部分を修正させていただきます。
このような建設的なフィードバックをいただけて本当に助かります。ありがとうございました!🙏