メソッド
レッスン8:メソッド
現実世界のアナロジー
車を買ったと想像してください。車にはさまざまな機能があります — 始動、加速、ブレーキ、ライト点灯。これらの機能は独立して存在するのではなく、車に属しています。「始動」は一般的な動作ではなく、「車が始動した」と言います。
Goでは、メソッドとは特定の型に結びついた関数です。「始動」が「車」に属するように、メソッドは結びついた型に属します。冷蔵庫に始動機能を搭載しないのと同じように、メソッドは本当に奉仕する型に定義されます。
コアコンセプト
メソッドとは?
メソッドとはレシーバを持つ関数です。レシーバはメソッドを型に結びつけ、その型の変数が直接メソッドを呼び出せるようにします。
レシーバ
レシーバはメソッドと型の間の架け橋です。その構文は func キーワードとメソッド名の間に現れます。
func (レシーバ変数 レシーバ型) メソッド名(パラメータ) 戻り値型 {
// メソッド本体
}
値レシーバとポインタレシーバの比較
| 特性 | 値レシーバ func (s Struct) |
ポインタレシーバ func (s *Struct) |
|---|---|---|
| コピーに対して操作 | はい(変更は元に影響しない) | いいえ(元を直接変更) |
| 使用場面 | 読み取り専用操作、小さな構造体 | フィールドの変更が必要、大きな構造体 |
| インターフェースの実装 | 値とポインタの両方で呼び出し可能 | ポインタ型でのみ呼び出し可能 |
メソッドセットのルール
- 値型のメソッドセット:値レシーバのメソッドのみを含む
- ポインタ型のメソッドセット:値レシーバとポインタレシーバの両方のメソッドを含む
つまり、*T 型の変数は T と *T の両方のすべてのメソッドを呼び出すことができますが、T 型の変数は T のメソッドのみを呼び出すことができます。
継承より合成
Goにはクラスの継承がありません。代わりに、合成(埋め込み)を通じてコードの再利用を実現します。ある構造体を別の構造体に埋め込むと、埋め込まれた構造体のメソッドが自動的に外側の構造体に「プロモーション」されます。
基本構文と使い方
メソッドの定義
package main
import "fmt"
// 構造体を定義
type Rectangle struct {
Width float64
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 main() {
rect := Rectangle{Width: 10, Height: 5}
fmt.Println("面積:", rect.Area()) // 出力: 面積: 50
rect.Scale(2)
fmt.Println("拡大後の面積:", rect.Area()) // 出力: 拡大後の面積: 200
}
メソッドと関数の違い
// これは関数で、どの型にも結びつかない
func Area(r Rectangle) float64 {
return r.Width * r.Height
}
// これはメソッドで、Rectangle型に結びついている
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
埋め込み構造体によるメソッドの継承
package main
import "fmt"
// ベース型
type Animal struct {
Name string
}
// Animal のメソッド
func (a Animal) Speak() string {
return a.Name + "が鳴きました"
}
// Animal を埋め込む型
type Dog struct {
Animal // 埋め込み、フィールド名は不要
Breed string
}
// Dog 独自のメソッド
func (d Dog) Bark() string {
return d.Name + "が吠えました!"
}
func main() {
dog := Dog{
Animal: Animal{Name: "Rex"},
Breed: "ゴールデンレトリバー",
}
// 埋め込まれた型のメソッドを直接呼び出し可能
fmt.Println(dog.Speak()) // 出力: Rexが鳴きました
fmt.Println(dog.Bark()) // 出力: Rexが吠えました!
}
Area()、Scale()、Save()、IsValid()。
func (rect Rectangle) よりも func (r Rectangle) が推奨されます。これはGoの慣例です。
sync.Mutex など)が含まれている場合、または構造体が大きい場合は、常にポインタレシーバを使用してください。
サンプルコード
例:基本的なメソッド定義(難易度⭐)
Circle 構造体を定義し、面積と周囲の長さを計算するメソッドを実装します。
package main
import (
"fmt"
"math"
)
// Circle 構造体
type Circle struct {
Radius float64
}
// Area は面積を計算する(値レシーバ、読み取り専用操作)
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
// Perimeter は周囲の長さを計算する
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
// SetRadius は新しい半径を設定する(ポインタレシーバ、フィールドを変更)
func (c *Circle) SetRadius(r float64) {
if r > 0 {
c.Radius = r
}
}
func main() {
c := Circle{Radius: 5}
fmt.Printf("半径: %.2f\n", c.Radius)
fmt.Printf("面積: %.2f\n", c.Area())
fmt.Printf("周囲の長さ: %.2f\n", c.Perimeter())
c.SetRadius(10)
fmt.Printf("\n新しい半径: %.2f\n", c.Radius)
fmt.Printf("新しい面積: %.2f\n", c.Area())
fmt.Printf("新しい周囲の長さ: %.2f\n", c.Perimeter())
}
出力:
半径: 5.00
面積: 78.54
周囲の長さ: 31.42
新しい半径: 10.00
新しい面積: 314.16
新しい周囲の長さ: 62.83
例:値レシーバとポインタレシーバの違い(難易度⭐⭐)
両者の主要な動作の違いを実演します。
package main
import "fmt"
// Account は銀行口座
type Account struct {
Owner string
Balance float64
}
// 値レシーバ:残高を取得(読み取り専用、変更なし)
func (a Account) GetBalance() float64 {
return a.Balance
}
// ポインタレシーバ:入金(残高の変更が必要)
func (a *Account) Deposit(amount float64) {
if amount > 0 {
a.Balance += amount
fmt.Printf(" %.2f入金、残高: %.2f\n", amount, a.Balance)
}
}
// ポインタレシーバ:出金
func (a *Account) Withdraw(amount float64) bool {
if amount > 0 && a.Balance >= amount {
a.Balance -= amount
fmt.Printf(" %.2f出金、残高: %.2f\n", amount, a.Balance)
return true
}
fmt.Printf(" %.2fの出金に失敗、残高不足\n", amount)
return false
}
// 値レシーバ:口座情報をフォーマット
func (a Account) String() string {
return fmt.Sprintf("口座[%s] 残高: %.2f", a.Owner, a.Balance)
}
func main() {
acc := Account{Owner: "Zhang San", Balance: 1000}
fmt.Println(acc)
fmt.Println("\n--- 取引履歴 ---")
acc.Deposit(500)
acc.Withdraw(200)
acc.Withdraw(2000) // 残高不足
fmt.Println("\n--- 最終状態 ---")
fmt.Println(acc)
fmt.Printf("現在の残高: %.2f\n", acc.GetBalance())
}
出力:
口座[Zhang San] 残高: 1000.00
--- 取引履歴 ---
500.00入金、残高: 1500.00
200.00出金、残高: 1300.00
2000.00の出金に失敗、残高不足
--- 最終状態 ---
口座[Zhang San] 残高: 1300.00
現在の残高: 1300.00
例:継承より合成(難易度⭐⭐⭐)
埋め込み構造体によるメソッドの継承とメソッドのオーバーライドを実演します。
package main
import "fmt"
// ==================== ベース層 ====================
// Base 構造体(「親クラス」に類似)
type Base struct {
ID int
Name string
}
// Describe は基本的な説明を返す
func (b Base) Describe() string {
return fmt.Sprintf("Base[ID=%d, Name=%s]", b.ID, b.Name)
}
// Identify は識別情報を返す
func (b Base) Identify() string {
return fmt.Sprintf("私は%sです(ID: %d)", b.Name, b.ID)
}
// ==================== ビジネス層 ====================
// Employee は Base を埋め込む
type Employee struct {
Base // 埋め込み、Base のメソッドを継承
Department string
Salary float64
}
// Base の Describe メソッドをオーバーライド
func (e Employee) Describe() string {
return fmt.Sprintf("Employee[ID=%d, Name=%s, Dept=%s, Salary=%.0f]",
e.ID, e.Name, e.Department, e.Salary)
}
// Employee 独自のメソッド
func (e Employee) AnnualSalary() float64 {
return e.Salary * 12
}
// Manager は Employee を埋め込む(多階層の埋め込み)
type Manager struct {
Employee // Employee を埋め込み
TeamSize int
}
// Describe メソッドをオーバーライド
func (m Manager) Describe() string {
return fmt.Sprintf("Manager[ID=%d, Name=%s, Dept=%s, Team=%d人]",
m.ID, m.Name, m.Department, m.TeamSize)
}
// Manager 独自のメソッド
func (m Manager) TeamReport() string {
return fmt.Sprintf("%sは%d人のチームを管理しています", m.Name, m.TeamSize)
}
func main() {
// Base インスタンスを作成
b := Base{ID: 1, Name: "基本オブジェクト"}
fmt.Println("=== Base ===")
fmt.Println(b.Describe())
fmt.Println(b.Identify())
// Employee インスタンスを作成
emp := Employee{
Base: Base{ID: 2, Name: "Li Si"},
Department: "Engineering",
Salary: 15000,
}
fmt.Println("\n=== Employee ===")
fmt.Println(emp.Describe()) // Employee のオーバーライドされたメソッドを呼び出し
fmt.Println(emp.Identify()) // Base から継承
fmt.Printf("年間給与: %.0f\n", emp.AnnualSalary())
// Manager インスタンスを作成
mgr := Manager{
Employee: Employee{
Base: Base{ID: 3, Name: "Wang Wu"},
Department: "R&D",
Salary: 25000,
},
TeamSize: 8,
}
fmt.Println("\n=== Manager ===")
fmt.Println(mgr.Describe()) // Manager のオーバーライドされたメソッドを呼び出し
fmt.Println(mgr.Identify()) // 多階層の埋め込みにより Base から継承
fmt.Printf("年間給与: %.0f\n", mgr.AnnualSalary()) // Employee から継承
fmt.Println(mgr.TeamReport()) // Manager 独自のメソッド
}
出力:
=== Base ===
Base[ID=1, Name=基本オブジェクト]
私は基本オブジェクトです(ID: 1)
=== Employee ===
Employee[ID=2, Name=Li Si, Dept=Engineering, Salary=15000]
私はLi Siです(ID: 2)
年間給与: 180000
=== Manager ===
Manager[ID=3, Name=Wang Wu, Dept=R&D, Team=8人]
私はWang Wuです(ID: 3)
年間給与: 300000
Wang Wuは8人のチームを管理しています
現実世界の使用場面
場面1:ユーザー認証システム
メソッドを使ってユーザー認証ロジックをカプセル化します。
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
)
// User 構造体
type User struct {
Username string
PasswordHash string
Email string
CreatedAt time.Time
IsActive bool
LoginCount int
}
// NewUser は新しいユーザーを作成する(コンストラクタパターン)
func NewUser(username, password, email string) *User {
return &User{
Username: username,
PasswordHash: hashPassword(password),
Email: email,
CreatedAt: time.Now(),
IsActive: true,
LoginCount: 0,
}
}
// hashPassword はパスワードをハッシュ化する(パッケージレベルの関数、メソッドではない)
func hashPassword(password string) string {
h := sha256.Sum256([]byte(password))
return hex.EncodeToString(h[:])
}
// VerifyPassword はパスワードを検証する
func (u *User) VerifyPassword(password string) bool {
return u.PasswordHash == hashPassword(password)
}
// Login はユーザーログイン
func (u *User) Login(password string) error {
if !u.IsActive {
return fmt.Errorf("アカウント%sは無効になっています", u.Username)
}
if !u.VerifyPassword(password) {
return fmt.Errorf("パスワードが正しくありません")
}
u.LoginCount++
return nil
}
// Deactivate はアカウントを無効にする
func (u *User) Deactivate() {
u.IsActive = false
}
// Info はユーザー情報を返す
func (u User) Info() string {
status := "有効"
if !u.IsActive {
status = "無効"
}
return fmt.Sprintf("[%s] %s | メール: %s | 状態: %s | ログイン回数: %d",
u.Username, u.Username, u.Email, status, u.LoginCount)
}
func main() {
user := NewUser("zhangsan", "mySecret123", "zhangsan@example.com")
fmt.Println("ユーザーを作成しました:")
fmt.Println(user.Info())
// 正しいパスワードでログイン
fmt.Println("\n正しいパスワードでログイン:")
err := user.Login("mySecret123")
if err != nil {
fmt.Println("ログイン失敗:", err)
} else {
fmt.Println("ログイン成功!")
}
fmt.Println(user.Info())
// 間違ったパスワードでログイン
fmt.Println("\n間違ったパスワードでログイン:")
err = user.Login("wrongPassword")
if err != nil {
fmt.Println("ログイン失敗:", err)
}
// アカウント無効化後のログイン試行
fmt.Println("\nアカウント無効化後のログイン試行:")
user.Deactivate()
err = user.Login("mySecret123")
if err != nil {
fmt.Println("ログイン失敗:", err)
}
fmt.Println(user.Info())
}
出力例:
ユーザーを作成しました:
[zhangsan] zhangsan | メール: zhangsan@example.com | 状態: 有効 | ログイン回数: 0
正しいパスワードでログイン:
ログイン成功!
[zhangsan] zhangsan | メール: zhangsan@example.com | 状態: 有効 | ログイン回数: 1
間違ったパスワードでログイン:
ログイン失敗: パスワードが正しくありません
アカウント無効化後のログイン試行:
ログイン失敗: アカウントzhangsanは無効になっています
[zhangsan] zhangsan | メール: zhangsan@example.com | 状態: 無効 | ログイン回数: 1
場面2:ショッピングカートシステム
メソッドを使ってショッピングカートのCRUDとチェックアウトを実装します。
package main
import "fmt"
// Product は商品
type Product struct {
Name string
Price float64
Category string
}
// CartItem はカートアイテム
type CartItem struct {
Product Product
Quantity int
}
// Subtotal は小計を計算する
func (ci CartItem) Subtotal() float64 {
return ci.Product.Price * float64(ci.Quantity)
}
// ShoppingCart はショッピングカート
type ShoppingCart struct {
Items []CartItem
Owner string
}
// Add は商品をカートに追加する
func (sc *ShoppingCart) Add(p Product, qty int) {
// 既に存在するか確認
for i := range sc.Items {
if sc.Items[i].Product.Name == p.Name {
sc.Items[i].Quantity += qty
fmt.Printf(" 数量を更新: %s x%d\n", p.Name, sc.Items[i].Quantity)
return
}
}
sc.Items = append(sc.Items, CartItem{Product: p, Quantity: qty})
fmt.Printf(" 商品を追加: %s x%d\n", p.Name, qty)
}
// Remove は商品をカートから削除する
func (sc *ShoppingCart) Remove(name string) bool {
for i, item := range sc.Items {
if item.Product.Name == name {
sc.Items = append(sc.Items[:i], sc.Items[i+1:]...)
fmt.Printf(" 商品を削除: %s\n", name)
return true
}
}
fmt.Printf(" 商品が見つかりません: %s\n", name)
return false
}
// Total は合計金額を計算する
func (sc ShoppingCart) Total() float64 {
var total float64
for _, item := range sc.Items {
total += item.Subtotal()
}
return total
}
// Count は商品の種類数を返す
func (sc ShoppingCart) Count() int {
return len(sc.Items)
}
// Display はカートの内容を表示する
func (sc ShoppingCart) Display() {
fmt.Printf("\n🛒 %sさんのカート(%d商品):\n", sc.Owner, sc.Count())
fmt.Println(" ─────────────────────────────────────")
for _, item := range sc.Items {
fmt.Printf(" %-12s $%.2f x %d = $%.2f\n",
item.Product.Name, item.Product.Price, item.Quantity, item.Subtotal())
}
fmt.Println(" ─────────────────────────────────────")
fmt.Printf(" 合計: $%.2f\n", sc.Total())
}
func main() {
cart := ShoppingCart{Owner: "Zhang San"}
// 商品を定義
laptop := Product{Name: "Laptop", Price: 999, Category: "Electronics"}
mouse := Product{Name: "Wireless Mouse", Price: 29, Category: "Electronics"}
book := Product{Name: "Go Programming", Price: 45, Category: "Books"}
coffee := Product{Name: "Coffee", Price: 5, Category: "Food"}
// 商品を追加
fmt.Println("商品を追加:")
cart.Add(laptop, 1)
cart.Add(mouse, 2)
cart.Add(book, 3)
cart.Add(coffee, 5)
cart.Display()
// 数量を更新
fmt.Println("\n数量を更新:")
cart.Add(mouse, 1) // マウスを1つ追加
cart.Add(book, -1) // 本を1冊削除(負の数で指定)
cart.Display()
// 商品を削除
fmt.Println("\n商品を削除:")
cart.Remove("Coffee")
cart.Display()
}
出力:
商品を追加:
商品を追加: Laptop x1
商品を追加: Wireless Mouse x2
商品を追加: Go Programming x3
商品を追加: Coffee x5
🛒 Zhang Sanさんのカート(4商品):
─────────────────────────────────────
Laptop $999.00 x 1 = $999.00
Wireless Mouse $29.00 x 2 = $58.00
Go Programming $45.00 x 3 = $135.00
Coffee $5.00 x 5 = $25.00
─────────────────────────────────────
合計: $1217.00
数量を更新:
数量を更新: Wireless Mouse x3
数量を更新: Go Programming x2
🛒 Zhang Sanさんのカート(4商品):
─────────────────────────────────────
Laptop $999.00 x 1 = $999.00
Wireless Mouse $29.00 x 3 = $87.00
Go Programming $45.00 x 2 = $90.00
Coffee $5.00 x 5 = $25.00
─────────────────────────────────────
合計: $1201.00
商品を削除:
商品を削除: Coffee
🛒 Zhang Sanさんのカート(3商品):
─────────────────────────────────────
Laptop $999.00 x 1 = $999.00
Wireless Mouse $29.00 x 3 = $87.00
Go Programming $45.00 x 2 = $90.00
─────────────────────────────────────
合計: $1176.00
❓ よくある質問
質問1:値レシーバとポインタレシーバ、どうやって選べばいい?
以下のルールに従ってください:
- レシーバのフィールドを変更する必要がある → ポインタレシーバ
- レシーバが大きな構造体(フィールドや配列が多い) → ポインタレシーバ(コピーのオーバーヘッドを回避)
- 構造体に
sync.Mutexなどのコピー不可能なフィールドが含まれている → ポインタレシーバを必ず使用 - 読み取り専用操作で構造体が小さい → 値レシーバで十分
- 迷ったとき → ポインタレシーバを優先
type Small struct { X int }
func (s Small) Get() int { return s.X } // ✅ 値レシーバ、読み取り専用
type Large struct { Data [1024]byte }
func (l *Large) Process() { /* ... */ } // ✅ ポインタレシーバ、コピーを回避
type Safe struct { mu sync.Mutex }
func (s *Safe) Lock() { s.mu.Lock() } // ✅ ポインタを必ず使用、Mutexはコピー不可能
質問2:メソッドを呼び出すとコンパイラがエラーを出すのはなぜ?
最も一般的な理由は、値レシーバとポインタレシーバの呼び出し規則を混同していることです。
type Dog struct { Name string }
func (d *Dog) Rename(name string) {
d.Name = name
}
func main() {
d := Dog{Name: "Rex"}
d.Rename("Buddy") // ✅ Goが自動的にアドレスを取り、(&d).Rename("Buddy") と同等
// しかし、これはエラーになります:
// Dog{"Rex"}.Rename("Buddy") // ❌ アドレス不可能な値のアドレスを取得できない
}
Goコンパイラは d.Rename() → (&d).Rename() の変換を自動的に処理しますが、変数がアドレス可能な場合のみです。リテラルや一時的な値はアドレス不可能です。
質問3:メソッドは複数の値を返すことはできますか?
はい、メソッドと関数は同じシグネチャルールを持ち、任意の数のパラメータと戻り値を持つことができます。
type Calculator struct {
Value float64
}
// 商と剰余を返す
func (c Calculator) DivideBy(divisor float64) (float64, float64, error) {
if divisor == 0 {
return 0, 0, fmt.Errorf("除数はゼロにできません")
}
return c.Value / divisor, math.Mod(c.Value, divisor), nil
}
質問4:埋め込み構造体のメソッド衝突はどうなる?
2つの埋め込まれた型が同じ名前のメソッドを持つ場合、Goコンパイラはエラーを報告するため、どちらを呼び出すかを明示的に指定する必要があります。
type A struct{}
func (A) Hello() string { return "A" }
type B struct{}
func (B) Hello() string { return "B" }
type C struct {
A
B
}
func main() {
c := C{}
// c.Hello() // ❌ コンパイルエラー:曖昧なセレクタ c.Hello
fmt.Println(c.A.Hello()) // ✅ 明示的:出力 "A"
fmt.Println(c.B.Hello()) // ✅ 明示的:出力 "B"
}
📖 まとめ
このレッスンではGoのメソッドについて学びました:
- メソッドの定義:レシーバを通じて関数を型に結びつける
- 値レシーバとポインタレシーバ:値レシーバはコピーに対して操作し、ポインタレシーバは元の値を変更できる
- メソッドセットのルール:値型は値レシーバのメソッドのみを持ち、ポインタ型はすべてのメソッドを持つ
- 継承より合成:埋め込み構造体を通じてメソッドの再利用とオーバーライドを実現
- ベストプラクティス:レシーバ名を短く保ち、同じ型には一貫したレシーバ型を使用し、変更が必要な場合はポインタを使用
メソッドはGoにおけるオブジェクト指向プログラミングの基盤です。次のレッスンではインターフェースについて学びます — Goの強力な多態性の仕組みで、メソッドと密接に連携してGoの型システムの中核を形成します。
📝 演習
演習1:温度変換 ⭐
摂氏フィールドを持つ Temperature 構造体を作成し、以下のメソッドを実装してください:
To Fahrenheit()— 華氏に変換(F = C × 9/5 + 32)To Kelvin()— ケルビンに変換(K = C + 273.15)Describe() string— フォーマットされた温度の説明を返す
// 期待される出力:
// Temperature: 100.00°C = 212.00°F = 373.15K
演習2:銀行口座(取引履歴付き) ⭐⭐
場面2のAccount構造体を以下の機能で拡張してください:
- 取引履歴(各取引の金額と時刻を格納するスライス)
History()メソッドですべての取引記録を表示Statement()メソッドでフォーマットされた口座明細書を生成
// 期待される出力:
// === 口座明細書 ===
// 口座名:Zhang San
// 取引履歴:
// 1. +1000.00 (初回入金)
// 2. -200.00 (購入)
// 3. +500.00 (給与)
// 現在の残高:1300.00
演習3:図形システム(合成継承) ⭐⭐⭐
図形システムを作成してください:
Area() float64とPerimeter() float64メソッドを持つShapeベースインターフェースを定義Rectangle、Circle、Triangle構造体を実装- 複数の図形を保持できる
Canvas構造体を作成 Canvas.TotalArea()ですべての図形の合計面積を計算- 埋め込み構造体を使って
ColoredRectangleを作成(RectangleのメソッドとColorフィールドの両方を持つ)
// 期待される出力:
// Canvas には3つの図形があります
// 合計面積:xxx.xx
// 赤い長方形:幅=10、高さ=5、色=赤
次のレッスン
レッスン9:インターフェース — Goのインターフェースシステムについて学びます:暗黙の実装、空のインターフェース、型アサーション、インターフェースの合成、そしてGoの最も強力な多態性の仕組み。



