第12章 メソッドとインターフェース

メソッド

Go言語における「メソッド」の基本は、特定の型に関連付けられた関数です。Goでは、任意の型(主に構造体)にメソッドを定義することができ、これによりオブジェクト指向プログラミングの一部の要素を実現できます。以下に、メソッドの基本に関する具体的な事例を示します。


メソッドの定義

メソッドは、レシーバ(receiver)と呼ばれる特別な引数を持つ関数です。レシーバは、そのメソッドが関連付けられる型を定義します。

package main

import "fmt"

// Person 構造体の定義
type Person struct {
    Name string
    Age  int
}

// Person型に紐付けられたメソッドの定義
func (p Person) SayHello() {
    fmt.Printf("Hello, my name is %s and I am %d years old.\n", p.Name, p.Age)
}

func main() {
    // Person 構造体のインスタンスを作成
    bob := Person{Name: "Bob", Age: 30}

    // SayHello メソッドを呼び出す
    bob.SayHello()
}

この例では、b型にSayHelloというメソッドを定義しています。このメソッドは、Person型のインスタンス(この例ではbob)に対して呼び出されます。


メソッドの呼び出し

メソッドは、関連付けられた型のインスタンスを通じて呼び出されます。このプロセスは、多くのオブジェクト指向言語で見られるメソッド呼び出しに似ています。


値レシーバとポインタレシーバ

メソッドは、値レシーバまたはポインタレシーバを持つことができます。値レシーバは、メソッド呼び出し時にレシーバの値のコピーを使用し、ポインタレシーバはレシーバのポインタを使用します。

// Person の年齢を変更するメソッド
func (p *Person) SetAge(newAge int) {
    p.Age = newAge
}

func main() {
    alice := Person{Name: "Alice", Age: 25}

    // ポインタレシーバを使用して Alice の年齢を変更
    alice.SetAge(26)
    alice.SayHello() // 出力: Hello, my name is Alice and I am 26 years old.
}

この例では、SetAgeメソッドはポインタレシーバを使用しており、PersonAgeフィールドを直接変更できます。


ポインタレシーバを使用する理由
  • レシーバの指すデータを変更する場合: メソッドがレシーバの指すオブジェクトの状態を変更する必要がある場合、ポインタレシーバを使用します。これは、メソッドが呼び出されたときにオリジナルのデータに作用するためです。
  • パフォーマンスの最適化: 大きなデータ構造をコピーすることはコストがかかります。ポインタレシーバを使用すると、データのコピーを避けることができ、パフォーマンスが向上します。

以下の例では、Person構造体に対してUpdateNameメソッドを定義し、そのメソッドを使用してPersonNameフィールドを更新します。

package main

import "fmt"

// Person 構造体
type Person struct {
    Name string
    Age  int
}

// UpdateName メソッド。ポインタレシーバを使用。
func (p *Person) UpdateName(newName string) {
    p.Name = newName
}

func main() {
    bob := Person{Name: "Bob", Age: 30}
    fmt.Println("Before:", bob.Name) // 出力: Before: Bob

    bob.UpdateName("Robert")
    fmt.Println("After:", bob.Name) // 出力: After: Robert
}

この例では、UpdateNameメソッドはPersonのポインタレシーバを使用しています。これにより、bobNameフィールドが実際に更新されます。もし値レシーバを使用していたら、bobのオリジナルのデータは変更されず、UpdateNameが呼び出された時のPersonのコピーのみが変更されることになります。


インターフェイス

Go言語における「インターフェイス」は、特定のメソッドシグネチャを持つメソッドの集合を定義するタイプです。インターフェイスは型が特定の動作を実行できることを保証し、異なる型間での共通の振る舞いを定義するのに役立ちます。以下に、インターフェイスの基本に関する具体的な事例を示します。


インターフェイスの定義

インターフェイスは、一つ以上のメソッドを定義します。型がインターフェイスを実装するためには、そのインターフェイスに定義されたすべてのメソッドを実装する必要があります。

package main

import "fmt"

// Greeter インターフェイスの定義
type Greeter interface {
    Greet() string
}

// EnglishSpeaker は Greeter インターフェイスを実装
type EnglishSpeaker struct{}

func (EnglishSpeaker) Greet() string {
    return "Hello!"
}

// SpanishSpeaker は Greeter インターフェイスを実装
type SpanishSpeaker struct{}

func (SpanishSpeaker) Greet() string {
    return "¡Hola!"
}

func main() {
    var greeter Greeter

    // EnglishSpeaker を Greeter として使用
    greeter = EnglishSpeaker{}
    fmt.Println(greeter.Greet()) // 出力: Hello!

    // SpanishSpeaker を Greeter として使用
    greeter = SpanishSpeaker{}
    fmt.Println(greeter.Greet()) // 出力: ¡Hola!
}

この例では、GreeterインターフェイスにGreetメソッドが定義されており、EnglishSpeakerSpanishSpeaker型はこのインターフェイスを実装しています。main関数内では、どちらの型もGreeterインターフェイスとして使用されています。


メソッドとインターフェイス


メソッドのオーバーロード

Go言語ではメソッドのオーバーロードはサポートされていません。つまり、同じ名前のメソッドを異なるシグネチャで複数定義することはできません。代わりに、異なる型に対して同じインターフェイスを実装することが推奨されます。


インターフェイスとポリモーフィズム

Go言語では、異なる型が同じインターフェイスを実装することにより、ポリモーフィズムを達成します。

type Animal interface {
    Speak() string
}

type Dog struct{}
func (d Dog) Speak() string {
    return "Woof!"
}

type Cat struct{}
func (c Cat) Speak() string {
    return "Meow!"
}

func main() {
    animals := []Animal{Dog{}, Cat{}}
    for _, animal := range animals {
        fmt.Println(animal.Speak())
    }
}

この例では、GreeterインターフェイスにGreetメソッドが定義されており、EnglishSpeakerSpanishSpeaker型はこのインターフェイスを実装しています。main関数内では、どちらの型もGreeterインターフェイスとして使用されています。


インターフェイスの型アサーションと型スイッチ

型アサーションと型スイッチを用いることで、インターフェイスの背後にある具体的な型を特定したり、型に応じて異なる処理を行ったりできます。

func printType(i interface{}) {
    switch v := i.(type) {
    case Dog:
        fmt.Println("This is a Dog")
    case Cat:
        fmt.Println("This is a Cat")
    default:
        fmt.Println("Unknown type")
    }
}

func main() {
    printType(Dog{})
    printType(Cat{})
    printType("hello")
}


ポインタとインターフェイス

Go言語では、ポインタ型と値型はインターフェイスを異なる方法で実装することがあります。特に、ポインタレシーバを使用するメソッドは、その型のポインタを通じてのみインターフェイスとして扱うことができます。

type Modifier interface {
    Modify()
}

type Data struct{}

func (d *Data) Modify() {
    // 実際の変更処理
}

func main() {
    var m Modifier = &Data{} // 正しい: Data のポインタは Modifier インターフェイスを実装
    m.Modify()
}


練習問題1.

Circleという構造体を定義し、Radiusというfloat64型のフィールドを持たせてください。その後、CircleAreaという名前のメソッドを定義し、円の面積を計算して返すようにしてください(円の面積 = π * 半径^2)。main関数でCircleのインスタンスを作成し、Areaメソッドを呼び出して結果を出力してください。

package main

import (
    "fmt"
    "math"
)

// ここに Circle 構造体を定義

// ここに Area メソッドを定義

func main() {
    // ここにコードを記述
}


練習問題2.

Speakerという名前のインターフェイスを定義し、Speakという名前のメソッドを持たせてください(このメソッドはstringを返す)。次に、DogCatという2つの構造体を定義し、それぞれにSpeakerインターフェイスを実装してください。DogSpeakメソッドは"Woof"を、CatSpeakメソッドは"Meow"を返すようにしてください。main関数で、DogCatのインスタンスを作成し、それぞれのSpeakメソッドを呼び出して結果を出力してください。

package main

import "fmt"

// ここに Speaker インターフェイスを定義

// ここに Dog と Cat 構造体を定義

func main() {
    // ここにコードを記述
}


練習問題3.

問題2で作成したSpeakerインターフェイスを使用し、異なる型(DogCat)のスライスを作成してください。このスライスをループし、各要素のSpeakメソッドを呼び出して結果を出力してください。この問題は、インターフェイスを通じて異なる型のオブジェクトを同じ方法で扱うポリモーフィズムの概念を実践するものです。

package main

import "fmt"

// Speaker インターフェイス、Dog と Cat 構造体は既に定義済みとする

func main() {
    speakers := []Speaker{
        Dog{},
        Cat{},
    }

    // ここに speakers スライスをループするコードを記述
}