序章

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

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

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

動作の定義

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

type Stringer interface {
    String() string
}

コードの最初の行は、 type と呼ばれる Stringer. それからそれはそれが 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. このタイプには TitleAuthor フィールドと両方が文字列データ型です。

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

次に、メソッドを定義します。 StringArticle タイプ。 The String メソッドは、を表す文字列を返します Article タイプ:

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

次に、私たちの main 関数、インスタンスを作成します Article 入力して、呼び出された変数に割り当てます a. 私たちはの値を提供します "Understanding Interfaces in Go" のために Title フィールド、および "Sammy Shark" のために Author 分野:

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

次に、結果を印刷します String 呼び出しによるメソッド fmt.Println との結果を渡す a.String() メソッド呼び出し:

main.go
...
fmt.Println(a.String())

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

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

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

インターフェイスの定義

目的の動作で型を定義したので、その動作の使用方法を確認できます。

ただし、その前に、 String からの方法 Article 関数を入力します:

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())
}

このコードでは、という新しい関数を追加します Print それはかかります Article 引数として。 唯一のものに注意してください 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
}
...

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

次に、署名を更新します Print 取る方法 Stringer、具体的なタイプではありません Article. コンパイラはそれを知っているので Stringer インターフェイスは、 String メソッド、それはまた持っているタイプのみを受け入れます String 方法。

これで、 Print を満たすものを使用するメソッド 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)
}

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())
}

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

Output
The "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つのインターフェイスのみを定義することから始めます。 2つの形状を定義します。 CircleSquare、そしてそれらは両方とも呼ばれるメソッドを定義します 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
}
...

次に、という関数を定義します Less それは2つかかります Sizer 最小のものを返します:

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 タイプは両方を実装します AreaString メソッドでは、その幅広い動作のセットを記述するための別のインターフェースを作成できるようになりました。 これを行うには、というインターフェイスを作成します Shaper. これを構成します Sizer インターフェイスと fmt.Stringer インターフェース:

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

注:で終わることでインターフェースに名前を付けようとするのは慣用的と見なされます er、 そのような fmt.Stringer, io.Writer、など。 これが、インターフェイスに名前を付けた理由です Shaper、ではなく Shape.

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

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

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

Output
area 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 関数、私たちはそれが両方を呼び出すつもりであると仮定するかもしれません AreaString メソッド。 ただし、 Area メソッド、それは Less 呼び出すことしかできないことがわかっているので、関数は明確です Area 渡された引数のメソッド。

結論

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

Goプログラミング言語について詳しく知りたい場合は、Goシリーズのコーディング方法全体を確認してください。