타입 단언과 타입 스위치
인터페이스 값에서 구체적인 타입을 꺼내거나 타입에 따라 분기하는 것이 타입 단언(type assertion)과 타입 스위치(type switch)입니다. 이를 통해 Go에서 다형성을 안전하게 구현할 수 있습니다.
타입 단언(Type Assertion)
인터페이스 값이 특정 타입임을 주장하고 해당 타입의 값을 꺼냅니다.
package main
import "fmt"
type Animal interface {
Sound() string
}
type Dog struct{ Name string }
type Cat struct{ Name string }
func (d Dog) Sound() string { return "멍멍" }
func (c Cat) Sound() string { return "야옹" }
func main() {
var a Animal = Dog{Name: "바둑이"}
// 1. 단순 타입 단언 — 실패 시 패닉!
dog := a.(Dog)
fmt.Println(dog.Name, dog.Sound()) // 바둑이 멍멍
// 2. 안전한 타입 단언 — ok 패턴 (권장)
if dog, ok := a.(Dog); ok {
fmt.Println("Dog:", dog.Name)
} else {
fmt.Println("Dog가 아닙니다")
}
if cat, ok := a.(Cat); ok {
fmt.Println("Cat:", cat.Name)
} else {
fmt.Println("Cat이 아닙니다") // 출력됨
}
// 3. 잘못된 타입 단언 — 패닉 발생
defer func() {
if r := recover(); r != nil {
fmt.Println("패닉 복구:", r)
}
}()
_ = a.(Cat) // panic: interface conversion: interface {} is Dog, not Cat
}
안전한 타입 단언 패턴
실무에서는 항상 ok 패턴을 사용합니다.
package main
import "fmt"
type Shape interface {
Area() float64
}
type Circle struct{ Radius float64 }
type Rectangle struct{ Width, Height float64 }
type Triangle struct{ Base, Height float64 }
func (c Circle) Area() float64 { return 3.14159 * c.Radius * c.Radius }
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func (t Triangle) Area() float64 { return 0.5 * t.Base * t.Height }
// 인터페이스 추가 메서드 확인
type Resizable interface {
Resize(factor float64) Shape
}
func (c Circle) Resize(factor float64) Shape {
return Circle{Radius: c.Radius * factor}
}
func processShape(s Shape) {
fmt.Printf("기본 넓이: %.2f\n", s.Area())
// 추가 기능 지원 여부 확인
if r, ok := s.(Resizable); ok {
bigger := r.Resize(2.0)
fmt.Printf("2배 크기 넓이: %.2f\n", bigger.Area())
} else {
fmt.Println("이 도형은 크기 조절을 지원하지 않습니다")
}
}
func main() {
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 4, Height: 6},
Triangle{Base: 3, Height: 4},
}
for _, s := range shapes {
fmt.Printf("=== %T ===\n", s)
processShape(s)
}
}
타입 스위치(Type Switch)
여러 타입에 따라 분기할 때 switch v.(type) 구문을 사용합니다.
package main
import "fmt"
func describe(i any) string {
switch v := i.(type) {
case nil:
return "nil"
case int:
return fmt.Sprintf("정수: %d", v)
case float64:
return fmt.Sprintf("실수: %.2f", v)
case bool:
if v {
return "불리언: true"
}
return "불리언: false"
case string:
return fmt.Sprintf("문자열: %q (길이: %d)", v, len(v))
case []int:
return fmt.Sprintf("int 슬라이스: %v (길이: %d)", v, len(v))
case map[string]any:
return fmt.Sprintf("맵: %v", v)
case error:
return fmt.Sprintf("에러: %s", v.Error())
default:
return fmt.Sprintf("알 수 없는 타입: %T = %v", v, v)
}
}
func main() {
values := []any{
nil,
42,
3.14,
true,
false,
"hello",
[]int{1, 2, 3},
map[string]any{"key": "value"},
fmt.Errorf("에러 발생"),
struct{ X, Y int }{1, 2},
}
for _, v := range values {
fmt.Println(describe(v))
}
}
타입 스위치 — 인터페이스 조합
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
}
type Circle struct{ Radius float64 }
type Rectangle struct{ Width, Height float64 }
type Triangle struct{ A, B, C float64 } // 세 변의 길이
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (t Triangle) Area() float64 {
// 헤론의 공식
s := (t.A + t.B + t.C) / 2
return math.Sqrt(s * (s - t.A) * (s - t.B) * (s - t.C))
}
// 타입별 상세 정보 출력
func shapeDetail(s Shape) string {
switch v := s.(type) {
case Circle:
return fmt.Sprintf("원(반지름=%.1f, 지름=%.1f)", v.Radius, v.Radius*2)
case Rectangle:
return fmt.Sprintf("사각형(%.1f×%.1f)", v.Width, v.Height)
case Triangle:
return fmt.Sprintf("삼각형(변: %.1f, %.1f, %.1f)", v.A, v.B, v.C)
default:
return fmt.Sprintf("미지의 도형(%T)", v)
}
}
// 여러 타입을 한 케이스에서 처리
func isPolygon(s Shape) bool {
switch s.(type) {
case Rectangle, Triangle:
return true
default:
return false
}
}
func main() {
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 3, Height: 4},
Triangle{A: 3, B: 4, C: 5},
}
for _, s := range shapes {
fmt.Printf("%s\n 넓이=%.2f, 다각형=%v\n",
shapeDetail(s), s.Area(), isPolygon(s))
}
}
JSON 파싱의 타입 단언
실제 현업에서 자주 쓰이는 JSON 파싱 후 타입 단언 패턴입니다.
package main
import (
"encoding/json"
"fmt"
)
func parseJSON(data string) {
var result any
if err := json.Unmarshal([]byte(data), &result); err != nil {
fmt.Println("파싱 에러:", err)
return
}
// JSON 파싱 결과는 map[string]any, []any, float64, string, bool, nil
processValue("root", result)
}
func processValue(key string, v any) {
switch val := v.(type) {
case map[string]any:
fmt.Printf("%s: {객체}\n", key)
for k, child := range val {
processValue(key+"."+k, child)
}
case []any:
fmt.Printf("%s: [배열, 길이=%d]\n", key, len(val))
for i, item := range val {
processValue(fmt.Sprintf("%s[%d]", key, i), item)
}
case float64:
fmt.Printf("%s: 숫자=%v\n", key, val)
case string:
fmt.Printf("%s: 문자열=%q\n", key, val)
case bool:
fmt.Printf("%s: 불리언=%v\n", key, val)
case nil:
fmt.Printf("%s: null\n", key)
}
}
func main() {
jsonData := `{
"name": "홍길동",
"age": 30,
"active": true,
"scores": [95.5, 87.0, 92.3],
"address": {
"city": "서울",
"zip": "04524"
},
"memo": null
}`
parseJSON(jsonData)
}
errors.As와 errors.Is — 에러 타입 단언
Go의 에러 처리에서 타입 단언이 사용되는 실전 패턴입니다.
package main
import (
"errors"
"fmt"
)
// 커스텀 에러 타입
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("검증 오류 [%s]: %s", e.Field, e.Message)
}
type NotFoundError struct {
Resource string
ID int
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s(ID=%d)을 찾을 수 없습니다", e.Resource, e.ID)
}
var ErrPermissionDenied = errors.New("권한이 없습니다")
func findUser(id int) error {
if id <= 0 {
return &ValidationError{Field: "id", Message: "양수여야 합니다"}
}
if id > 100 {
return fmt.Errorf("사용자 조회 실패: %w", &NotFoundError{Resource: "User", ID: id})
}
if id == 13 {
return fmt.Errorf("접근 차단: %w", ErrPermissionDenied)
}
return nil
}
func handleError(err error) {
if err == nil {
fmt.Println("성공!")
return
}
// errors.As: 특정 타입으로 언래핑
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf("입력값 오류 — 필드: %s, 내용: %s\n", ve.Field, ve.Message)
return
}
var nfe *NotFoundError
if errors.As(err, &nfe) {
fmt.Printf("리소스 없음 — %s ID=%d\n", nfe.Resource, nfe.ID)
return
}
// errors.Is: 특정 에러 값 확인
if errors.Is(err, ErrPermissionDenied) {
fmt.Println("권한 오류 — 로그인 필요")
return
}
fmt.Println("알 수 없는 오류:", err)
}
func main() {
testCases := []int{-1, 42, 999, 13}
for _, id := range testCases {
fmt.Printf("ID=%d: ", id)
handleError(findUser(id))
}
}
인터페이스 업캐스팅과 다운캐스팅
package main
import "fmt"
type Base interface {
BaseMethod() string
}
type Extended interface {
Base
ExtendedMethod() string
}
type MyType struct {
value string
}
func (m MyType) BaseMethod() string { return "base: " + m.value }
func (m MyType) ExtendedMethod() string { return "extended: " + m.value }
func useBase(b Base) {
fmt.Println(b.BaseMethod())
// 다운캐스팅: Base → Extended
if ext, ok := b.(Extended); ok {
fmt.Println(ext.ExtendedMethod())
}
// 구체 타입으로 캐스팅
if mt, ok := b.(MyType); ok {
fmt.Println("구체 타입:", mt.value)
}
}
func main() {
m := MyType{value: "hello"}
// 업캐스팅: 구체 타입 → 인터페이스 (자동)
var b Base = m
var e Extended = m
useBase(b)
fmt.Println("---")
useBase(e) // Extended도 Base를 구현
}
핵심 정리
v.(T): 단언 실패 시 패닉 → 확신할 때만 사용v, ok := i.(T): ok 패턴 → 실무에서 항상 이 방식 사용switch v := i.(type): 여러 타입 분기 → 타입별 처리에 최적errors.As/errors.Is: 에러 타입 단언의 표준 방식- 타입 단언은 인터페이스 값에만 사용 가능 (구체 타입에 불가)