인터페이스 타입

인터페이스 타입 (Interface types)

인터페이스는 Go에서 타입의 행동을 추상화하는 핵심 장치예요. 함수 시그니처나 구조체가 "무엇을 담는지"에 집중한다면, 인터페이스는 "어떤 행동(메서드)을 보장하는지"에 집중하죠. Go 1.18부터는 메서드뿐 아니라 타입 집합까지 다루게 되면서 인터페이스가 훨씬 강력해졌는데, 이번 글에서 그 규칙을 차근차근 살펴볼게요.

출처: Go Specification

본문

인터페이스 타입은 타입 집합(type set)을 정의해요. 인터페이스 타입의 변수는 그 인터페이스의 타입 집합에 속한 어떤 타입의 값이든 저장할 수 있고, 그런 타입을 두고 인터페이스를 구현한다(implement the interface)고 말해요. 초기화되지 않은 인터페이스 타입 변수의 nil이에요.

InterfaceType  = "interface" "{" { InterfaceElem ";" } "}" .
InterfaceElem  = MethodElem | TypeElem .
MethodElem     = MethodName Signature .
MethodName     = identifier .
TypeElem       = TypeTerm { "|" TypeTerm } .
TypeTerm       = Type | UnderlyingType .
UnderlyingType = "~" Type .

인터페이스 타입은 인터페이스 요소(interface element)들의 나열로 지정해요. 인터페이스 요소는 메서드이거나 타입 요소인데, 타입 요소는 하나 이상의 타입 항(term)의 합집합이에요. 타입 항은 단일 타입이거나 단일 기저 타입(underlying type)이에요.

기본 인터페이스 (Basic interfaces)

가장 기본적인 형태에서 인터페이스는 (빈 목록일 수도 있는) 메서드 목록을 지정해요. 이런 인터페이스가 정의하는 타입 집합은 그 모든 메서드를 구현하는 타입들의 집합이고, 대응하는 메서드 집합은 인터페이스가 지정한 메서드들로 정확히 구성돼요. 타입 집합이 전적으로 메서드 목록으로 정의될 수 있는 인터페이스를 기본 인터페이스(basic interface)라고 불러요.

인터페이스 메서드는 타입 파라미터를 선언할 수 없지만, 인터페이스 선언에서 온 타입 파라미터는 사용할 수 있어요.

// A simple File interface.
interface {
	Read([]byte) (int, error)
	Write([]byte) (int, error)
	Close() error
}

명시적으로 지정된 각 메서드의 이름은 유일해야 하고, 빈 이름(blank)이면 안 돼요.

interface {
	String() string
	String() string  // illegal: String not unique
	_(x int)         // illegal: method must have non-blank name
}

하나의 인터페이스를 여러 타입이 구현할 수도 있어요. 예를 들어 두 타입 S1S2가 다음 메서드 집합을 가진다면,

func (p T) Read(p []byte) (n int, err error)
func (p T) Write(p []byte) (n int, err error)
func (p T) Close() error

(여기서 TS1 또는 S2를 뜻해요) S1S2가 다른 메서드를 가지거나 공유하는지와 무관하게, 두 타입 모두 File 인터페이스를 구현해요.

인터페이스의 타입 집합에 속한 모든 타입은 그 인터페이스를 구현해요. 어떤 타입이든 여러 개의 서로 다른 인터페이스를 구현할 수 있죠. 예를 들어 모든 타입은, 모든 (비-인터페이스) 타입의 집합을 뜻하는 빈 인터페이스를 구현합니다:

interface{}

편의를 위해, 미리 선언된(predeclared) 타입 any는 빈 인터페이스의 별칭(alias)이에요. any는 (이름 있는) 지명 타입(named type)이 아니에요. [Go 1.18]

마찬가지로, 타입 선언 안에 나타나 Locker라는 인터페이스를 정의하는 다음 인터페이스 지정을 살펴볼게요:

type Locker interface {
	Lock()
	Unlock()
}

만약 S1S2가 다음 메서드들도 구현한다면,

func (p T) Lock() { … }
func (p T) Unlock() { … }

두 타입은 File 인터페이스뿐 아니라 Locker 인터페이스도 구현해요.

임베디드 인터페이스 (Embedded interfaces)

조금 더 일반적인 형태로, 인터페이스 T는 (한정될 수도 있는) 인터페이스 타입 이름 E를 인터페이스 요소로 사용할 수 있어요. 이를 두고 인터페이스 ET임베딩(embedding)한다고 해요 [Go 1.14]. T의 타입 집합은 T가 명시적으로 선언한 메서드들이 정의하는 타입 집합들과, T가 임베딩한 인터페이스들의 타입 집합들의 교집합이에요. 다시 말해, T의 타입 집합이란 T의 명시적으로 선언된 모든 메서드와 E의 모든 메서드를 전부 구현하는 모든 타입들의 집합이에요 [Go 1.18].

type Reader interface {
	Read(p []byte) (n int, err error)
	Close() error
}

type Writer interface {
	Write(p []byte) (n int, err error)
	Close() error
}

// ReadWriter's methods are Read, Write, and Close.
type ReadWriter interface {
	Reader  // includes methods of Reader in ReadWriter's method set
	Writer  // includes methods of Writer in ReadWriter's method set
}

인터페이스를 임베딩할 때, 같은 이름의 메서드들은 동일한 시그니처(identical signatures)를 가져야 해요.

type ReadCloser interface {
	Reader   // includes methods of Reader in ReadCloser's method set
	Close()  // illegal: signatures of Reader.Close and Close are different
}

일반 인터페이스 (General interfaces)

가장 일반적인 형태에서, 인터페이스 요소는 임의의 타입 항 T이거나, 기저 타입이 T임을 나타내는 ~T 형태의 항, 또는 항들의 합집합 t1|t2|…|tn일 수도 있어요 [Go 1.18]. 메서드 지정과 함께, 이 요소들은 인터페이스의 타입 집합을 다음과 같이 정밀하게 정의할 수 있게 해줘요:

  • 빈 인터페이스의 타입 집합은 모든 비-인터페이스 타입의 집합이에요.
  • 비어 있지 않은 인터페이스의 타입 집합은 그 인터페이스 요소들의 타입 집합들의 교집합이에요.
  • 메서드 지정의 타입 집합은 그 메서드를 메서드 집합에 포함하는 모든 비-인터페이스 타입의 집합이에요.
  • 비-인터페이스 타입 항의 타입 집합은 그 타입 하나로만 이루어진 집합이에요.
  • ~T 형태의 항의 타입 집합은 기저 타입이 T인 모든 타입의 집합이에요.
  • 항들의 합집합 t1|t2|…|tn의 타입 집합은 그 항들의 타입 집합들의 합집합이에요.

"모든 비-인터페이스 타입의 집합"이라는 양화(quantification)는 현재 프로그램에 선언된 모든 (비-인터페이스) 타입만 가리키는 게 아니라, 가능한 모든 프로그램의 가능한 모든 타입을 가리키기 때문에 무한해요. 마찬가지로 특정 메서드를 구현하는 모든 비-인터페이스 타입의 집합이 주어졌을 때, 그 타입들의 메서드 집합들의 교집합은 정확히 그 메서드만을 포함해요 — 현재 프로그램의 모든 타입이 항상 그 메서드를 다른 메서드와 짝 지어 쓰더라도 그렇습니다.

구조상, 인터페이스의 타입 집합은 절대 인터페이스 타입을 포함하지 않아요.

// An interface representing only the type int.
interface {
	int
}

// An interface representing all types with underlying type int.
interface {
	~int
}

// An interface representing all types with underlying type int that implement the String method.
interface {
	~int
	String() string
}

// An interface representing an empty type set: there is no type that is both an int and a string.
interface {
	int
	string
}

~T 형태의 항에서, T의 기저 타입은 T 자신이어야 하고, T는 인터페이스일 수 없어요.

type MyInt int

interface {
	~[]byte  // the underlying type of []byte is itself
	~MyInt   // illegal: the underlying type of MyInt is not MyInt
	~error   // illegal: error is an interface
}

합집합 요소는 타입 집합들의 합집합을 나타내요:

// The Float interface represents all floating-point types
// (including any named types whose underlying types are
// either float32 or float64).
type Float interface {
	~float32 | ~float64
}

T 또는 ~T 형태의 항에서의 타입 T는 타입 파라미터일 수 없고, 모든 비-인터페이스 항들의 타입 집합은 서로 서로소(pairwise disjoint)여야 해요 (타입 집합 간의 쌍별 교집합이 비어 있어야 한다는 뜻). 타입 파라미터 P가 주어졌을 때:

interface {
	P                // illegal: P is a type parameter
	int | ~P         // illegal: P is a type parameter
	~int | MyInt     // illegal: the type sets for ~int and MyInt are not disjoint (~int includes MyInt)
	float32 | Float  // overlapping type sets but Float is an interface
}

구현 제한(Implementation restriction): (두 개 이상의 항을 가진) 합집합은 미리 선언된 식별자 comparable이나, 메서드를 지정하는 인터페이스, 또는 comparable이나 메서드를 지정하는 인터페이스를 임베딩하는 것을 포함할 수 없어요.

기본이 아닌(non-basic) 인터페이스는 타입 제약(type constraints)으로만, 또는 제약으로 쓰이는 다른 인터페이스의 요소로만 사용할 수 있어요. 값이나 변수의 타입이 되거나, 다른 비-인터페이스 타입의 구성 요소가 될 수는 없어요.

var x Float                     // illegal: Float is not a basic interface

var x interface{} = Float(nil)  // illegal

type Floatish struct {
	f Float                 // illegal
}

인터페이스 타입 T는 직간접적으로 T이거나, T를 포함하거나, T를 임베딩하는 타입 요소를 임베딩할 수 없어요.

// illegal: Bad may not embed itself
type Bad interface {
	Bad
}

// illegal: Bad1 may not embed itself using Bad2
type Bad1 interface {
	Bad2
}
type Bad2 interface {
	Bad1
}

// illegal: Bad3 may not embed a union containing Bad3
type Bad3 interface {
	~int | ~string | Bad3
}

// illegal: Bad4 may not embed an array containing Bad4 as element type
type Bad4 interface {
	[10]Bad4
}

인터페이스 구현하기 (Implementing an interface)

타입 T가 인터페이스 I를 구현한다(implements)는 것은 다음 중 하나가 성립할 때예요:

  • T가 인터페이스가 아니면서 I의 타입 집합의 요소이거나;
  • T가 인터페이스이면서 T의 타입 집합이 I의 타입 집합의 부분집합(subset)인 경우.

타입 T의 값은 T가 그 인터페이스를 구현할 때 그 인터페이스를 구현한다고 해요.

더 알아보기