Goでインターフェイスを使用する方法

前書き

汎用性の高いプログラムを開発するには、柔軟で再利用可能なモジュール式のコードを書くことが不可欠です。 この方法で作業すると、複数の場所で同じ変更を行う必要がなくなるため、コードの保守が容易になります。 これを達成する方法は、言語によって異なります。 たとえば、inheritanceは、Java、C ++、C#などの言語で使用される一般的なアプローチです。

開発者は、compositionを使用して同じ設計目標を達成することもできます。 コンポジションは、オブジェクトまたはデータ型をより複雑なものに結合する方法です。 これは、Goがコードの再利用、モジュール性、および柔軟性を促進するために使用するアプローチです。 Goのインターフェイスは、複雑なコンポジションを整理する方法を提供し、それらの使用方法を学習することで、共通の再利用可能なコードを作成できます。

この記事では、一般的な動作を持つカスタムタイプを作成する方法を学習します。これにより、コードを再利用できます。 また、別のパッケージで定義されたインターフェイスを満たす独自のカスタムタイプのインターフェイスを実装する方法についても学習します。

動作の定義

コンポジションのコア実装の1つは、インターフェイスの使用です。 インターフェイスは、型の動作を定義します。 Go標準ライブラリで最も一般的に使用されるインターフェイスの1つは、fmt.Stringerインターフェイスです。

type Stringer interface {
    String() string
}

コードの最初の行は、Stringerと呼ばれるtypeを定義します。 次に、それがinterfaceであると述べます。 構造体を定義するのと同じように、Goは中括弧({})を使用してインターフェイスの定義を囲みます。 構造体の定義と比較して、インターフェイスのbehaviorのみを定義します。つまり、「このタイプで何ができるか」です。

Stringerインターフェースの場合、唯一の動作はString()メソッドです。 メソッドは引数を取らず、文字列を返します。

次に、fmt.Stringerの動作をするコードを見てみましょう。

main.go

package main

import "fmt"

type Article struct {
    Title string
    Author string
}

func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

func main() {
    a := Article{
        Title: "Understanding Interfaces in Go",
        Author: "Sammy Shark",
    }
    fmt.Println(a.String())
}

最初に行うことは、Articleという新しいタイプを作成することです。 このタイプにはTitleフィールドとAuthorフィールドがあり、どちらも文字列data typeです。

main.go

...
type Article struct {
    Title string
    Author string
}
...

次に、ArticleタイプでStringと呼ばれるmethodを定義します。 Stringメソッドは、Articleタイプを表す文字列を返します。

main.go

...
func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}
...

次に、mainfunctionで、Articleタイプのインスタンスを作成し、それをaと呼ばれるvariableに割り当てます。 Titleフィールドには"Understanding Interfaces in Go"の値を提供し、Authorフィールドには"Sammy Shark"の値を提供します。

main.go

...
a := Article{
    Title: "Understanding Interfaces in Go",
    Author: "Sammy Shark",
}
...

次に、fmt.Printlnを呼び出し、a.String()メソッド呼び出しの結果を渡すことにより、Stringメソッドの結果を出力します。

main.go

...
fmt.Println(a.String())

プログラムを実行すると、次の出力が表示されます。

OutputThe "Understanding Interfaces in Go" article was written by Sammy Shark.

これまで、インターフェイスを使用していませんでしたが、動作を持つタイプを作成しました。 その動作はfmt.Stringerインターフェースと一致しました。 次に、その動作を使用してコードを再利用可能にする方法を見てみましょう。

インターフェイスの定義

目的の動作で定義された型ができたので、その動作の使用方法を見てみましょう。

ただし、その前に、関数のArticle型からStringメソッドを呼び出す場合に何をする必要があるかを見てみましょう。

main.go

package main

import "fmt"

type Article struct {
    Title string
    Author string
}

func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

func main() {
    a := Article{
        Title: "Understanding Interfaces in Go",
        Author: "Sammy Shark",
    }
    Print(a)
}

func Print(a Article) {
    fmt.Println(a.String())
}

このコードでは、Articleを引数として取るPrintという新しい関数を追加します。 Print関数が行うのは、Stringメソッドを呼び出すことだけであることに注意してください。 このため、代わりに関数に渡すインターフェイスを定義できます。

main.go

package main

import "fmt"

type Article struct {
    Title string
    Author string
}

func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

type Stringer interface {
    String() string
}

func main() {
    a := Article{
        Title: "Understanding Interfaces in Go",
        Author: "Sammy Shark",
    }
    Print(a)
}

func Print(s Stringer) {
    fmt.Println(s.String())
}

ここでは、Stringerというインターフェイスを作成しました。

main.go

...
type Stringer interface {
    String() string
}
...

Stringerインターフェースには、stringを返すString()と呼ばれるメソッドが1つだけあります。 methodは、Goの特定のタイプにスコープされた特別な関数です。 関数とは異なり、メソッドは定義された型のインスタンスからのみ呼び出すことができます。

次に、Printメソッドのシグネチャを更新して、Articleの具体的なタイプではなく、Stringerを取得します。 コンパイラーは、StringerインターフェースがStringメソッドを定義することを知っているため、Stringメソッドも持つ型のみを受け入れます。

これで、Stringerインターフェイスを満たすものなら何でもPrintメソッドを使用できます。 これを実証するために別のタイプを作成しましょう。

main.go

package main

import "fmt"

type Article struct {
    Title  string
    Author string
}

func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

type Book struct {
    Title  string
    Author string
    Pages  int
}

func (b Book) String() string {
    return fmt.Sprintf("The %q book was written by %s.", b.Title, b.Author)
}

type Stringer interface {
    String() string
}

func main() {
    a := Article{
        Title:  "Understanding Interfaces in Go",
        Author: "Sammy Shark",
    }
    Print(a)

    b := Book{
        Title:  "All About Go",
        Author: "Jenny Dolphin",
        Pages:  25,
    }
    Print(b)
}

func Print(s Stringer) {
    fmt.Println(s.String())
}

ここで、Bookという2番目のタイプを追加します。 また、Stringメソッドが定義されています。 これは、Stringerインターフェースも満たしていることを意味します。 このため、Print関数に送信することもできます。

OutputThe "Understanding Interfaces in Go" article was written by Sammy Shark.
The "All About Go" book was written by Jenny Dolphin. It has 25 pages.

これまで、単一のインターフェイスを使用する方法を示してきました。 ただし、インターフェイスには複数の動作を定義できます。 次に、より多くのメソッドを宣言することにより、インターフェイスをより汎用的にする方法を確認します。

インターフェイス内の複数の動作

Goコードを書く中核となる原則の1つは、小さくて簡潔な型を記述し、それらをより大きく、より複雑な型に構成することです。 インターフェイスを構成する場合も同様です。 インターフェースの構築方法を確認するには、最初に1つのインターフェースのみを定義することから始めます。 CircleSquareの2つの形状を定義し、どちらもAreaというメソッドを定義します。 このメソッドは、それぞれの形状の幾何学的領域を返します。

main.go

package main

import (
    "fmt"
    "math"
)

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * math.Pow(c.Radius, 2)
}

type Square struct {
    Width  float64
    Height float64
}

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

type Sizer interface {
    Area() float64
}

func main() {
    c := Circle{Radius: 10}
    s := Square{Height: 10, Width: 5}

    l := Less(c, s)
    fmt.Printf("%+v is the smallest\n", l)
}

func Less(s1, s2 Sizer) Sizer {
    if s1.Area() < s2.Area() {
        return s1
    }
    return s2
}

各タイプはAreaメソッドを宣言しているため、その動作を定義するインターフェイスを作成できます。 次のSizerインターフェイスを作成します。

main.go

...
type Sizer interface {
    Area() float64
}
...

次に、2つのSizerを取り、最小のものを返すLessという関数を定義します。

main.go

...
func Less(s1, s2 Sizer) Sizer {
    if s1.Area() < s2.Area() {
        return s1
    }
    return s2
}
...

両方の引数をタイプSizerとして受け入れるだけでなく、結果もSizerとして返すことに注意してください。 これは、SquareまたはCircleではなく、Sizerのインターフェイスを返すことを意味します。

最後に、面積が最小のものを印刷します。

Output{Width:5 Height:10} is the smallest

次に、各タイプに別の動作を追加しましょう。 今回は、文字列を返すString()メソッドを追加します。 これにより、fmt.Stringerインターフェイスが満たされます。

main.go

package main

import (
    "fmt"
    "math"
)

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * math.Pow(c.Radius, 2)
}

func (c Circle) String() string {
    return fmt.Sprintf("Circle {Radius: %.2f}", c.Radius)
}

type Square struct {
    Width  float64
    Height float64
}

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

func (s Square) String() string {
    return fmt.Sprintf("Square {Width: %.2f, Height: %.2f}", s.Width, s.Height)
}

type Sizer interface {
    Area() float64
}

type Shaper interface {
    Sizer
    fmt.Stringer
}

func main() {
    c := Circle{Radius: 10}
    PrintArea(c)

    s := Square{Height: 10, Width: 5}
    PrintArea(s)

    l := Less(c, s)
    fmt.Printf("%v is the smallest\n", l)

}

func Less(s1, s2 Sizer) Sizer {
    if s1.Area() < s2.Area() {
        return s1
    }
    return s2
}

func PrintArea(s Shaper) {
    fmt.Printf("area of %s is %.2f\n", s.String(), s.Area())
}

CircleタイプとSquareタイプの両方がAreaメソッドとStringメソッドの両方を実装しているため、その幅広い動作セットを記述するための別のインターフェイスを作成できます。 これを行うには、Shaperというインターフェイスを作成します。 これをSizerインターフェースとfmt.Stringerインターフェースで構成します。

main.go

...
type Shaper interface {
    Sizer
    fmt.Stringer
}
...

[.note]#Note:fmt.Stringerio.Writerなどのerで終わるようにインターフェイスに名前を付けようとするのは、慣用句と見なされます。 これが、インターフェイスにShapeではなくShaperという名前を付けた理由です。

これで、Shaperを引数として取るPrintAreaという関数を作成できます。 これは、AreaメソッドとStringメソッドの両方の渡された値に対して両方のメソッドを呼び出すことができることを意味します。

main.go

...
func PrintArea(s Shaper) {
    fmt.Printf("area of %s is %.2f\n", s.String(), s.Area())
}

プログラムを実行すると、次の出力が表示されます。

Outputarea of Circle {Radius: 10.00} is 314.16
area of Square {Width: 5.00, Height: 10.00} is 50.00
Square {Width: 5.00, Height: 10.00} is the smallest

これで、必要に応じて、より小さなインターフェイスを作成し、より大きなインターフェイスに構築する方法を確認しました。 大きなインターフェイスから開始してすべての関数に渡すこともできますが、必要な関数には最小のインターフェイスのみを送信することをお勧めします。 特定の小さなインターフェイスを受け入れるものはすべて、その定義された動作でのみ動作することを意図しているため、これは通常、より明確なコードになります。

たとえば、ShaperLess関数に渡した場合、AreaメソッドとStringメソッドの両方を呼び出すと想定できます。 ただし、Areaメソッドのみを呼び出すことを意図しているため、渡された引数のAreaメソッドしか呼び出せないことがわかっているため、Less関数が明確になります。

結論

小さなインターフェイスを作成し、それを大きなインターフェイスに構築することで、関数またはメソッドに必要なものだけを共有できることを確認しました。 また、パッケージだけでなく、他のパッケージで定義されたものを含む、他のインターフェースからインターフェースを構成できることも学びました。

Goプログラミング言語について詳しく知りたい場合は、How To Code in Go series全体を確認してください。