🐰

Go言語でのポインタについて

2025/02/13に公開3

はじめに

普段Pythonをメインで使っているので、ポインタという概念にあまり馴染みがなかったので、改めてGo言語のポインタについてまとめてみます。
間違っていたらすいません。

そもそもGo言語はポインタを必要とするのか?

Go は、設計上、C や C++ のようにポインタを頻繁に直接操作しなくても済むようになっています。たとえば、

  • 自動メモリ管理: ガーベジコレクションにより、メモリの解放や割当てを手動で行う必要がありません。
  • 高レベルなデータ構造: スライス、マップ、チャネルなどが用意され、低レベルのポインタ操作を回避できます。

ただし、必要に応じてポインタは使えるようになっており、構造体を柔軟に扱えたり、効率的なデータ処理のために利用されます。また、Go のポインタは安全性を重視しており、C/C++ のようなポインタ演算はできない設計となっています。

つまり、Go は「基本的にはポインタを使わなくても済む」ように設計されていますが、必要な場合はポインタを適切に利用できる柔軟性も持っていると考えられます。

なぜGo言語でポインタを使うのか

Go では、構造体を扱うときにデフォルトで値渡し(コピー)が行われます。しかし、大きな構造体や複雑なデータを扱う場合、コピーのコストがパフォーマンスに影響するため、ポインタ渡しを使うことで効率的なメモリ管理と柔軟な操作が可能になります。

メモリの効率化(データのコピーを防ぐ)

値渡し(コピーが発生する)
値として渡すと、関数呼び出しごとにデータがコピーされてしまいます。

type User struct {
    ID       int
    Name     string
    Email    string
    Password string
}

func PrintUser(user User) {
    fmt.Println(user.Name) // user はコピーされる
}

✅ ポインタ渡し(コピーを防ぐ)
ポインタを渡すと、参照だけが渡されるのでコピーのコストを回避できます。

func PrintUser(user *User) {
    fmt.Println(user.Name) // user はコピーされず、元のデータを参照
}

nil を使った状態表現

❌ 値渡しでは「データなし」を明確に表現できない
データが存在しない場合、空の値(User{})を返すしかなく、意図が不明瞭になります。

func GetUser(id int) (User, error) {
    var user User
    if id != 1 {
        return User{}, fmt.Errorf("user not found") // 空の User を返すしかない
    }
    return user, nil
}

✅ ポインタなら nil を返して「データなし」を明示
ポインタを使えば、存在しない場合に nil を返すことで状態を明確に表現できます。

func GetUser(id int) (*User, error) {
    if id != 1 {
        return nil, fmt.Errorf("user not found") // nil を返す
    }
    return &User{ID: id, Name: "Alice"}, nil
}

関数内での値の変更が呼び出し元に反映される

❌ 値渡しでは変更が反映されない
関数内で値を変更しても、コピーされているため元のデータは変わりません。

func UpdateName(user User, newName string) {
    user.Name = newName // user はコピーなので元のデータには影響しない
}

u := User{ID: 1, Name: "Alice"}
UpdateName(u, "Bob")
fmt.Println(u.Name) // "Alice" のまま

✅ ポインタ渡しなら変更がそのまま反映される
ポインタを渡すことで、関数内での変更が直接元のデータに反映されます。

func UpdateName(user *User, newName string) {
    user.Name = newName // user は参照されているので、元のデータが更新される
}

u := &User{ID: 1, Name: "Alice"}
UpdateName(u, "Bob")
fmt.Println(u.Name) // "Bob"

スライスやマップとの一貫性

Go のスライス([]T)やマップ(map[K]V)は参照型であり、関数に渡すと変更が反映されます。しかし、構造体はデフォルトで値渡しとなるため、ポインタを使わないと同様の動作にはなりません。

値渡しの場合(変更が反映されない)

func ModifyStruct(u User) {
    u.Name = "Bob"
}

user := User{ID: 1, Name: "Alice"}
ModifyStruct(user)
fmt.Println(user.Name) // "Alice"(変更されない)

ポインタを使えば変更が反映される

func ModifyStruct(u *User) {
    u.Name = "Bob"
}

user := &User{ID: 1, Name: "Alice"}
ModifyStruct(user)
fmt.Println(user.Name) // "Bob"

インターフェースとの相性

インターフェースを実装する際、値レシーバーでは期待通りに動作しないケースもあります。
ポインタレシーバーを用いることで、柔軟な実装が可能になります。

❌ 値レシーバーの場合(インターフェースを満たさない場合がある)

type Printer interface {
    Print()
}

type User struct {
    Name string
}

func (u User) Print() { // 値レシーバー
    fmt.Println(u.Name)
}
var p Printer = User{"Alice"} //コンパイルエラーになる可能性がある

✅ ポインタレシーバーの場合

func (u *User) Print() { // ポインタレシーバー
    fmt.Println(u.Name)
}

var p Printer = &User{"Alice"} // *User 型でインターフェースを満たす
p.Print() // "Alice"

まとめ

Go では、構造体などのデータを扱う際に *T(ポインタ)を使うのが一般的です。
その理由は以下の通りです:

理由 値渡し(T) ポインタ渡し(*T)
メモリ効率 データがコピーされる コピーを防ぎ、メモリの使用を最適化
nil の活用 空の値(例:User{})しか返せない nil を返すことで「データなし」を明示
関数内の変更反映 変更がコピーに留まる 呼び出し元のデータが直接更新される
スライス・マップとの一貫性 値渡しのため変更が反映されない場合がある ポインタを使えば参照と同じように変更が伝播
インターフェースの実装 値レシーバーだと満たせない場合がある ポインタレシーバーなら柔軟に実装できる

結論:
Go は、ポインタを「使わなくても済む」設計ですが、メモリ効率の向上、状態の明示、関数内での変更伝播、そしてインターフェースの柔軟な実装などの理由から、あえてポインタを利用する方針になっています。

BIDIRE

Discussion

junerjuner

ただし、必要に応じてポインタは使えるようになっており、構造体の参照渡しや効率的なデータ処理のために利用されます。

ポインタにより 元の変数を 上書きするには 元の変数への代入と同様の構文では上書きできない (別途変数にアクセスする構文が必要な)為 参照渡しでいうところのエイリアスは満たせないので参照渡しにはならないのではないでしょうか?

ハルちんハルちん

ご指摘の通りだと思います。

厳密な参照渡しにはならないです。
適切な内容に変更いたします。

ご指摘ありがとうございます。