値渡し vs 参照渡し
CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 21/107。
Goは値渡しの言語です。関数に変数を渡すと、Goはその値のコピーを作成します。つまり、関数内での変更は元の変数には影響しません。
func double(n int) {
n = n * 2
}
func main() {
x := 10
double(x)
fmt.Println(x) // 10 (変更なし)
}構造体についても同様です。構造体を関数に渡すと、そのすべてのフィールドがコピーされるため、関数内での変更は失われます。
type Person struct {
Name string
Age int
}
func birthday(p Person) {
p.Age++
}
func main() {
alice := Person{Name: "Alice", Age: 30}
birthday(alice)
fmt.Println(alice.Age) // 30 (変更なし)
}元の値を変更するには、代わりにポインタを渡します。ポインタ自体はコピーされますが、それでも同じメモリ番地を指しているため、元のデータを変更することができます。
func birthday(p *Person) {
p.Age++
}
func main() {
alice := Person{Name: "Alice", Age: 30}
birthday(&alice)
fmt.Println(alice.Age) // 31 (変更されました!)
}元の値を変更する必要がある場合や、コストの高いコピーを避けるために大きな構造体を渡す場合には、ポインタを使用してください。意図しない変更から元のデータを保護したい場合は、値を使用してください。
チャレンジ
簡単構造体を値渡しする場合とポインタ渡しする場合の違いを実証するショッピングカートシステムを構築しましょう。ポインタ引数だけが元のデータを変更できることを、実際に確認してください。
コードは2つのファイルに分けて構成します:
cart.go:Name(string)、Price(float64)、Quantity(int) フィールドを持つCartItem構造体を定義します。数量の更新を試みる2つの関数を作成してください:UpdateQuantityByValueはCartItem(値渡し) と新しい数量を受け取り、アイテムの数量を新しい値に設定します。UpdateQuantityByPointerは*CartItem(ポインタ渡し) と新しい数量を受け取り、アイテムの数量を新しい値に設定します。
TotalメソッドをCartItemに作成してください。main.go: 入力からアイテムの詳細を読み取り、CartItem を作成し、両方の更新アプローチを実証します。まず値渡し関数で更新を試みて結果を表示し、次にポインタ渡し関数で更新して再度結果を表示します。
以下の入力が提供されます:
- 1行目: アイテム名
- 2行目: 価格 (float)
- 3行目: 初期の数量 (integer)
- 4行目: 設定する新しい数量 (integer)
出力形式:
Initial: [Name] x[Quantity] = $[Total]
After value update: [Name] x[Quantity] = $[Total]
After pointer update: [Name] x[Quantity] = $[Total]例えば、Laptop、999.99、1、3 が与えられた場合、出力は以下のようになります:
Initial: Laptop x1 = $999.99
After value update: Laptop x1 = $999.99
After pointer update: Laptop x3 = $2999.97値渡しによる更新の後では数量が 1 のまま(関数がコピーを修正したため)ですが、ポインタ渡しによる更新の後では 3 に変わっている(関数がオリジナルを修正したため)ことに注目してください。合計金額は小数点以下2桁でフォーマットしてください。
チートシート
Goは**値渡し**(pass-by-value)の言語です。変数を関数に渡すと、Goはその値のコピーを作成するため、関数内での変更は元の変数に影響を与えません。
func double(n int) {
n = n * 2
}
func main() {
x := 10
double(x)
fmt.Println(x) // 10 (変更なし)
}これは構造体にも当てはまります。構造体を関数に渡すと、そのすべてのフィールドがコピーされます:
type Person struct {
Name string
Age int
}
func birthday(p Person) {
p.Age++
}
func main() {
alice := Person{Name: "Alice", Age: 30}
birthday(alice)
fmt.Println(alice.Age) // 30 (変更なし)
}元の値を変更するには、**ポインタ**を渡します。ポインタ自体はコピーされますが、それでも同じメモリ番地を指しています:
func birthday(p *Person) {
p.Age++
}
func main() {
alice := Person{Name: "Alice", Age: 30}
birthday(&alice)
fmt.Println(alice.Age) // 31 (変更されました!)
}元の値を変更する必要がある場合や、大きな構造体を渡す際に高コストなコピーを避ける場合にはポインタを使用します。意図しない変更から元のデータを保護したい場合には値を使用します。
自分で試してみよう
package main
import (
"fmt"
)
func main() {
// 入力を読み込む
var name string
var price float64
var initialQty, newQty int
fmt.Scanln(&name)
fmt.Scanln(&price)
fmt.Scanln(&initialQty)
fmt.Scanln(&newQty)
// TODO: 入力値を使用して CartItem を作成する
// TODO: 次の形式を使用して初期状態を出力する:
// fmt.Printf("Initial: %s x%d = $%.2f\n", ...)
// TODO: UpdateQuantityByValue を使用して数量の更新を試みる
// その後、結果を出力する(数量が変わらないことに注意)
// TODO: UpdateQuantityByPointer を使用して数量を更新する
// その後、結果を出力する(数量が変わることに注意)
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
1Go オブジェクト指向の基礎
外部ファイルGo ワークスペースとモジュールパッケージとインポート公開された名前と非公開の名前Go におけるオブジェクト指向入門クラスとしての構造体構造体へのメソッド定義ポインタレシーバと値レシーバ構造体の初期化コンストラクタ関数まとめ:簡易計算機8エラー処理とOOP
error インターフェースカスタムエラー型エラーラッピング (fmt.Errorf)センチネルエラーerrors.Is() と errors.As()Panic、Defer、Recover復習 - ファイルパーサー