ポインタ(Pointers) - Golang learning step 2-11
- 公開日
- カテゴリ:GoingDeeper
- タグ:Golang,roadmap.sh,学習メモ
roadmap.sh > Go > Going Deeper > Pointers の学習を進めていきます。
※ 学習メモとしての記録ですが、後にこのセクションを学ぶ道しるべとなるよう、ですます調で記載しています。
contents
- 開発環境
- 参考 URL
- Go のポインタ(Pointers)
- ポインターの基本概念
- ポインターを使った値の変更
- 関数とポインター
- 構造体とポインター
- ポインターの使いどころ
- 注意点
- ベストプラクティス
開発環境
- チップ: Apple M2 Pro
- OS: macOS Sonoma
- go version: go1.23.2 darwin/arm64
参考 URL
Go のポインタ(Pointers)
Go のポインターは、値のメモリアドレスを格納する特殊な変数で、プログラム中で効率的にデータを操作するために使われます。
ポインターの基本概念
Go では以下の演算子を使用します。
&
: アドレス演算子 - 変数のメモリアドレスを取得*
: 間接参照演算子 - ポインターを通じて値にアクセス
基本的な使用例を見てみましょう。
package main
import "fmt"
func main() {
// 通常の変数
x := 10
// ポインター変数の宣言 (*int は int型を指すポインター)
var p *int
// x のアドレスを p に格納
p = &x // x のメモリアドレスを取得して p に格納
// ポインターを通じて x の値を変更
*p = 20
fmt.Printf("x の値: %d\n", x) // 20
fmt.Printf("p の値(アドレス): %v\n", p) // メモリアドレス
fmt.Printf("p の指す値: %d\n", *p) // 20
}
// 出力:
// x の値: 20
// p の値(アドレス): 0x1400000e0d0
// p の指す値: 20
ポインターを使った値の変更
ポインターを使用して変数の値を直接変更できます。
package main
import "fmt"
func main() {
x := 42
p := &x
*p = 100 // ポインターを通じて値を変更
fmt.Println(x) // 100
}
関数とポインター
ポインターを関数の引数に渡すと、関数内で元の変数の値を変更できます。
package main
import "fmt"
func updateValue(p *int) {
*p = 99 // ポインターを使って値を更新
}
func main() {
x := 42
updateValue(&x) // x のアドレスを渡す
fmt.Println(x) // 99
}
構造体とポインター
構造体でのポインターの使用は特に一般的です。
type Person struct {
Name string
Age int
}
func (p *Person) Birthday() {
p.Age++ // (*p).Age++ と同じ
}
func main() {
person := &Person{
Name: "田中",
Age: 25,
}
person.Birthday()
fmt.Printf("年齢: %d\n", person.Age) // 26
}
Birthday メソッドではポインターレシーバーを使用しており、呼び出し元の構造体フィールドを直接変更しています。
ポインターの使いどころ
- 大きなデータ構造を効率的に操作
- ポインターを使うことで、値のコピーを避けられ、メモリ使用量を削減できます。
- 関数間で値を共有
- ポインターを渡すことで、関数内で変更した内容を呼び出し元に反映できます。
- 構造体の変更
- 構造体を関数で操作する際にポインターを渡すと、元のデータを変更できます。
注意点
1. nil ポインター
ポインターが何も指していない場合、デフォルト値は nil です。nil を参照しようとするとランタイムエラーになります。
var p *int
if p == nil {
fmt.Println("p is nil") // panic: runtime error: invalid memory address or nil pointer dereference
}
2. メモリリーク
Go はガーベジコレクションを持つため、C 言語のような手動のメモリ管理は不要ですが、以下のような場合に注意が必要です。
- 大きなオブジェクトへのポインターを不必要に保持し続ける
- goroutineが完了しないままポインターを保持している
- 循環参照の作成
3. スライスとマップ
スライスとマップは内部的にポインターを使用しているため、通常はポインターとして渡す必要はありません
// スライスは参照型なので、ポインターは不要
func modifySlice(s []int) {
s[0] = 100
}
func main() {
slice := []int{1, 2, 3}
modifySlice(slice)
fmt.Println(slice[0]) // 100
}
スライスは Go において参照型と呼ばれるデータ構造の一種で、スライス自身が内部的にポインタを持っています。そのため、スライスを関数に渡すとポインタを渡した場合と同じ挙動になります。
また、これはマップも同様です。
// マップも参照型なので、ポインターは不要
func modifyMap(m map[string]int) {
m["key"] = 100
}
func main() {
m := map[string]int{"key": 1}
modifyMap(m)
fmt.Println(m["key"]) // 100
}
ベストプラクティス
- 適切な使用場面の判断
- 大きな構造体を渡す場合
- 値の変更が必要な場合
- メソッドレシーバーとして使用する場合
type Circle struct {
Radius float64
}
func (c *Circle) Scale(factor float64) {
c.Radius *= factor
}
func main() {
c := &Circle{Radius: 5}
c.Scale(2)
fmt.Println(c.Radius) // 10
}
- シンプルさの重視
- 必要でない限り、ポインターの使用は避ける
- 特に小さな基本型の場合は値渡しを優先
- 安全性の確保
- ポインター使用前の nil チェック
- 適切なスコープ管理
Go のポインターは、C 言語などと比べて比較的安全に設計されていますが、適切な理解と使用が重要です。必要な場面で適切に使用することで、プログラムの効率と可読性を向上させることができます。
まとめ
- Go のポインタは値のメモリアドレスを操作するための重要な機能
&
と*
演算子を使ってアドレスの取得と値の操作が可能- ポインタを使うことで関数間で値を共有し、効率的なデータ操作が可能
- 構造体とポインタを組み合わせることで、データの直接操作が可能
- スライスやマップは参照型のため、通常ポインタを明示的に渡す必要がない
- 適切な場面でポインタを使うことで、メモリ使用量の削減やコードの効率化が可能
[Next] Step 2-12: モジュール(Modules)
[Prev] Step 2-10: ジェネリクス(Generics)