メソッドとは?
これまでのステップで、データのかたまりである「構造体(struct)」について学びましたね。メソッドは、特定の型(今回の場合は構造体)に関連付けられた関数のことです。オブジェクト指向プログラミングにおける「メソッド」と考え方は似ています。
構造体(データ)にメソッド(振る舞い)を定義することで、データとその操作をまとめて管理しやすくなり、コードが整理され、再利用性が高まります。✨
メソッドの定義方法
メソッドは通常の関数定義と似ていますが、func
キーワードと関数名の間に「レシーバー(receiver)」と呼ばれる引数を追加します。
func (レシーバー変数 レシーバー型) メソッド名(引数リスト) (戻り値リスト) {
// メソッドの処理
}
レシーバーは、どの型のメソッドであるかを指定する部分です。レシーバー変数を使って、メソッド内でその型のフィールドにアクセスできます。
レシーバーには主に2つの種類があります。
- 値レシーバー (Value Receiver):
(v 型名)
のように、型名をそのまま指定します。メソッド内ではレシーバーのコピーが渡されます。 - ポインタレシーバー (Pointer Receiver):
(p *型名)
のように、型名の前に*
をつけます。メソッド内ではレシーバーのポインタ(メモリアドレス)が渡されます。
どちらを使うかは重要なポイントです。次のセクションで具体例を見ていきましょう。
値レシーバーの例
まず、値レシーバーを使ったメソッドの例を見てみましょう。長方形(Rectangle)構造体を定義し、その面積を計算するメソッドを追加します。
package main
import "fmt"
// 長方形を表す構造体
type Rectangle struct {
Width float64
Height float64
}
// 値レシーバーを持つメソッド (面積を計算)
// レシーバー変数 `r` は Rectangle 型
func (r Rectangle) Area() float64 {
// r.Width や r.Height のようにフィールドにアクセスできる
return r.Width * r.Height
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
fmt.Printf("長方形の幅: %.2f, 高さ: %.2f\n", rect.Width, rect.Height) // 出力: 長方形の幅: 10.00, 高さ: 5.00
// メソッド呼び出し
area := rect.Area()
fmt.Printf("面積: %.2f\n", area) // 出力: 面積: 50.00
// 値レシーバーのメソッド内でレシーバーのフィールドを変更しようとしても...
// (仮に Area メソッド内で r.Width = 20 としても...)
// main 関数内の rect の値は変わらない(コピーに対して操作するため)
fmt.Printf("Areaメソッド呼び出し後の幅: %.2f\n", rect.Width) // 出力: Areaメソッド呼び出し後の幅: 10.00
}
Area
メソッドはRectangle
型の値レシーバーr
を持っています。このメソッドはr
のフィールド(Width
, Height
)を読み取って面積を計算しますが、r
自体はrect
変数のコピーなので、Area
メソッド内でr
のフィールドを変更しても、元のrect
には影響しません。✅
ポインタレシーバーの例
次に、ポインタレシーバーを使った例です。長方形のサイズを指定した倍率で変更するメソッドScale
を追加します。この操作は元の長方形オブジェクト自体を変更する必要があるため、ポインタレシーバーが適しています。
package main
import "fmt"
// 長方形を表す構造体
type Rectangle struct {
Width float64
Height float64
}
// 値レシーバーを持つメソッド (面積を計算)
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// ポインタレシーバーを持つメソッド (サイズを変更)
// レシーバー変数 `r` は *Rectangle 型 (Rectangleへのポインタ)
func (r *Rectangle) Scale(factor float64) {
// ポインタレシーバーなので、フィールドの変更が元のオブジェクトに反映される
r.Width = r.Width * factor
r.Height = r.Height * factor
fmt.Printf("Scaleメソッド内: 幅 %.2f, 高さ %.2f\n", r.Width, r.Height)
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
fmt.Printf("初期状態: 幅 %.2f, 高さ %.2f\n", rect.Width, rect.Height) // 出力: 初期状態: 幅 10.00, 高さ 5.00
// ポインタレシーバーのメソッド呼び出し
// Goでは、構造体のポインタでなくても、暗黙的にポインタが渡される
rect.Scale(2.0)
// fmt.Printf("Scaleメソッド内...") がここで出力される
fmt.Printf("Scale後: 幅 %.2f, 高さ %.2f\n", rect.Width, rect.Height) // 出力: Scale後: 幅 20.00, 高さ 10.00
// 面積も変わっている
fmt.Printf("Scale後の面積: %.2f\n", rect.Area()) // 出力: Scale後の面積: 200.00
}
Scale
メソッドは*Rectangle
型のポインタレシーバーr
を持っています。メソッド内でr.Width
やr.Height
を変更すると、呼び出し元のrect
変数が指す実際の構造体の値が変更されます。⬆️
🤔 補足: Goでは、rect.Scale(2.0)
のように構造体変数からポインタレシーバーメソッドを呼び出すと、Goが自動的に(▭).Scale(2.0)
のように解釈してくれます。逆(ポインタ変数から値レシーバーメソッドを呼び出す)も同様です。便利ですね!
値レシーバー vs ポインタレシーバー
どちらのレシーバーを使うべきか、判断基準をまとめます。
種類 | レシーバー型 | 主な目的 | メリット | デメリット | 使い所 |
---|---|---|---|---|---|
値レシーバー | (v 型名) | レシーバーの値を変更しないメソッド | 元の値が変更されないことが保証される | 大きな構造体の場合、コピーのコストがかかる | ・値を読み取るだけのメソッド (例: Area() )・レシーバーの不変性を保証したい場合 |
ポインタレシーバー | (p *型名) | レシーバーの値を変更するメソッド | ・元の値を変更できる ・大きな構造体でもコピーコストが小さい(ポインタのみコピー) | 意図せず元の値を変更してしまう可能性がある | ・フィールドの値を変更する必要があるメソッド (例: Scale() )・大きな構造体で、コピーのオーバーヘッドを避けたい場合 ・nilレシーバーを扱いたい場合(メソッド内でnilチェックが必要) |
一般的なガイドライン:
- メソッドがレシーバーのフィールドを変更する必要がある場合は、ポインタレシーバーを使用します。
- メソッドがレシーバーを変更する必要がない場合でも、構造体が大きい、または将来的に変更が必要になる可能性がある場合は、ポインタレシーバーを検討します(コピーのコスト削減、一貫性のため)。
- 非常に小さな構造体で、不変性が重要な場合は、値レシーバーが良い選択肢となることがあります。
- 一つの型に対するメソッド群では、レシーバーの種類(値かポインタか)を一貫させることが推奨されます。混在させると混乱を招く可能性があります。迷ったらポインタレシーバーを選ぶことが多いです。
まとめ
今回は、構造体にメソッドを定義する方法と、値レシーバー・ポインタレシーバーの違いについて学びました。メソッドを使うことで、データ(構造体)とそれに関連する操作(メソッド)をカプセル化し、よりオブジェクト指向に近い形でコードを構成できます。
レシーバーの選択はGoの重要な概念の一つです。実際にコードを書きながら、それぞれの挙動を確認してみてくださいね! 💪
次のステップでは、構造体とメソッドに関連して、ポインタと参照についてさらに詳しく見ていきます。
コメント