Menu

Golang 인터페이스: 암묵적 구현, any, nil 함정

Go 인터페이스는 암묵적으로 만족됩니다. 알맞은 메서드를 가진 타입이라면 무엇이든 구현한 것입니다. io.Reader와 fmt.Stringer 같은 작은 인터페이스, 빈 인터페이스 any, nil 인터페이스 함정, 그리고 인터페이스를 받고 구조체를 반환하라는 원칙을 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

인터페이스는 메서드의 집합이다

인터페이스 타입은 메서드 시그니처를 나열합니다. 그 메서드를 가진 타입은 따로 선언하지 않아도 인터페이스를 만족합니다.

RectCircleShape를 언급하지 않습니다. 이것이 Go 인터페이스를 정의하는 특징인 암묵적 구현입니다. 다른 패키지의 타입이 이미 만족하는 인터페이스를 그 코드를 건드리지 않고 여러분의 패키지에 정의할 수 있다는 뜻입니다.

표준 라이브러리의 작은 인터페이스

Go 코드는 메서드가 한두 개인 인터페이스를 선호합니다. 가장 중요한 것들:

인터페이스메서드사용하는 곳
fmt.StringerString() stringfmt 출력
errorError() string실패할 수 있는 모든 함수
io.ReaderRead(p []byte) (n int, err error)파일, 네트워크, gzip, HTTP 본문
io.WriterWrite(p []byte) (n int, err error)파일, 버퍼, 해시, HTTP 응답
sort.InterfaceLen, Less, Swapsort 패키지
http.HandlerServeHTTP(w, r)net/http

io.Reader는 메서드가 하나뿐이라서 수십 개의 타입이 이를 구현하고, io.Reader를 받는 함수는 그 모든 타입과 함께 동작합니다:

Go 격언은 "인터페이스가 클수록 추상화는 약해진다"입니다. 큰 인터페이스는 작은 것들을 조합해서 만듭니다. io.ReadWriterReaderWriter를 합친 것이며, 인터페이스 안에 다른 인터페이스를 임베딩해서 작성합니다.

any: 빈 인터페이스

interface{}에는 메서드가 없으므로 모든 타입이 이를 만족합니다. Go 1.18에서 별칭 any가 추가되었고, 둘은 똑같습니다.

any 값은 무엇이든 담을 수 있지만, 타입 단언이나 타입 스위치로 구체 타입을 되찾기 전까지는 할 수 있는 일이 거의 없습니다. 타입의 집합을 알고 있다면 진짜 인터페이스나 제네릭을 쓰세요. any는 모양을 모르는 JSON을 디코딩한 결과처럼 정말로 동적인 데이터와 출력에 어울립니다.

인터페이스 값에 담긴 것

인터페이스 값은 동적 타입과 동적 값의 쌍입니다. var s Shape = Rect{3, 4}는 타입 Rect와 값의 복사본을 저장합니다. s.Area()를 호출하면 런타임에 Rect의 메서드를 찾습니다.

인터페이스는 두 부분이 모두 비어 있을 때만 nil입니다. 이 규칙이 Go에서 가장 헷갈리는 버그를 만듭니다.

nil 인터페이스 함정

인터페이스에 저장한 nil 포인터는 nil이 아닌 인터페이스를 만듭니다.

출력:

false
*main.MyError true
true

validate(true)는 타입 *MyError와 값 nil을 담은 error 인터페이스를 반환합니다. 인터페이스에 타입이 있으므로 nil과 같지 않고, 호출자의 if err != nil 분기가 실행됩니다. 거기서 err.Error()를 호출하면 nil 리시버의 필드에 접근하면서 패닉이 납니다.

해결책은 간단합니다. 변수를 구체 포인터 타입이 아니라 error로 선언하거나, 성공 경로에서 리터럴 nil을 반환하세요. 결과가 error인 함수에서 구체 오류 포인터 타입을 반환하지 마세요. 같은 함정은 오류만이 아니라 모든 인터페이스에 적용됩니다.

타입이 인터페이스를 구현하는지 확인하기

구현 여부는 값이 인터페이스에 대입되는 곳에서 확인됩니다. 아직 그런 코드가 없다면 메서드 시그니처의 실수를 알아차리지 못합니다. 패키지 수준의 빈 식별자 대입으로 확인을 명시하세요:

var _ io.Writer = (*LogWriter)(nil)
var _ fmt.Stringer = Temp(0)

런타임 비용은 없습니다. *LogWriterWrite(p []byte) (int, error) 대신 Write(p []byte) error가 있다면 빌드가 실패합니다:

cannot use (*LogWriter)(nil) (value of type *LogWriter) as io.Writer value in variable declaration: *LogWriter does not implement io.Writer (wrong type for method Write)
		have Write([]byte) error
		want Write([]byte) (int, error)

포인터 리시버와 인터페이스

메서드가 포인터 리시버라면 그 메서드는 포인터 타입에만 있습니다. *Counter는 그 메서드로 인터페이스를 만족하지만 Counter는 그렇지 않습니다. 컴파일러는 Counter does not implement Incrementer (method Inc has pointer receiver)라고 알려 줍니다. 인터페이스에 &Counter{}를 저장하세요. 메서드 집합은 메서드 페이지에서 설명합니다.

인터페이스를 받고, 구조체를 반환하라

흔한 Go 지침입니다. 함수는 인터페이스를 매개변수로 받고 구체 타입을 반환해야 합니다.

  • 인터페이스를 받으면 호출자가 테스트용 가짜 객체를 포함해 맞는 것이면 무엇이든 넘길 수 있습니다. 데이터를 읽는 함수는 *os.File이 아니라 io.Reader를 받아야 합니다.
  • 구체 타입을 반환하면 호출자가 그 타입의 메서드와 필드를 모두 쓸 수 있고, nil 인터페이스 함정도 피할 수 있습니다. os.Openio.Reader가 아니라 *os.File을 반환합니다.

관련된 습관이 하나 있습니다. 인터페이스는 구현하는 곳이 아니라 사용하는 곳에서 정의하세요. 서비스에 사용자를 Get(id)할 수 있는 무언가가 필요하다면, 서비스 패키지에 메서드 하나짜리 인터페이스를 선언하고 데이터베이스 패키지는 구조체만 공개하게 하세요.

인터페이스 값 비교

두 인터페이스 값은 동적 타입이 같고 동적 값이 같을 때 같습니다. 동적 타입이 비교 불가능한 타입(슬라이스, 맵)이라면 ==는 컴파일되지만 런타임에 패닉이 납니다: runtime error: comparing uncomparable type []int.

흔한 실수

  • 타입 있는 nil 포인터를 인터페이스로 반환함. 리터럴 nil을 반환하세요.
  • 너무 이른 인터페이스. 구체 타입부터 작성하세요. 두 번째 구현이나 테스트가 필요로 할 때 인터페이스를 추가하세요.
  • 인터페이스의 포인터. *io.Reader가 맞는 경우는 거의 없습니다. 인터페이스에 포인터를 저장하면 인터페이스가 이미 포인터를 담고 있습니다.
  • 큰 인터페이스. 메서드가 열 개인 인터페이스는 구현하기도, 가짜를 만들기도 어렵습니다. 나누세요.

자주 묻는 질문

Go에서 인터페이스는 어떻게 구현하나요?

인터페이스가 나열한 메서드를 같은 이름과 시그니처로 여러분의 타입에 정의하면 됩니다. implements 키워드는 없습니다. *FileRead(p []byte) (int, error)가 있으면 자동으로 io.Reader입니다. 컴파일러는 값을 인터페이스 타입에 대입하는 곳마다 이를 확인합니다.

Go에서 빈 인터페이스 또는 any란 무엇인가요?

interface{}에는 메서드가 없으므로 모든 타입이 이를 만족합니다. Go 1.18부터 anyinterface{}의 내장 별칭입니다. any 타입의 값은 무엇이든 담을 수 있지만, 구체 타입을 다시 꺼내려면 타입 단언이나 타입 스위치가 필요합니다.

nil 포인터를 대입했는데 Go 인터페이스가 nil이 아닌 이유는 무엇인가요?

인터페이스 값은 타입과 값을 담습니다. nil인 *MyErrorerror에 대입하면 타입은 *MyError이고 값은 nil인 인터페이스가 되며, 이 인터페이스는 nil과 같지 않습니다. 오류가 없을 때는 타입 있는 nil 포인터 대신 리터럴 nil을 반환하세요.

타입이 인터페이스를 구현하는지 컴파일 타임에 확인하려면 어떻게 하나요?

패키지 수준에 빈 식별자 대입을 추가합니다: var _ io.Reader = (*MyReader)(nil). *MyReader에 메서드가 빠져 있으면 빠진 메서드를 알려 주는 메시지와 함께 빌드가 실패합니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기