Go言語 インターフェース

Goの主な特徴の1つは、インターフェースのサポートです。インターフェースは、実装可能な抽象型を定義する方法を提供します。

Go言語 インターフェースの定義

Goのインターフェースは、メソッドシグネチャの集合体です。言い換えれば、インターフェースに準拠したい任意の型が実装できるメソッドの集合の設計図のようなものです。インターフェースは、構造体やカスタム型が実装できる汎用型を定義する方法を提供します。

Goのインターフェースを理解するために、動物を例にとって考えてみましょう。Animalというインターフェースを定義して、文字列を返すMakeSoundというメソッドを持たせます。

type Animal interface {
    MakeSound() string
}

このインターフェースを実装したい型は、MakeSoundメソッドを実装する必要があります。例えば、Dog型は以下のようにこのインターフェースを実装することができます。

type Dog struct {}

func (d Dog) MakeSound() string {
    return "Bark"
}

同様に、Animalインターフェースを実装したCatやLionなどの型も定義することができます。

type Cat struct {}

func (c Cat) MakeSound() string {
    return "Meow"
}

type Lion struct {}

func (l Lion) MakeSound() string {
    return "Roar"
}

ここで、Animalインターフェース型のスライスを作成して出力してみましょう。

animals := []Animal{Dog{}, Cat{}, Lion{}}

for _, animal := range animals {
    fmt.Println(animal.MakeSound())
}

// Bark
// Meow
// Roar

Goのインターフェースの実装は暗黙的です。つまり、実装を明示的に宣言する必要はありません。インターフェースの実装は、インターフェースのメソッドを実装する型によって決定されます。その型がインターフェースで定義された全てのメソッドを実装している限り、追加の宣言は必要なく、そのインターフェースを暗黙のうちに実装することになります。

Dog型がAnimalインターフェースを実装しているかどうかは、Dog型がAnimalインターフェースで定義されたすべてのメソッドを実装しているかどうかで判断できます。

Go言語 インターフェースのコンパイルエラー

先ほどの例で定義したAnimalインターフェースを考えてみましょう。では、Animalインターフェースを実装していないBird型を作成してみましょう。

type Bird struct {}
animals := []Animal{Dog{}, Cat{}, Lion{}, Bird{}}

// error

もし、Animalインターフェース型のスライスで、Bird型を使おうとすると、コンパイルエラーになります。

Bird型がMakeSoundメソッドを実装していないため、Animal型として使用できないことをエラーとして示します。

Goでは、型はインターフェースで定義されたすべてのメソッドを実装することが義務付けられています。もし型がインターフェースの実装に失敗すると、コンパイルエラーになります。この機能により、コードの堅牢性が確保され、メソッドの欠落によるバグの発生を防ぐことができます。

Go言語 インターフェースへの追加

ある型がインターフェースで定義されていない追加のメソッドを実装していても、コンパイルエラーや問題は発生しません。その型をインターフェースとして使う場合、追加されたメソッドは単に無視されます。

例えば、次のようなAnimalインターフェースを考えてみましょう。

type Animal interface {
    MakeSound() string
}

では、Animalインターフェースを実装したDog型を作成しましょう。

type Dog struct {}

func (d Dog) MakeSound() string {
    return "Bark"
}

func (d Dog) PlayFetch() string {
    return "The dog is playing fetch."
}

このように、Dog型にはAnimalインターフェースでは定義されていないPlayFetchメソッドが追加されています。このため、Dog型をAnimal型として使用しても、問題やエラーは発生しません。

animals := []Animal{Dog{}}

for _, animal := range animals {
    fmt.Println(animal.MakeSound())
}

上記のコードでは、AnimalインターフェースのMakeSoundメソッドのみを使用し、追加メソッドのPlayFetchは無視されます。

Go言語 インターフェースの例

インターフェースを使用しないと、コードの管理、保守、テストが難しくなるいくつかの問題が発生する可能性があります。ここでは、インターフェースを使用しない場合に発生しうる問題を示す例を示します。

正方形、長方形、三角形などの異なる形状の面積を出力するコードを作成するシナリオを考えてみましょう。このシナリオを処理する1つの方法は、それぞれの形状に対して別々の関数を作成することです。

ある図形を取り込んで、その面積を計算する関数を定義しましょう。

func SquareArea(side float64) float64 {
    return side * side
}

func RectangleArea(length, width float64) float64 {
    return length * width
}

func TriangleArea(base, height float64) float64 {
    return 0.5 * base * height
}

この方法は、コードの管理が難しくなる、将来的に新しい形状を追加しにくくなる、コードのテストがしにくくなる等の問題があります。

例えば、新しい形状を追加したい場合、新しい関数を追加する必要があり、形状の数が増えるにつれて管理・保守が困難になる可能性があります。さらに、関数ごとに別々のテストを書く必要があるため、コードのテストを書くのが難しくなります。

より良い方法は、すべての図形が実装すべき共通のメソッドを定義するインターフェースを使用することです。

type Shape interface {
    Area() float64
}

type Square struct {
    Side float64
}

func (s Square) Area() float64 {
    return s.Side * s.Side
}

type Rectangle struct {
    Length float64
    Width float64
}

func (r Rectangle) Area() float64 {
    return r.Length * r.Width
}

type Triangle struct {
    Base float64
    Height float64
}

func (t Triangle) Area() float64 {
    return 0.5 * t.Base * t.Height
}

このようにすることで、Shapeインターフェースタイプのスライスを使用して、さまざまな形状の面積を一度に計算できるようになりました。構造別に関数を設定すると、非常に煩雑になってしまうことが容易に想像できるでしょう。

shapes := []Shape{Square{5}, Rectangle{3, 4}, Triangle{3, 4}}

for _, shape := range shapes {
    fmt.Println(shape.Area())
}

このアプローチにより、コードの管理が容易になり、将来的に新しい形状を追加する能力が加わり、コードのテストが容易になります。

Go言語 インターフェースの型アサーション

Goにおける型アサーションでは、インターフェース型の値が実際の具体的な型を満たしているかどうかを判定することができます。

package main

import (
    "fmt"
)

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() {
    var animal Animal
    animal = Dog{}

    // 型アサーションは(インターフェース型を満たす変数).(型)の形式
    dog, ok := animal.(Dog)
    if ok {
        fmt.Println("The animal is a Dog:", dog.Speak())
    } else {
        fmt.Println("The animal is not a Dog.")
    }

    cat, ok := animal.(Cat)
    if ok {
        fmt.Println("The animal is a Cat:", cat.Speak())
    } else {
        fmt.Println("The animal is not a Cat.")
    }
}

この例では、Speak()というメソッドを定義したAnimalインターフェースがあります。また、Animalインターフェースを実装したDogとCatという型があります。

main関数では、Animal型の変数を宣言することで、Animalインターフェースを実装したさまざまな型の値を格納することができます。Animalインターフェースの型に格納された値が、DogなのかCatなのかを型アサーションでチェックします。型アサーションが成功すると、変数okにtrueがセットされ、変数dogまたはcatにその型の値が格納されます。

型アサーションに失敗した場合、型アサーション内の変数okは falseに設定され、指定した変数にその型の値が格納されることはありません。第二引数が省略されている場合、パニックというエラー状態になってしまいます。

Go言語 型スイッチ

Goの型スイッチは複数の型を同時に扱う便利な方法で、インターフェースの値の型をチェックして、基になる型に基づいて異なる操作を実行することができます。

package main

import (
    "fmt"
)

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() {
    var animal Animal
    animal = Dog{}

    // switch文と(変数).(type)という書き方をすることで、型スイッチを構築
    // caseには実際に判定したい具体的な型を書く
    switch animalType := animal.(type) {
    case Dog:
        fmt.Println("The animal is a Dog:", animalType.Speak())
    case Cat:
        fmt.Println("The animal is a Cat:", animalType.Speak())
    default:
        fmt.Println("The animal type is unknown.")
    }

    animal = Cat{}

    switch animalType := animal.(type) {
    case Dog:
        fmt.Println("The animal is a Dog:", animalType.Speak())
    case Cat:
        fmt.Println("The animal is a Cat:", animalType.Speak())
    default:
        fmt.Println("The animal type is unknown.")
    }
}

それでは、他の例を見てみましょう。

package main

import (
    "fmt"
)

type Shape interface {
    Area() float64
}

type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return 3.14 * c.Radius * c.Radius
}

func PrintArea(shape Shape) {
    switch shape := shape.(type) {
    case Rectangle:
        fmt.Println("The area of the rectangle is", shape.Area())
    case Circle:
        fmt.Println("The area of the circle is", shape.Area())
    default:
        fmt.Println("The shape type is unknown.")
    }
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    PrintArea(rect)

    circle := Circle{Radius: 5}
    PrintArea(circle)
}

この例では、メソッドArea()を定義したインターフェースShapeを用意しています。また、Shapeインターフェースを実装したRectangleとCircleという2つの型があります。

PrintArea関数では、引数shapeの型を型スイッチで判別し、型によって異なる処理を行います。引数shapeがRectangleであれば、長方形の面積を表示し、引数shapeがCircleであれば、円の面積を表示します。

この記事を書いた人

著者の画像

Jeffry Alvarado

Ex-Facebook Engineer 大学ではコンピュータサイエンスを専攻し、在学中に複数のインターンシップを経験。コンピュータサイエンスが学習できるプラットフォームRecursionを創業し、CTOとしてカリキュラム作成、ソフトウェア開発を担当。


ツイート