x.(T): 인터페이스에서 값 꺼내기
인터페이스 값은 자신이 담은 구체 타입을 숨깁니다. 타입 단언 x.(T)는 그것을 돌려 달라고 요청합니다.
출력:
gopher 6
0 false
string of length 6
x는 인터페이스 타입이어야 합니다. 구체 타입에 단언하면 컴파일에 실패합니다: invalid operation: s (variable of type string) is not an interface.
값 하나를 받는 형태는 패닉이 난다
단언이 틀리면 값 하나를 받는 형태는 두 타입을 모두 적은 메시지와 함께 패닉을 일으킵니다:
출력:
recovered: interface conversion: interface {} is int, not string
다른 타입이 들어오면 프로그래밍 오류로 보고 프로그램을 멈추고 싶을 때만 값 하나를 받는 형태를 쓰세요. 그 밖의 모든 경우에는 comma-ok를 씁니다. 실패하면 comma-ok는 T의 제로 값과 false를 줍니다.
nil 인터페이스에 대한 단언도 실패합니다. var v any; v.(string)은 interface conversion: interface {} is nil, not string으로 패닉이 나고, comma-ok 형태는 "", false를 반환합니다.
다른 인터페이스로 단언하기
T는 인터페이스 타입일 수도 있습니다. 이때 단언은 동적 값이 T를 구현하면 성공하고, 결과는 같은 동적 값을 유지합니다. 표준 라이브러리는 이 방식으로 선택적인 기능을 확인합니다.
io.Copy는 소스가 io.WriterTo를 구현하는지 확인하고, 구현한다면 그것을 씁니다. fmt는 Stringer와 error를 확인합니다. 이 패턴 덕분에 작은 인터페이스는 작게 유지하면서도, 호출하는 쪽은 더 풍부한 구현의 이점을 누릴 수 있습니다.
타입 스위치
값이 여러 타입 중 하나일 수 있다면 타입 스위치가 단언의 연쇄를 대신합니다. x.(type)은 switch 안에서만 쓸 수 있습니다.
출력:
hi
integer 7
integer 8
3.14
nil
error: boom
unhandled []int
알아 둘 규칙:
- 타입이 하나인 case에서
x는 그 타입입니다. 여러 타입을 나열한 case와default에서x는 원래 인터페이스 타입(여기서는any)입니다. - case는 순서대로 검사됩니다. 한 값이 여러 인터페이스를 만족할 수 있으므로, 더 구체적인 인터페이스를 넓은 인터페이스보다 앞에 두세요.
case nil은 nil 포인터를 담은 인터페이스가 아니라 nil 인터페이스에만 일치합니다.- 타입 스위치에는
fallthrough가 없습니다.
errors.As: 감싼 오류를 위한 단언
오류는 흔히 맥락과 함께 감싸집니다: fmt.Errorf("load config: %w", err). 직접적인 타입 단언은 가장 바깥 오류만 보고 안에 있는 오류를 놓칩니다. errors.As는 체인을 따라갑니다.
출력:
type assertion finds it: false
errors.As finds it: open /no/such/file
오류에 타입 단언을 쓰는 것은 오류가 감싸지지 않았다고 확신할 때뿐이며, 실제로는 거의 없는 일입니다. errors.As, errors.Is와 직접 오류 타입을 정의하는 방법은 커스텀 오류 페이지에서 다룹니다.
단언, 변환, 제네릭
| 가진 것 | 원하는 것 | 사용할 것 |
|---|---|---|
int | float64 | 변환: float64(n) |
int를 담은 any | 그 int | 단언: v.(int) |
여러 타입이 올 수 있는 any | 타입에 따라 분기 | 타입 스위치 |
감싸졌을 수 있는 error | 특정 오류 타입 | errors.As |
| 여러 타입에 동작하는 함수 | 컴파일 타임 안전성 | 제네릭 |
any와 타입 스위치로 가득한 코드는 제네릭이나 제대로 된 인터페이스가 의도를 더 잘 표현하고 실수를 컴파일 타임에 잡을 수 있다는 신호인 경우가 많습니다.
흔한 실수
- 신뢰할 수 없는 데이터에 패닉이 나는 형태를 씀. 디코딩한 JSON,
any타입의 맵 값, 플러그인 입력에는 항상 comma-ok를 쓰세요. - 엉뚱한 숫자 타입으로 단언함. JSON 숫자는
any로 디코딩될 때float64가 되므로v.(int)는 실패합니다. - 포인터가 저장되어 있는데 값 타입으로 단언함. 인터페이스가
*User를 담고 있다면v.(User)는 실패합니다.v.(*User)로 단언하세요. - 오류에 타입 단언을 씀.
errors.As를 쓰세요.
자주 묻는 질문
Go에서 타입 단언이란 무엇인가요?
x가 인터페이스 값일 때 쓰는 x.(T) 표현식입니다. T가 구체 타입이면 x에 저장된 값을 T로 반환합니다. T가 인터페이스 타입이면 저장된 값이 T도 구현하는지 확인합니다. 값 하나를 받는 형태는 확인에 실패하면 패닉이 나고, 값 둘을 받는 형태 v, ok := x.(T)는 실패를 ok로 알려 줍니다.
Go에서 인터페이스 값의 타입은 어떻게 확인하나요?
타입 스위치를 씁니다: switch v := x.(type) { case int: ...; case string: ...; default: ... }. 각 case 안에서 v는 그 case의 타입을 가집니다. 타입 하나만 확인한다면 comma-ok 단언 s, ok := x.(string)이 더 짧습니다. 디버깅용으로는 fmt.Printf("%T", x)가 타입 이름을 출력합니다.
Go에서 타입 단언과 타입 변환은 무엇이 다른가요?
변환 T(x)는 float64(n)처럼 한 타입의 값을 다른 타입으로 바꿉니다. 허용 여부는 컴파일러가 정하고 런타임에 실패하지 않습니다. 단언 x.(T)는 인터페이스 값에만 쓸 수 있고, 안에 저장된 타입을 런타임에 확인합니다. int를 담은 any에 int(x)라고 쓸 수는 없고, x.(int)가 필요합니다.
오류 타입을 확인할 때 타입 단언을 써야 하나요?
아니요. errors.As(err, &target)를 쓰세요. 직접 단언하는 err.(*MyError)는 바깥 오류만 보므로, 오류가 fmt.Errorf("...: %w", err)로 감싸지는 순간 실패합니다. errors.As는 감싼 체인을 따라갑니다.