Goでインターフェースを使用する方法
序章
柔軟で再利用可能なモジュラーコードを作成することは、用途の広いプログラムを開発するために不可欠です。 このように作業することで、複数の場所で同じ変更を加える必要がなくなるため、コードの保守が容易になります。 これを達成する方法は、言語によって異なります。 たとえば、継承は、Java、C ++、C#などの言語で使用される一般的なアプローチです。
開発者は、構成を介して同じ設計目標を達成することもできます。 コンポジションは、オブジェクトまたはデータ型をより複雑なものに結合する方法です。 これは、Goがコードの再利用、モジュール性、および柔軟性を促進するために使用するアプローチです。 Goのインターフェースは、複雑な構成を整理する方法を提供し、それらの使用方法を学ぶことで、共通の再利用可能なコードを作成できるようになります。
この記事では、コードを再利用できるようにする、一般的な動作を持つカスタムタイプを作成する方法を学習します。 また、別のパッケージから定義されたインターフェイスを満たす独自のカスタムタイプのインターフェイスを実装する方法についても学習します。
動作の定義
コンポジションのコア実装の1つは、インターフェイスの使用です。 インターフェイスは、型の動作を定義します。 Go標準ライブラリで最も一般的に使用されるインターフェイスの1つは、fmt.Stringerインターフェイスです。
type Stringer interface { String() string }
コードの最初の行は、Stringer
と呼ばれるtype
を定義します。 次に、それがinterface
であると述べます。 構造体を定義するのと同じように、Goは中括弧({}
)を使用してインターフェースの定義を囲みます。 構造体の定義と比較して、インターフェイスの動作のみを定義します。 つまり、「このタイプで何ができるか」です。
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
フィールドがあり、どちらも文字列データタイプです。
main.go
... type Article struct { Title string Author string } ...
次に、Article
タイプでString
というメソッドを定義します。 String
メソッドは、Article
タイプを表す文字列を返します。
main.go
... func (a Article) String() string { return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author) } ...
次に、main
function で、Article
タイプのインスタンスを作成し、それをa
という変数に割り当てます。 。 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つのインターフェイスのみを定義することから始めます。 Circle
とSquare
の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 } ...
注: fmt.Stringer
、io.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
これで、必要に応じて、より小さなインターフェイスを作成し、それらをより大きなインターフェイスに構築する方法を見てきました。 より大きなインターフェースから始めて、それをすべての関数に渡すこともできますが、必要な関数に最小のインターフェースのみを送信することがベストプラクティスと見なされます。 これにより、通常、コードがより明確になります。特定の小さなインターフェイスを受け入れるものはすべて、その定義された動作でのみ機能することを意図しているためです。
たとえば、Shaper
をLess
関数に渡した場合、Area
メソッドとString
メソッドの両方を呼び出すと想定できます。 ただし、Area
メソッドのみを呼び出すことを意図しているため、渡された引数のArea
メソッドしか呼び出せないことがわかっているため、Less
関数が明確になります。 。
結論
小さなインターフェースを作成し、それらを大きなインターフェースに構築することで、関数またはメソッドに必要なものだけを共有できるようになることを確認しました。 また、パッケージだけでなく、他のパッケージから定義されたものも含め、他のインターフェイスからインターフェイスを構成できることも学びました。
Goプログラミング言語について詳しく知りたい場合は、Goシリーズのコーディング方法全体を確認してください。