Go에서 타입 단언 및 변환 탐색
Olivia Novak
Dev Intern · Leapcell

Go는 강력한 정적 타이핑에도 불구하고 데이터를 한 타입에서 다른 타입으로 변환하는 강력한 메커니즘을 제공합니다. 다른 언어에서 종종 "타입 캐스팅"이라고 불리는 이 개념은 Go에서 특정 뉘앙스를 가지며, 주로 타입 변환과 타입 단언을 구분합니다. 이러한 차이점을 이해하는 것은 견고하고 관용적인 Go 코드를 작성하는 데 중요합니다.
타입 변환: 명시적 변환
Go에서는 암시적 타입 변환이 엄격히 금지됩니다. 예를 들어, 값이 맞는다고 해도 int
를 int32
에 직접 할당할 수 없습니다. Go는 의도하지 않은 데이터 손실이나 오해를 방지하기 위해 명시적 변환을 요구합니다. 이러한 엄격함은 Go의 타입 안전성의 초석입니다.
타입 변환의 구문은 간단합니다: T(v)
, 여기서 T
는 대상 타입이고 v
는 변환할 값입니다.
몇 가지 일반적인 시나리오를 살펴보겠습니다.
숫자 타입 변환
숫자 타입 간의 변환은 빈번한 작업입니다. 더 큰 정수 타입에서 더 작은 타입으로 변환하거나 부동 소수점 타입에서 정수로 변환할 때 잘림이 발생할 수 있습니다. Go는 이를 암시적으로 처리하지 않으며, 명시적으로 변환해야 합니다.
package main import ( "fmt" ) func main() { var i int = 100 var j int32 = 200 // 명시적 변환 필요 j = int32(i) // i (int)가 int32로 변환됨 fmt.Printf("i: %T, %v\n", i, i) fmt.Printf("j: %T, %v\n", j, j) // 잘림을 포함한 변환 var f float64 = 3.14159 var k int = int(f) // f (float64)가 int로 잘림 fmt.Printf("f: %T, %v\n", f, f) fmt.Printf("k: %T, %v\n", k, k) // 변환 중 오버플로 var bigInt int64 = 20000000000 // 매우 큰 숫자 // var smallInt int32 = int32(bigInt) // 이것은 컴파일되지만, 결과는 잘림(오버플로)됩니다. // fmt.Printf("smallInt after overflow: %v\n", smallInt) // 예상치 못한 값이 표시됩니다. // 잠재적 오버플로를 처리하려면 일반적으로 변환 전에 경계를 확인합니다. const maxInt32 = int64(^uint32(0) >> 1) const minInt32 = -maxInt32 - 1 if bigInt > maxInt32 || bigInt < minInt32 { fmt.Println("Warning: bigInt is out of range for int32") } else { var safeInt32 int32 = int32(bigInt) fmt.Printf("safeInt32: %v\n", safeInt32) } }
문자열 및 바이트 슬라이스 변환
Go의 문자열은 불변의 바이트 시퀀스입니다. string
과 []byte
(바이트 슬라이스) 간에 변환할 수 있습니다. 이는 특히 I/O 작업이나 데이터 인코딩/디코딩을 다룰 때 유용합니다.
package main import "fmt" func main() { s := "hello, Go!" b := []byte(s) // 문자열을 []byte로 변환 fmt.Printf("s: %T, %v\n", s, s) fmt.Printf("b: %T, %v\n", b, b) s2 := string(b) // []byte를 다시 문자열로 변환 fmt.Printf("s2: %T, %v\n", s2, s2) // 단일 바이트를 문자열로 변환하면 해당 바이트로 표현되는 문자가 제공됩니다. var charByte byte = 71 // 'G'의 ASCII 코드 charString := string(charByte) fmt.Printf("charString: %T, %v\n", charString, charString) // 출력: charString: string, G }
중요 참고: 문자열을 []byte
로 변환하면 새로운 슬라이스가 생성됩니다. 문자열은 불변이므로 이 슬라이스를 수정해도 원본 문자열에는 영향을 미치지 않습니다.
사용자 정의 타입 변환
동일한 기본 타입을 가진다면 사용자 정의 타입을 정의하고 서로 변환할 수도 있습니다.
package main import "fmt" type Celsius float64 type Fahrenheit float64 func main() { var c Celsius = 25.0 var f Fahrenheit // 섭씨를 화씨로 변환 f = Fahrenheit(c*9/5 + 32) fmt.Printf("25 Celsius is %.2f Fahrenheit\n", f) // 필요한 경우 다시 변환할 수 있습니다. 기본 타입이 동일하기 때문입니다. c2 := Celsius(Fahrenheit(100.0) - 32) * 5 / 9 // 화씨를 섭씨로 다시 변환 fmt.Printf("100 Fahrenheit is %.2f Celsius\n", c2) }
타입 단언: 인터페이스에서 기본 타입 추출
타입 단언은 타입 변환과는 근본적으로 다릅니다. 인터페이스 타입에서만 사용되어 기본 구체 값을 추출하고 해당 타입을 확인하는 데 사용됩니다. 이를 통해 인터페이스 변수에 저장된 값을 "언래핑"할 수 있습니다.
타입 단언의 구문은 i.(T)
이며, 여기서 i
는 인터페이스 변수이고 T
는 해당 값을 가진다고 생각하는 구체 타입입니다.
타입 단언에는 두 가지 형태가 있습니다.
1. "Comma-Ok" 관용구 (안전한 단언)
이것은 타입 단언을 수행하는 선호되는 방법이며, 단언이 성공했는지 확인할 수 있는 방법을 제공합니다. 단언된 값과 성공을 나타내는 부울 값의 두 가지를 반환합니다.
package main import "fmt" type Walker interface { Walk() } type Dog struct { Name string } func (d Dog) Walk() { fmt.Printf("%s is walking.\n", d.Name) } type Bird struct { Species string } func (b Bird) Fly() { fmt.Printf("%s is flying.\n", b.Species) } func main() { var w Walker = Dog{Name: "Buddy"} // 안전한 단언: 'w'가 Dog를 포함하는지 확인 if dog, ok := w.(Dog); ok { fmt.Printf("The walker is a Dog named %s.\n", dog.Name) } else { fmt.Println("The walker is not a Dog.") } // 다른 타입으로 단언 시도 if bird, ok := w.(Bird); ok { fmt.Printf("The walker is a Bird: %s.\n", bird.Species) } else { fmt.Println("The walker is not a Bird.") // 이것이 출력됩니다. } // 또 다른 일반적인 사용 사례: 타입 스위치 processAnimal(Dog{Name: "Max"}) processAnimal(Bird{Species: "Pigeon"}) processAnimal("string literal") // 이것도 처리됩니다. } func processAnimal(thing interface{}) { switch v := thing.(type) { case Dog: fmt.Printf("🐕 Dog found: %s\n", v.Name) v.Walk() // 특정 메서드 호출 가능 case Bird: fmt.Printf("🐦 Bird found: %s\n", v.Species) v.Fly() // 특정 메서드 호출 가능 case string: fmt.Printf("📄 String found: \"%s\"\n", v) default: fmt.Printf("❓ Unknown type: %T\n", v) } }
"Comma-ok" 관용구는 기본 타입이 단언된 타입과 일치하지 않는 경우 패닉을 방지합니다.
2. 단일 값 단언 (안전하지 않음)
v := i.(T)
만 사용하는 경우, i
의 기본 타입인 T
가 아니면 프로그램이 panic
됩니다. 이 형태는 기본 타입을 절대적으로 확신하는 경우 또는 예상치 못한 시나리오에서 패닉을 발생시키려는 경우가 아니면 사용하지 마세요.
package main import "fmt" func main() { var myInterface interface{} = 123 // int 타입 // var myInterface interface{} = "hello" // string 타입, 패닉을 일으킴 // 안전하지 않은 단언 value := myInterface.(int) // myInterface가 int가 아니면 패닉 발생 fmt.Printf("Asserted value: %v\n", value) // 만약 myInterface가 다음과 같은 문자열이었다면: // var myInterface interface{} = "hello" // value := myInterface.(int) // 이 줄은 패닉을 일으킴: // panic: interface conversion: interface {} is string, not int }
안전을 위해 "comma-ok" 관용구나 type switch
를 사용하는 것이 좋습니다.
언제 무엇을 사용해야 할까요?
- 타입 변환은 값의 타입 표현을 변경하는 데 사용됩니다 (예:
int
에서float64
로,string
에서[]byte
로), 하지만 Go 규칙에 의해 명시적으로 변환 가능한 타입 간에만 가능합니다 (동일한 기본 타입, 또는 숫자 타입과 같은 정의된 변환). 항상 Go에 무엇을 해야 할지 명시적으로 지시하는 것입니다. - 타입 단언은 인터페이스 변수에 저장된 구체 값을 언래핑하고 해당 런타임 타입을 발견하는 데 사용됩니다. 인터페이스 타입 (
interface{}
또는 사용자 정의 인터페이스)의 값을 가지고 있고 해당 동적 구체 타입에 특정한 메서드나 필드에 액세스해야 할 때 사용됩니다.
모범 사례 및 고려 사항
interface{}
사용 최소화:interface{}
는 강력하지만 과도하게 사용하면 정적 타입 확인의 이점을 잃을 수 있습니다. 다형성이 진정으로 필요할 때 사용하세요.type switch
활용: 인터페이스에서 가능한 여러 구체 타입을 처리하려면type switch
문은 여러 타입 단언을 수행하는 깔끔하고 안전한 방법을 제공합니다.- 오류 처리: 단언이 원하는 동작인 경우를 제외하고 항상 타입 단언에 "comma-ok" 관용구를 사용하세요.
- 가독성: 명시적 변환은 의도된 데이터 변환에 대해 코드를 더 명확하게 만듭니다.
- 런타임 vs. 컴파일 타임: 타입 변환은 컴파일 타임에 발생합니다 (Go는 소스 및 대상 타입을 알고 있습니다). 타입 단언은 런타임에 발생합니다 (Go는 인터페이스에 저장된 동적 타입을 검사합니다).
결론
Go의 타입 "캐스팅" 접근 방식은 명시적 타입 변환과 동적 타입 단언을 명확하게 구분합니다. 이 구분은 Go의 강력한 타이핑과 함께 코드 안전성과 예측 가능성을 향상시킵니다. 타입 변환과 타입 단언을 언제 어떻게 적용해야 하는지를 익힘으로써 개발자는 다양한 데이터 타입을 우아하게 처리하는 유연하고 견고하며 관용적인 Go 프로그램을 작성할 수 있습니다.