Go에서 Interface를 사용하는 方法

導言

유연하고 다중 사용 가능하고 모듈이라는 의미의 코드를 작성하는 것은 다양한 프로그램을 开発する 데에 중요합니다. 이러한 방법으로 일어나면 같은 変경을 여러 곳에 대해 하는 것을 避け고 코드를 보호하는 데 도움이 됩니다. 이를 실현하는 方法은 언어에 따라 다르게 나타낼 수 있습니다. 예를 들어, 상속은 Java, C++, C# 등의 언어들에서 사용되는 일반적인 접근法입니다.

開発자는 同样한 디자인 목표를 구성으로 달성할 수 있습니다. 구성은 객체나 데이터 형을 더 複雑한 것으로 结合起来하는 것입니다. 이것은 Go가 코드 재사용, 모듈성, 유연성을 촉진하기 위해 사용하는 방법입니다. Go의 인터페이스는 错綜复杂的한 구성을 organizing하는 方法을 제공합니다. 이를 이용하여 일반적이고 재사용 가능한 코드를 만들 수 있습니다.

이 記事에서는 공통적인 행위를 가지는 사용자 정의 형을 구성하는 方法을 배울 것입니다. 이렇게 我们的 코드를 재사용 할 수 있습니다. 또한 다른 패키지에서 정의한 인터페이스를 満たす 것을 我们的 사용자 정의 형에 구현하는 方法을 배울 것입니다.

행위를 정의하는 方法

컴포지션의 핵심 구현 방법 중 하나는 인터페이스 사용입니다. 인터페이스는 타입의 행동을 정의합니다. Go 표준 라이브러리에서 가장 일반적으로 사용되는 인터페이스 중 하나는 fmt.Stringer 인터페이스입니다:

type Stringer interface {
    String() string
}

코드의 첫 줄은 Stringer라는 type을 정의합니다. 그런 다음, 그것이 interface라고 states합니다. 구조체를 정의하는 것과 마찬가지로, 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
}
...

다음으로, Article 타입에 method으로 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 유형의 인스턴스를 생성하고 variable라는 a 이라는 이름을 가진 変数에 대입합니다. Title 필드에 "Understanding Interfaces in Go"의 값을, Author 필드에 "Sammy Shark"의 값을 제공합니다:

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.

지금까지 인터페이스를 사용하지 않았지만, Behavior을 가진 타입을 생성했습니다. 그 行为的 fmt.Stringer 인터페이스와 일치합니다. 次에, 우리는 코드를 더 이용하기 좋게 만들 수 있는 Behavior을 어떻게 사용할 수 있는지 보겠습니다.

인터페이스 정의

이제 원하는 Behavior을 갖춘 타입을 정의했으며, 이 Behavior을 사용하는 방법에 대해 보겠습니다.

그러나 그 之前, 우리가 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() 이라는 하나의 메서드만 포함되어 있습니다. method은 특정 형에 소속된 스코프가 있는 특수한 함수입니다. 함수와 다르게, 메서드는 정해진 형의 인스턴스에서 だけ 호출할 수 있습니다.

그 다음 Print 方法的 인자를 Stringer로 갱신하고, literal 형 Article로는 안 됩니다. 컴파일러는 Stringer 인터페이스가 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())
}

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 코드를 작성하는 Core Tenant(주요 原则) 之一는 작은, 간결한 형과 그것을 更大, 더 복잡한 형으로 구성하는 것입니다. 인터페이스를 구성하는 것도 마찬가지입니다. 인터페이스를 구성하는 방법을 보기 위해서는 먼저 하나의 인터페이스를 정의하는 것부터 시작합니다. 두 가지 도형, 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
}
...

Sizer 두 개를 받아서 가장 작은 것을 리턴하는 Less 함수를 정의합니다:

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

이것을 보면, 두 인자를 모두 Sizer 형으로 받았지만, 리턴 값도 Sizer로 했습니다. 这意味着 우리는 더 이상 SquareCircle를 리턴하지 않고, 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`インターフェースを composition します。

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

注意: インターフェースの名前に`er`を付けることがidiomaticであることが认知されています。例えば`fmt.Stringer`、`io.Writer`などです。これが理由で、私たちのインターフェースを`Shaper`に名付け、`Shape`ではないのです。

今度は、`Shaper`を引数として持つ関数`PrintArea`を作成します。これは、渡された値に対して`Area`と`String`の両方のメソッドを呼び出すことができます。

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

これで、必要に応じてより小さなインターフェースを作成し、必要な大きさのインターフェースに構築する方法を見てきました。大きなインターフェースを最初に持っておき、すべての関数に渡すこともできますが、必要な最小限のインターフェースだけを関数に渡すことはベストプracticeとされています。これは、特定の小さなインターフェースを受け取る関数が特定の定義された行動しか扱う意图があることを示し、より明快なコードになります。

다음은 Less 함수에서 Shaper 객체를 전달할 때의 예시입니다. 이 경우, 우리는 만약 Shaper 객체가 Area 메thod을 호출하도록 설정했다면, Less 함수는 어떤 argument를 받아도 단지 Area 메thod만 호출할 것임을 알 수 있습니다. 즉, Less 함수가 어떤 argument를 받아도 그 argument에 대한 모든 ethods(Area, String 등)를 호출하지는 않을 것임을 명확히 해줍니다.

결론

우리는 이번에는 작은 interface를 만들고 그 다음으로 larger interface를 만들어 나타내는 방법을 보여주었습니다. 또한, 다른 패키지에 define된 interface(s)를 사용할 수 있는 것을 배웠습니다.

若想了解更多关于Go编程语言的内容,请参考整个《如何在Go中编码》系列

Source:
https://www.digitalocean.com/community/tutorials/how-to-use-interfaces-in-go