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シリーズのコーディング方法全体を確認してください。