가장 단순한 형태에서 인터페이스 타입은 메서드 시그니처의 집합이에요.
다음은 Add와 Value 두 메서드를 포함하는 인터페이스 정의의 예시예요:
type Counter interface {
Add(increment int)
Value() int
}
increment와 같은 매개변수 이름은 인터페이스 정의에서 생략할 수 있지만, 이름을 붙이면 가독성이 좋아지는 경우가 많아요.
Go에서 인터페이스 이름에는 Interface나 I라는 단어를 넣지 않아요.
대신 Reader, Stringer처럼 er로 끝나는 경우가 많아요.
인터페이스의 메서드를 정의하는 타입은 자동으로, 암묵적으로 그 인터페이스를 "구현"해요.
Go에는 implements 키워드가 없어요.
다음 타입은 위에서 본 Counter 인터페이스를 구현해요.
type Stats struct {
value int
// ...
}
func (s Stats) Add(v int) {
s.value += v
}
func (s Stats) Value() int {
return s.value
}
func (s Stats) SomeOtherMethod() {
// The type can have additional methods not mentioned in the interface.
}
인터페이스를 구현하는 데에는 메서드가 값 리시버를 갖는지 포인터 리시버를 갖는지는 중요하지 않아요. (이 부분이 확실하지 않다면 메서드 개념을 다시 살펴봐요.)
인터페이스 타입의 값은 그 메서드들을 구현하는 어떤 값이든 담을 수 있어요. 1
즉, 이제 Counter 인터페이스를 기대하는 모든 곳에서 Stats를 사용할 수 있어요.
func SetUpAnalytics(counter Counter) {
// ...
}
stats := Stats{}
SetUpAnalytics(stats)
// works because Stats implements Counter
함수의 매개변수와 반환 값이 모두 구체적인(즉, 인터페이스가 아닌) 타입을 써야 한다면, 재사용성은 크게 떨어질 거예요. 매개변수에 인터페이스 타입을 사용하면, 함수가 제 일을 하는 데 필요한 동작만 정의하면 돼요. 그러면 그 구체 타입들이 인터페이스만 구현하고 있다면, 서로 다른 구체 타입의 매개변수와 함께 함수를 사용할 수 있어요. 인터페이스 타입을 반환 값으로 쓸 때도 같은 논리가 적용돼요.
인터페이스는 암묵적으로 구현되기 때문에, 하나의 타입이 여러 인터페이스를 쉽게 구현할 수 있어요. 필요한 메서드만 모두 정의하면 돼요.
인터페이스 정의에는 다른 인터페이스를 포함할 수 있어요. 다음은 표준 라이브러리에 있는 예시예요.
type ReadCloser interface {
Reader
Closer
}
// Based on these interfaces:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
ReadCloser를 정의할 때 Read와 Close의 메서드 시그니처를 반복해서 쓰는 대신, 인터페이스 정의는 기존의 Reader와 Closer 정의를 활용해요.
Go에는 아주 특별한 인터페이스 타입이 하나 있는데, 메서드를 하나도 갖지 않는 빈 인터페이스 타입이에요.
빈 인터페이스는 interface{}처럼 써요.
Go 1.18 이상에서는 any도 쓸 수 있어요.
any는 별칭으로 정의되었어요.
빈 인터페이스에는 메서드가 없기 때문에, 모든 타입이 이를 암묵적으로 구현해요. 이는 어떤 값이든 일반적으로 받을 수 있는 함수를 정의할 때 유용해요. 이럴 때는 함수 매개변수에 빈 인터페이스 타입을 사용해요.
예를 들어:
func AnythingGoes(i interface{}) string {
if i == 0 {
return "zero"
}
if i == "" {
return "empty string"
}
return "something else"
}