Go言語入門:よくある質問 -Pointers and Allocation Vol.1-

スポンサーリンク
Go言語入門:よくある質問 -Pointers and Allocation Vol.1- ノウハウ
Go言語入門:よくある質問 -Pointers and Allocation Vol.1-
この記事は約18分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語のよくある質問 について解説しています。

スポンサーリンク

背景

Go言語を学んでいると「なんでこんな仕様になっているんだろう?」「他の言語と違うのはなぜ?」といった疑問が湧いてきませんか。Go言語の公式サイトにあるFAQページには、そんな疑問に対する開発チームからの丁寧な回答がたくさん載っているんです。ただ、英語で書かれているため読むのに少しハードルがあるのも事実で、今回はこのFAQを日本語に翻訳して、Go言語への理解を深めていけたらと思い、これを読んだ時の内容を備忘として残しました。

Pointers and Allocation

いつ関数パラメータは値渡しされるのですか?

Cファミリーのすべての言語と同様に、Goではすべてが値渡しされます。つまり、関数は常に渡されるもののコピーを取得します。まるで値をパラメータに代入する代入文があるかのようです。例えば、int値を関数に渡すとintのコピーが作られ、ポインタ値を渡すとポインタのコピーが作られますが、それが指すデータのコピーは作られません。(これがメソッドレシーバーにどのような影響を与えるかについては、後のセクションで議論します。)

マップとスライスの値はポインタのように動作します:それらは基底のマップまたはスライスデータへのポインタを含む記述子です。マップまたはスライスの値をコピーしても、それが指すデータはコピーされません。インターフェース値をコピーすると、インターフェース値に格納されているもののコピーが作られます。インターフェース値が構造体を保持している場合、インターフェース値をコピーすると構造体のコピーが作られます。インターフェース値がポインタを保持している場合、インターフェース値をコピーするとポインタのコピーが作られますが、再び、それが指すデータのコピーは作られません。

この議論は操作のセマンティクスについてであることに注意してください。実際の実装では、最適化がセマンティクスを変更しない限り、コピーを避けるための最適化を適用する場合があります。

解説

この節では、Go言語における関数パラメータの渡し方について、「すべてが値渡し」という重要な原則を説明しています。これはGo言語の動作を理解する上で必須の概念です。

基本的な値渡しの概念

プリミティブ型の値渡し

func demonstrateBasicValuePassing() {
    // 基本型の値渡し
    func modifyInt(x int) {
        x = 100  // 引数のコピーを変更
        fmt.Printf("関数内での x: %d\n", x)
    }
    
    original := 42
    fmt.Printf("関数呼び出し前: %d\n", original)
    modifyInt(original)
    fmt.Printf("関数呼び出し後: %d\n", original)  // 元の値は変更されない
    
    // 文字列の値渡し
    func modifyString(s string) {
        s = "changed"  // 引数のコピーを変更
        fmt.Printf("関数内での s: %s\n", s)
    }
    
    originalStr := "original"
    fmt.Printf("関数呼び出し前: %s\n", originalStr)
    modifyString(originalStr)
    fmt.Printf("関数呼び出し後: %s\n", originalStr)  // 元の値は変更されない
}

ポインタの値渡し

func demonstratePointerPassing() {
    // ポインタ自体も値渡し(ポインタのコピーが作られる)
    func modifyThroughPointer(ptr *int) {
        *ptr = 200  // ポインタが指す先の値を変更
        fmt.Printf("関数内でのポインタが指す値: %d\n", *ptr)
        fmt.Printf("関数内でのポインタのアドレス: %p\n", ptr)
    }
    
    func modifyPointerItself(ptr *int) {
        newValue := 300
        ptr = &newValue  // ポインタのコピーを変更(元のポインタには影響なし)
        fmt.Printf("関数内での新しいポインタが指す値: %d\n", *ptr)
    }
    
    original := 42
    originalPtr := &original
    
    fmt.Printf("元の値: %d\n", original)
    fmt.Printf("元のポインタのアドレス: %p\n", originalPtr)
    
    modifyThroughPointer(originalPtr)
    fmt.Printf("ポインタ経由の変更後: %d\n", original)  // 値が変更される
    
    modifyPointerItself(originalPtr)
    fmt.Printf("ポインタ自体の変更後: %d\n", original)  // 値は変更されない
}

配列 vs スライスの違い

配列の値渡し(完全コピー)

func demonstrateArrayPassing() {
    // 配列は完全にコピーされる
    func modifyArray(arr [3]int) {
        arr[0] = 999
        fmt.Printf("関数内での配列: %v\n", arr)
        fmt.Printf("関数内での配列のアドレス: %p\n", &arr)
    }
    
    originalArray := [3]int{1, 2, 3}
    fmt.Printf("元の配列: %v\n", originalArray)
    fmt.Printf("元の配列のアドレス: %p\n", &originalArray)
    
    modifyArray(originalArray)
    fmt.Printf("関数呼び出し後の配列: %v\n", originalArray)  // 変更されない
    
    // 大きな配列のパフォーマンスへの影響
    type LargeArray [1000000]int
    
    func processLargeArray(arr LargeArray) {
        // 全体がコピーされるため、メモリ使用量とコピー時間が大きい
        fmt.Printf("大きな配列を処理中... (完全コピー)\n")
    }
    
    largeArr := LargeArray{}
    processLargeArray(largeArr)  // 効率が悪い
    
    // ポインタを使った効率的な方法
    func processLargeArrayPtr(arr *LargeArray) {
        // ポインタのコピーのみ(8バイト on 64-bit)
        fmt.Printf("大きな配列を処理中... (ポインタ経由)\n")
    }
    
    processLargeArrayPtr(&largeArr)  // 効率的
}

スライスの「参照的」動作

func demonstrateSlicePassing() {
    // スライスは「記述子」のコピーが作られる
    func modifySliceContents(s []int) {
        s[0] = 999  // 基底配列の内容を変更
        fmt.Printf("関数内でのスライス: %v\n", s)
        fmt.Printf("関数内でのスライスヘッダのアドレス: %p\n", &s)
        fmt.Printf("関数内でのスライスが指すデータのアドレス: %p\n", &s[0])
    }
    
    func modifySliceHeader(s []int) {
        s = append(s, 4, 5, 6)  // 新しいスライスを作成(コピーを変更)
        fmt.Printf("関数内での append 後: %v\n", s)
    }
    
    originalSlice := []int{1, 2, 3}
    fmt.Printf("元のスライス: %v\n", originalSlice)
    fmt.Printf("元のスライスヘッダのアドレス: %p\n", &originalSlice)
    fmt.Printf("元のスライスが指すデータのアドレス: %p\n", &originalSlice[0])
    
    modifySliceContents(originalSlice)
    fmt.Printf("内容変更後のスライス: %v\n", originalSlice)  // 内容が変更される
    
    modifySliceHeader(originalSlice)
    fmt.Printf("ヘッダ変更後のスライス: %v\n", originalSlice)  // 長さは変更されない
}

マップの動作

func demonstrateMapPassing() {
    // マップも「記述子」のコピーが作られる
    func modifyMap(m map[string]int) {
        m["new"] = 999  // 基底のマップデータを変更
        fmt.Printf("関数内でのマップ: %v\n", m)
        fmt.Printf("関数内でのマップのアドレス: %p\n", &m)
    }
    
    func reassignMap(m map[string]int) {
        m = make(map[string]int)  // 新しいマップを作成(コピーを変更)
        m["reassigned"] = 777
        fmt.Printf("関数内での再代入後: %v\n", m)
    }
    
    originalMap := map[string]int{"a": 1, "b": 2}
    fmt.Printf("元のマップ: %v\n", originalMap)
    fmt.Printf("元のマップのアドレス: %p\n", &originalMap)
    
    modifyMap(originalMap)
    fmt.Printf("変更後のマップ: %v\n", originalMap)  // 内容が変更される
    
    reassignMap(originalMap)
    fmt.Printf("再代入後のマップ: %v\n", originalMap)  // 元のマップは変更されない
}

インターフェース値の動作

インターフェースのコピー動作

func demonstrateInterfacePassing() {
    type Shape interface {
        Area() float64
    }
    
    type Rectangle struct {
        Width, Height float64
    }
    
    func (r Rectangle) Area() float64 {
        return r.Width * r.Height
    }
    
    func (r *Rectangle) Scale(factor float64) {
        r.Width *= factor
        r.Height *= factor
    }
    
    // インターフェース値(構造体を保持)の場合
    func modifyInterfaceStruct(s Shape) {
        // インターフェース内の構造体のコピーを変更
        if rect, ok := s.(Rectangle); ok {
            rect.Width = 999  // コピーを変更(元には影響なし)
            fmt.Printf("関数内での構造体: %+v\n", rect)
        }
    }
    
    // インターフェース値(ポインタを保持)の場合
    func modifyInterfacePointer(s Shape) {
        // インターフェース内のポインタのコピーを取得
        if rect, ok := s.(*Rectangle); ok {
            rect.Width = 999  // ポインタが指す先を変更
            fmt.Printf("関数内でのポインタ経由構造体: %+v\n", *rect)
        }
    }
    
    rect := Rectangle{Width: 10, Height: 5}
    
    // 構造体をインターフェースとして渡す
    var shapeStruct Shape = rect
    fmt.Printf("元の構造体: %+v\n", rect)
    modifyInterfaceStruct(shapeStruct)
    fmt.Printf("構造体変更後: %+v\n", rect)  // 変更されない
    
    // ポインタをインターフェースとして渡す
    var shapePointer Shape = &rect
    fmt.Printf("ポインタ変更前: %+v\n", rect)
    modifyInterfacePointer(shapePointer)
    fmt.Printf("ポインタ変更後: %+v\n", rect)  // 変更される
}

構造体の値渡し

構造体のコピー動作

func demonstrateStructPassing() {
    type Person struct {
        Name string
        Age  int
        Tags []string  // スライス(参照的要素)を含む
    }
    
    func modifyPerson(p Person) {
        p.Name = "Modified"  // 構造体のコピーを変更
        p.Age = 999
        p.Tags[0] = "modified"  // スライスの内容は変更される
        fmt.Printf("関数内での構造体: %+v\n", p)
    }
    
    func modifyPersonPointer(p *Person) {
        p.Name = "Modified by pointer"  // ポインタ経由で元を変更
        p.Age = 888
        fmt.Printf("関数内でのポインタ経由構造体: %+v\n", *p)
    }
    
    person := Person{
        Name: "Alice",
        Age:  30,
        Tags: []string{"developer", "gopher"},
    }
    
    fmt.Printf("元の構造体: %+v\n", person)
    
    modifyPerson(person)
    fmt.Printf("値渡し後: %+v\n", person)  // Name, Age は変更されないがTagsは変更される
    
    modifyPersonPointer(&person)
    fmt.Printf("ポインタ渡し後: %+v\n", person)  // すべて変更される
}

実用的な例

効率的な関数設計

func demonstrateEfficientDesign() {
    type LargeData struct {
        Values [1000]float64
        Names  [1000]string
        Config map[string]interface{}
    }
    
    // 非効率:大きな構造体の値渡し
    func processDataInefficient(data LargeData) LargeData {
        // 全体がコピーされる(非効率)
        data.Values[0] = 999
        return data  // さらにコピーが発生
    }
    
    // 効率的:ポインタを使用
    func processDataEfficient(data *LargeData) {
        // ポインタのコピーのみ(8バイト)
        data.Values[0] = 999
    }
    
    // 読み取り専用の場合:ポインタを使用
    func analyzeDataReadOnly(data *LargeData) float64 {
        // データを変更しないが、コピーのオーバーヘッドを避ける
        sum := 0.0
        for _, value := range data.Values {
            sum += value
        }
        return sum
    }
    
    // 小さな構造体の場合:値渡しも適切
    type Point struct {
        X, Y float64
    }
    
    func distance(p1, p2 Point) float64 {
        // 小さな構造体なので値渡しでも問題ない
        dx := p1.X - p2.X
        dy := p1.Y - p2.Y
        return math.Sqrt(dx*dx + dy*dy)
    }
    
    largeData := LargeData{
        Config: make(map[string]interface{}),
    }
    
    // 効率的な処理
    processDataEfficient(&largeData)
    result := analyzeDataReadOnly(&largeData)
    
    fmt.Printf("Analysis result: %.2f\n", result)
    
    // 小さな構造体は値渡しで問題なし
    p1 := Point{X: 0, Y: 0}
    p2 := Point{X: 3, Y: 4}
    dist := distance(p1, p2)
    fmt.Printf("Distance: %.2f\n", dist)
}

メソッドレシーバーへの影響

func demonstrateMethodReceivers() {
    type Counter struct {
        value int
    }
    
    // 値レシーバー:レシーバーのコピーを取得
    func (c Counter) IncrementValue() {
        c.value++  // コピーを変更(元には影響なし)
        fmt.Printf("値レシーバー内: %d\n", c.value)
    }
    
    // ポインタレシーバー:レシーバーのポインタのコピーを取得
    func (c *Counter) IncrementPointer() {
        c.value++  // ポインタ経由で元を変更
        fmt.Printf("ポインタレシーバー内: %d\n", c.value)
    }
    
    // 値を返すメソッド
    func (c Counter) GetValue() int {
        return c.value
    }
    
    counter := Counter{value: 0}
    
    fmt.Printf("初期値: %d\n", counter.GetValue())
    
    counter.IncrementValue()
    fmt.Printf("値レシーバー後: %d\n", counter.GetValue())  // 変更されない
    
    counter.IncrementPointer()
    fmt.Printf("ポインタレシーバー後: %d\n", counter.GetValue())  // 変更される
}

コンパイラ最適化

最適化の例

func demonstrateOptimizations() {
    fmt.Println("コンパイラ最適化について:")
    fmt.Println("- セマンティクスを変更しない範囲での最適化")
    fmt.Println("- 不要なコピーの省略")
    fmt.Println("- インライン化による最適化")
    fmt.Println("- エスケープ解析による最適化")
    
    // エスケープ解析の例
    func createLocal() *int {
        x := 42  // 通常はスタックに配置されるが、
        return &x  // ポインタを返すためヒープに移動される
    }
    
    func useLocal() {
        y := 42  // ローカルでのみ使用されるためスタックに配置
        fmt.Printf("Local value: %d\n", y)
    }
    
    ptr := createLocal()
    fmt.Printf("Escaped value: %d\n", *ptr)
    
    useLocal()
    
    fmt.Println("\n最適化を確認するコマンド:")
    fmt.Println("go build -gcflags=-m main.go  # エスケープ解析の結果を表示")
}

Go言語の「すべて値渡し」という原則は一貫性があり理解しやすいものですが、スライス、マップ、インターフェースの「記述子」的な性質により、実際の動作は複雑になることがあります。この動作を正しく理解することで、効率的で予測可能なコードを書くことができます。

おわりに 

本日は、Go言語のよくある質問について解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました