2020 TIL no. 8 - Go의 Interface

박경연 (돌진어흥이)·2020년 3월 1일
0

스터디 자료 출처
https://medium.com/rungo/interfaces-in-go-ab1601159b3a

Golang의 Interface란 무엇인가

Golang에서 Struct는 다양한 타입의 필드들로 이루어진 구조체들을 의미하며, Method를 선언하는것으로 해당 구조체의 행동에 대해서 정의할 수 있었다.

Golang의 Interface는, 하나의 타입이 구현해낼 수 있는 Method Signature의 모음이다.
Method Signature는 Method를 구현하기 위해 필요한 이름(name)과 인자(parameter)를 Method Signature라고한다.
Interface의 가장 주된 용도는, Method name, parameter, return value를 명시하는것이다.
고에서는 한 타입이 인터페이스를 구현하는지 언급하지 않는다.(코드에 적지 않는다)
한 타입이 인터페이스내에서 메소드를 구현한다면, 그 타입은 인터페이스를 구현하는 것이다.

if it walks like a duck, swims like a duck and quacks like a duck, then it’s a duck

Interface의 선언

Struct의 선언처럼, Interface의 선언도 타입 선언으로 가능하다

type Shape interface{
	Area() float64
	Perimeter() float64
}

func main() {
	var s Shape
	fmt.Println("value of s is", s)
	fmt.Printf("type of s is %T\n", s)
}


//Result
value of s is <nil>
type of s is <nil>

위의 코드예시에서, Shape Interface는 Area와 Perimeter라는 아무런 인자도 받지않고, float64형의 리턴을 반환하는 메소드로 구성됨을 알 수 있다. 이러한 Method와 Method Signature를 가지고 구현되는 모든 타입은, Shape Interface 또한 구현하게 되는것이다.

Interface의 구현

Shape 인터페이스를 구현하는, Area()와 Perimeter() 메소드를 가진 Shape Struct를 선언해보자.

// Rect ...
type Rect struct {
	width float64
	height float64
}

// Area returns area of rect
func (r Rect) Area() float64{
	return r.width * r.height
}

// Perimeter returns perimeter of rect
func (r Rect) Perimeter() float64{
	return 2 * (r.width + r.height)
}

func main() {
	var s Shape
	s = Rect{5.0, 4.0}
	r := Rect{5.0, 4.0}
	fmt.Println("value of s is", s)
	fmt.Printf("type of s is %T\n", s)
	fmt.Println("Area of rectangle s is", s.Area())
	fmt.Println("s == r is", s == r)
}

//Result
value of s is {5 4}
type of s is main.Rect
Area of rectangle s is 20
s == r is true




위의 코드에서, Shape인터페이스와, Rect타입을 생성하였다. 그리고 나서 Rect type에 속하는 Area와 Perimeter Method를 정의했으니, Rect가 해당 메소드를 구현한것이된다.

이 메소드들을 Shape Interface에서 정의하고 있기 때문에, Rect struct type은 Shape Interface를 구현한것이 된다. 필자는 Rect type이 Shape을 구현하도록 강제하지 않았기 때문에, 이 모든것이 자동으로 이루어진 셈이다. 나아가 Go에서는 Interface들이 내포되어 구현된다고 말할 수 있다.

한 타입이 인터페이스를 구현하면, 그 타입의 변수도 Interface의 타입으로 표현될 수 있다. 이는 nil타입이었던 인터페이스 Shape타입의 s가 Rect struct타입으로 지정되는 것을 통해 확인 가능하다. 이는 s의 동적 타입은 Rect이며, s의 동적 값은 {5,4} 가 된 것이라고도 표현할 수 있다.

이를 다형성 이라고 하며, 언어의 각 요소가 다양한 자료형을 가질 수 있음을 말한다

동적 타입을 콘크리트 타입이라고 하기도 하는데, 이는 인터페이스의 타입을 호출했을 때, 정적타입은 가려진체로 있고, 내재된 동적 값의 타입을 리턴하기 때문이다.

이제 s의 동적값과 동적타입을 바꾸어보자

// Circle ...
type Circle struct {
	radius float64
}

// Area returns area of Circle
func (c Circle) Area() float64{
	return math.Pi * c.radius * c.radius
}

// Perimeter returns perimeter of Circle
func (c Circle) Perimeter() float64{
	return math.Pi * 2 * c.radius
}

func main() {
	var s Shape = Rect{10, 3}
	fmt.Printf("type of s is %T\n", s)
	fmt.Printf("value of s is %v\n", s)
	fmt.Printf("value of s is %0.2f\n\n", s.Area())

	s = Circle{10}
	fmt.Printf("type of s is %T\n", s)
	fmt.Printf("value of s is %v\n", s)
	fmt.Printf("value of s is %0.2f\n", s.Area())
}

//Result
type of s is main.Rect
value of s is {10 3}
value of s is 30.00

type of s is main.Circle
value of s is {10}
value of s is 314.16

이처럼 Circle또한 Shape Interface를 구현할 수 있으므로, s에 struct type Circle을 할당할 수있다.

그렇다면 type이 Interface를 구현하지 못하면 어떻게 될까?
이미 선언해놓은 Rect의 메소드중 Perimeter()를 주석처리하면, 결과는 다음과 같다.

# command-line-arguments
./main.go:46:6: cannot use Rect literal (type Rect) as type Shape in assignment:
        Rect does not implement Shape (missing Perimeter method)

이를 통해 인터페이스를 구현하기 위해서는 인터페이스에 의해 선언된 메소드를 모두 가지고 있어야만 가능하다는 것을 알 수 있다.

Empty Interface

인터페이스가 한 개의 메소드도 가지고 있지 않을 경우, Empty Interface라고 부르며, 다음과 같이 표현된다.

Inteface{}

인터페이스가 메소드를 하나도 가지지 않기때문에, 내부적으로 모든 타입을 구현하는 것이라고 볼 수 있다.

fmt패키지의 Println함수가 어떻게 다른 종류 타입의 값들을 받아들이는지 궁금하다면 그 해답또한 여기에 있다.

(VSCode기준 cmd + 클릭) Println의 Signature를 확인하면, 다음과 같다

func Println(a ...Interface{}) (n int, err error)

Println은 많은 수의 Interface타입을 인자로 받아들일 수 있는 Variadic Function이다.

package main

import (
	"fmt"
)

// MyString ...
type MyString string

// Rect ...
type Rect struct {
	width float64
	height float64
}

func explain(i interface{}) {
	fmt.Printf("Value given to explain function is of Type %T with value %v\n", i,i)
}

func main() {
	ms := MyString("Hello World")
	r := Rect{5.0, 4.0}
	explain(ms)
	explain(r)
}

//Result
Value given to explain function is of Type main.MyString with value Hello World
Value given to explain function is of Type main.Rect with value {5 4}

위의 프로그램에서, 커스텀 스트링 타입 MyString과 스트럭트 타입 Rect를 만들었다. explain함수는 Empty Interface를 인자로 받으며, Empty Inteface는 모든 타입으로 구현할 수 있으므로, 이런것이 가능하다.

Multiple Interfaces

하나의 타입이 여러가지 Interface를 구현하는 것도 가능하다.

package main

import (
	"fmt"
)

// Shape ...
type Shape interface{
	Area() float64
}

// Object ...
type Object interface{
	Volume() float64
}

// Cube ...
type Cube struct{
	side float64
}


// Area ...
func (c Cube) Area() float64{
	return 6 * (c.side * c.side)
}

// Volume ...
func (c Cube) Volume() float64{
	return c.side * c.side * c.side
}

func main() {
	c:= Cube{3}
	var s Shape = c
	var o Object = c
	fmt.Println("volume of interface of type Shape is", s.Area())
	fmt.Println("volume of interface of type Object is", o.Volume())
}

//Result
volume of interface of type Shape is 54
volume of interface of type Object is 27

위의 프로그램에서는 Area 메소드를 가진 Shape 인터페이스와, Volume 메소드를 가진 Object를 만들었다. struct type Cube는 이 두가지 메소드를 모두 가졌기 때문에 이 두가지 인터페이스를 모두 구현하는 것이라고 볼 수있다.

변수 s의 정적 타입은 Shape이며, o의 정적타입은 Object이기 때문에, s.Volume()이나, o.Side를 구하려하면 에러가 난다.

이런것이 가능하려면, 어떻게든 인터페이스들의 동적 타입인 Cube를 뽑아내어 사용하여야 하는데, 이는 Type assertion이라는 것으로 가능하다.

Type Assertion

다음과 같은 문법으로 인터페이스의 동적 값을 얻어낼 수 있다.

i.(Type)

i는 인터페이스 타입의 변수이고, Type은 인터페이스를 구현하는 타입이다.

package main

import (
	"fmt"
)

// Shape ...
type Shape interface{
	Area() float64
}

// Object ...
type Object interface{
	Volume() float64
}

// Cube ...
type Cube struct{
	side float64
}

// Area ...
func (c Cube) Area() float64{
	return 6 * (c.side * c.side)
}

// Volume ...
func (c Cube) Volume() float64{
	return c.side * c.side * c.side
}

func main() {
	var s Shape = Cube{3}
	c := s.(Cube)
	fmt.Println("volume of interface of type Shape is", c.Area())
	fmt.Println("volume of interface of type Object is", c.Volume())
}

///Result
volume of interface of type Shape is 54
volume of interface of type Object is 27

위의 코드에서 Shape타입의 변수인 s는 동적 값으로 struct 타입의 Cube를 가진다.
이를 s.(Cube) 문법으로 변수 c에 담아내고, Cube type은 Volume과 Area메소드를 모두 가지고 있기 때문에, c.Volume이나, c.Area를 모두 사용할 수 있다.

Type이 인터페이스를 구현하지만 i는 Concrete Value를 가지지 않을 경우, i의 현재 값은 nil일 것이기 때문에, 런타임 에러가 발생할 것이다.

//s가 값을 가지지 않은 체 실행시킬 경우

panic: interface conversion: main.Shape is nil, not main.Cube

이를 막기위해 다음과 같은 문법을 사용할 수 있다.

value, ok := i.(Type)

위의 문법에서, ok변수를 통해 i가 동적 타입인 Type을 가지고있는지 아닌지를 확인할 수 있다. 만약 아니라면, value는 Type의 ZeroValue를, ok변수는 false를 가진다.

package main

import (
	"fmt"
)

// Shape ...
type Shape interface{
	Area() float64
}

// Object ...
type Object interface{
	Volume() float64
}

// Skin ...
type Skin interface{
	Color() float64
}

// Cube ...
type Cube struct{
	side float64
}

// Area ...
func (c Cube) Area() float64{
	return 6 * (c.side * c.side)
}

// Volume ...
func (c Cube) Volume() float64{
	return c.side * c.side * c.side
}

func main() {
	var s Shape = Cube{3}
	value1, ok1 := s.(Object)
	fmt.Printf("dynamic value of Shape s with value %v implements interface object? %v\n", value1, ok1)
	value2, ok2 := s.(Skin)
	fmt.Printf("dynamic value of Shape s with value %v implements interface object? %v\n", value2, ok2)
}

//Result
dynamic value of Shape s with value {3} implements interface object? true
dynamic value of Shape s with value <nil> implements interface object? false

Cube가 Object interface를 구현하기 때문에, 첫번째 Type assertion은 성공한다. value1의 값은 Object type의 동적 값인 {3}이며, ok1은 성공했기 때문에 true가 된다.
반대로 Cube는 Skin Object를 구현하지 않기때문에, 두번째 Type assertion은 실패한다. value2의 값은 인터페이스의 zero value인 nil이 되며, ok2는 실패했기 때문에 false가 된다.

Type assertion은 해당 인터페이스가 주어진 타입들 중에서 동적값을 가지고있는지 확인하는 역할도 하지만, 주어진 인터페이스 타입의 값을 다른 타입의 인터페이스로 바꾸는 역할도 한다.

Type switch

위에서 사용했던 explain에서 우리는 empty interface를 사용하는 법을 배웠다. 여기서 만약, 받아온 인자가 string type일 경우, 대문자로 표현하고 싶다면 어떻게 해야할까?

strings 패키지의 ToUpper함수가 있지만 이는 string을 인자로 받기때문에 불가능하다. 이를 위해서는 explain함수 내부에서 받아온 인자가 string인지를 확신할 수 있어야한다.

이럴때 Type switch를 사용할 수 있다. Type switch의 문법은 Type assertion과 비슷하다.

package main

import (
	"fmt"
	"strings"
)

func explain(i interface{}) {
	switch i.(type) {
	case string:
		fmt.Println("i stored string ", strings.ToUpper(i.(string)))
	case int:
		fmt.Println("i stored int ", i)
	default:
		fmt.Println("i stored something else ", i)
	}
}

func main() {
	explain("this is string")
	explain(12)
	explain(true)
}

//Result
i stored string  THIS IS STRING
i stored int  12
i stored something else  true

위의 코드가 Type switch의 예시이다. Type switch를 통해 우리는 동적 타입에 접근할 수 있다.

Embedding Interfaces

go에서는 한 인터페이스가 다른 인터페이스를 구현하거나 확장할 수 없다. 그러나 두 개 이상의 인터페이스들을 병합하는 새로운 인터페이스를 만들 수는 있다.

package main

import (
	"fmt"
)

// Shape ...
type Shape interface{
	Area() float64
}

// Object ...
type Object interface{
	Volume() float64
}

// Material ...
type Material interface{
	Shape
	Object
}

// Cube ...
type Cube struct{
	side float64
}

// Area ...
func (c Cube) Area() float64{
	return 6 * (c.side * c.side)
}

// Volume ...
func (c Cube) Volume() float64{
	return c.side * c.side * c.side
}

func main() {
	c:= Cube{3}
	var m Material = c
	var s Shape = c
	var o Object = c
	fmt.Printf("dynamic type and value of m of static type Material is %T and %v\n", m, m)
	fmt.Printf("dynamic type and value of s of static type Shape is %T and %v\n", s, s)
	fmt.Printf("dynamic type and value of o of static type Object is %T and %v\n", o, o)
}

//result
dynamic type and value of m of static type Material is main.Cube and {3}
dynamic type and value of s of static type Shape is main.Cube and {3}
dynamic type and value of o of static type Object is main.Cube and {3}

위의 코드에서 큐브는 Area와 Volume메소드를 구현하기때문에, Shape과 Object인터페이스를 구현한다. Material 인터페이스가 이 두 인터페이스들을 포함하기 때문에, Cube는 Material또한 구현한다.

Value vs Pointer receiver

지금까지의 예시에서는 모두 Value receiver를 사용해왔다.
그렇다면 Pointer receiver를 사용해도 괜찮은걸까?

package main

import "fmt"

// Shape ...
type Shape interface {
	Area() float64
	Perimeter() float64
}

// Rect ...
type Rect struct {
	width  float64
	height float64
}

// Area ...
func (r *Rect) Area() float64 {
	return r.width * r.height
}

// Perimeter ...
func (r Rect) Perimeter() float64{
	return 2 * (r.width + r.height)
}

func main() {
	r:= Rect{5.0, 4.0}
	var s Shape = r
	area := s.Area()
	fmt.Println("Area of Rectangle is", area)
}

//Result
# command-line-arguments
./main.go:29:6: cannot use r (type Rect) as type Shape in assignment:

위의 코드에서 Area메소드는 *Rect 타입에 속하고, 그것의 receiver는 Rect타입의 변수의 포인터를 가진다. 하지만 컴파일 에러가 난다. 왜일까?

struct type에서의 메소드는, pointer receiver를 써서 선언할 경우, pointer receiver와 value receiver에서 모두 작동한다. 그러나 인터페이스는 다르다.

인터페이스의 메소드 자체를 호출할때, 런타임에서 동적타입이 고려되고, 동적 값이 메소드의 리시버로 넘겨지게 된다. s의 동적타입은 Rect 이고, Rect는 Area 메소드를 구현하는것이 아닌, *Area를 구현한다.

이 코드가 작동하려면, 인터페이스 변수인 s 에 Rect type의 값이 아닌, Rect typea의 포인터인 *Rect를 할당해야한다.

package main

import "fmt"

// Shape ...
type Shape interface {
	Area() float64
	Perimeter() float64
}

// Rect ...
type Rect struct {
	width  float64
	height float64
}

// Area ...
func (r *Rect) Area() float64 {
	return r.width * r.height
}

// Perimeter ...
func (r Rect) Perimeter() float64{
	return 2 * (r.width + r.height)
}

func main() {
	r:= Rect{5.0, 4.0}
	var s Shape = &r
	area := s.Area()
	perimeter := s.Perimeter()
	fmt.Println("Area of Rectangle is", area)
	fmt.Println("Perimeter of Rectangle is", perimeter)
}

//Result
Area of Rectangle is 20
Perimeter of Rectangle is 18
profile
Back-end Developer, pursuing to be a steadily improving person.

2개의 댓글

comment-user-thumbnail
2020년 3월 25일

좋은글 감사합니다.

답글 달기
comment-user-thumbnail
2021년 1월 20일

정말 정리가 잘 되어있네요 감사합니다

답글 달기