[Go] 타입 변환
Updated:
개요
Go는 정적 타입 언어로, 타입 안정성을 중요시합니다. 암묵적(implicit) 타입 변환을 지원하지 않으며, 모든 타입 변환은 명시적(explicit)으로 수행해야 합니다. 이는 예상치 못한 버그를 방지하고 코드의 명확성을 높입니다.
타입 변환의 종류
Go에서는 두 가지 주요 타입 변환 메커니즘을 제공합니다:
- 타입 변환 (Type Conversion): 기본 타입 간의 변환
- 타입 단언 (Type Assertion): 인터페이스 타입의 변환
기본 타입 변환 (Type Conversion)
구문
T(v) // v를 타입 T로 변환
규칙
- 명시적 변환 필수: 자동 변환 없음
- 호환 가능한 타입만 변환 가능
- 데이터 손실 가능성 주의
숫자 타입 변환
정수 간 변환
크기 변환
var i8 int8 = 127
var i16 int16 = int16(i8) // int8 → int16
var i32 int32 = int32(i16) // int16 → int32
var i64 int64 = int64(i32) // int32 → int64
// 큰 타입에서 작은 타입으로 (데이터 손실 주의)
var large int64 = 1000
var small int8 = int8(large) // 1000은 int8 범위(-128~127) 초과
fmt.Println(small) // -24 (오버플로우)
부호 변환
var signed int32 = -100
var unsigned uint32 = uint32(signed) // -100 → 4294967196 (비트 패턴 유지)
var u uint32 = 300
var s int32 = int32(u) // 정상 변환
int와 고정 크기 정수
var i int = 100
var i32 int32 = int32(i) // int → int32
var i64 int64 = int64(i) // int → int64
var fixed int32 = 200
var platform int = int(fixed) // int32 → int
정수와 실수 변환
정수 → 실수
var i int = 42
var f32 float32 = float32(i) // 42.0
var f64 float64 = float64(i) // 42.0
var large int64 = 9007199254740993 // 큰 정수
var f float64 = float64(large) // 정밀도 손실 가능
실수 → 정수 (소수점 절삭)
var f float64 = 3.14
var i int = int(f) // 3 (소수점 이하 버림)
var negative float64 = -2.99
var n int = int(negative) // -2 (0 방향으로 절삭)
var f32 float32 = 1.9
var i32 int32 = int32(f32) // 1
실수 간 변환
var f32 float32 = 3.14159
var f64 float64 = float64(f32) // float32 → float64
var pi float64 = 3.141592653589793
var f32Short float32 = float32(pi) // 정밀도 손실
fmt.Println(f32Short) // 3.1415927
복소수 변환
var c64 complex64 = complex(float32(3.0), float32(4.0))
var c128 complex128 = complex128(c64) // complex64 → complex128
var c complex128 = complex(3.0, 4.0)
var c64Small complex64 = complex64(c) // 정밀도 손실 가능
문자열 변환
숫자 → 문자열
strconv 패키지 사용 (권장)
import "strconv"
// 정수 → 문자열
var i int = 42
s1 := strconv.Itoa(i) // "42"
s2 := strconv.FormatInt(int64(i), 10) // "42" (10진수)
s3 := strconv.FormatInt(int64(255), 16) // "ff" (16진수)
s4 := strconv.FormatInt(int64(8), 2) // "1000" (2진수)
// 실수 → 문자열
var f float64 = 3.14159
s5 := strconv.FormatFloat(f, 'f', 2, 64) // "3.14" (소수점 2자리)
s6 := strconv.FormatFloat(f, 'e', 2, 64) // "3.14e+00" (지수 표기)
s7 := strconv.FormatFloat(f, 'g', -1, 64) // "3.14159" (자동)
// 불린 → 문자열
b := true
s8 := strconv.FormatBool(b) // "true"
fmt 패키지 사용
import "fmt"
var i int = 42
s1 := fmt.Sprintf("%d", i) // "42"
s2 := fmt.Sprintf("%x", i) // "2a" (16진수)
s3 := fmt.Sprintf("%b", i) // "101010" (2진수)
var f float64 = 3.14159
s4 := fmt.Sprintf("%.2f", f) // "3.14"
s5 := fmt.Sprintf("%e", f) // "3.141590e+00"
문자열 → 숫자
strconv 패키지 사용 (권장)
import "strconv"
// 문자열 → 정수
s1 := "42"
i1, err1 := strconv.Atoi(s1) // 42, nil
i2, err2 := strconv.ParseInt(s1, 10, 64) // 42, nil (10진수, int64)
i3, err3 := strconv.ParseInt("ff", 16, 64) // 255, nil (16진수)
// 문자열 → 실수
s2 := "3.14"
f1, err4 := strconv.ParseFloat(s2, 64) // 3.14, nil
// 문자열 → 불린
s3 := "true"
b1, err5 := strconv.ParseBool(s3) // true, nil
b2, _ := strconv.ParseBool("1") // true, nil
b3, _ := strconv.ParseBool("0") // false, nil
// 에러 처리
invalid := "abc"
_, err := strconv.Atoi(invalid)
if err != nil {
fmt.Println("변환 실패:", err)
}
기본값과 함께 파싱
func parseIntWithDefault(s string, defaultValue int) int {
if v, err := strconv.Atoi(s); err == nil {
return v
}
return defaultValue
}
result := parseIntWithDefault("invalid", 0) // 0
바이트와 문자열 변환
문자열 → 바이트 슬라이스
s := "Hello, 세계"
bytes := []byte(s) // UTF-8 인코딩된 바이트 슬라이스
fmt.Println(bytes) // [72 101 108 108 111 44 32 236 132 184 234 179 132]
바이트 슬라이스 → 문자열
bytes := []byte{72, 101, 108, 108, 111}
s := string(bytes) // "Hello"
Rune과 문자열 변환
문자열 → Rune 슬라이스
s := "Hello, 세계"
runes := []rune(s)
fmt.Println(runes) // [72 101 108 108 111 44 32 49464 44228]
fmt.Println(len(s)) // 13 (바이트)
fmt.Println(len(runes)) // 9 (문자)
Rune 슬라이스 → 문자열
runes := []rune{72, 101, 108, 108, 111}
s := string(runes) // "Hello"
단일 Rune/Byte → 문자열
var r rune = 'A'
s1 := string(r) // "A"
var b byte = 65
s2 := string(b) // "A"
// 주의: 숫자를 직접 변환하면 유니코드 문자로 변환됨
num := 65
s3 := string(num) // "A" (ASCII 65)
// 숫자를 문자열로 변환하려면 strconv 사용
s4 := strconv.Itoa(num) // "65"
타입 단언 (Type Assertion)
인터페이스 타입을 구체적인 타입으로 변환할 때 사용합니다.
기본 타입 단언
단일 값 반환 (패닉 가능)
var i interface{} = "hello"
s := i.(string) // 성공: "hello"
fmt.Println(s)
// n := i.(int) // 패닉 발생!
두 값 반환 (안전)
var i interface{} = "hello"
s, ok := i.(string)
if ok {
fmt.Println("문자열:", s) // 실행됨
} else {
fmt.Println("문자열이 아닙니다")
}
n, ok := i.(int)
if ok {
fmt.Println("정수:", n)
} else {
fmt.Println("정수가 아닙니다") // 실행됨
}
타입 스위치 (Type Switch)
여러 타입을 확인할 때 유용합니다.
func describe(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("정수: %d\n", v)
case float64:
fmt.Printf("실수: %f\n", v)
case string:
fmt.Printf("문자열: %s\n", v)
case bool:
fmt.Printf("불린: %t\n", v)
case []int:
fmt.Printf("정수 슬라이스: %v\n", v)
default:
fmt.Printf("알 수 없는 타입: %T\n", v)
}
}
describe(42) // 정수: 42
describe(3.14) // 실수: 3.140000
describe("hello") // 문자열: hello
describe(true) // 불린: true
describe([]int{1, 2}) // 정수 슬라이스: [1 2]
인터페이스 타입 단언
type Reader interface {
Read() string
}
type Writer interface {
Write(string)
}
type ReadWriter interface {
Reader
Writer
}
type File struct{}
func (f File) Read() string { return "reading" }
func (f File) Write(s string) { fmt.Println("writing:", s) }
func main() {
var rw ReadWriter = File{}
// ReadWriter → Reader
r := rw.(Reader)
fmt.Println(r.Read())
// ReadWriter → Writer
w := rw.(Writer)
w.Write("data")
}
포인터 변환
기본 규칙
// 값 → 포인터
var i int = 42
var p *int = &i
// 포인터 → 값
var v int = *p
// 타입이 다른 포인터 간 직접 변환 불가
var i32 int32 = 100
var p32 *int32 = &i32
// var p64 *int64 = (*int64)(p32) // 컴파일 에러!
unsafe 패키지를 이용한 포인터 변환 (주의 필요)
import "unsafe"
var i32 int32 = 100
var p32 *int32 = &i32
// unsafe를 통한 포인터 변환 (위험!)
p64 := (*int64)(unsafe.Pointer(p32))
// 주의: 메모리 레이아웃이 다르면 예상치 못한 결과 발생
슬라이스와 배열 변환
배열 → 슬라이스
var arr [5]int = [5]int{1, 2, 3, 4, 5}
slice := arr[:] // 전체 배열을 슬라이스로
fmt.Println(slice) // [1 2 3 4 5]
slice2 := arr[1:4] // 부분 슬라이스
fmt.Println(slice2) // [2 3 4]
슬라이스 → 배열 (Go 1.17+)
slice := []int{1, 2, 3, 4, 5}
// 슬라이스를 배열로 변환
arr := [5]int(slice)
fmt.Println(arr) // [1 2 3 4 5]
// 크기가 맞지 않으면 컴파일 에러
// arr2 := [10]int(slice) // 에러: 길이 불일치
타입이 다른 슬라이스
// 수동 변환 필요
var ints []int = []int{1, 2, 3}
var floats []float64
for _, v := range ints {
floats = append(floats, float64(v))
}
fmt.Println(floats) // [1 2 3]
사용자 정의 타입 변환
기본 타입 기반의 사용자 정의 타입
type Celsius float64
type Fahrenheit float64
func CToF(c Celsius) Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
func FToC(f Fahrenheit) Celsius {
return Celsius((f - 32) * 5 / 9)
}
func main() {
var c Celsius = 100
f := CToF(c)
fmt.Printf("%.2f°C = %.2f°F\n", c, f) // 100.00°C = 212.00°F
// 직접 변환도 가능 (기본 타입이 같으면)
var temp float64 = float64(c)
fmt.Println(temp) // 100
}
구조체 변환
type Point2D struct {
X, Y float64
}
type Point3D struct {
X, Y, Z float64
}
// 수동 변환 함수
func Point2DTo3D(p Point2D) Point3D {
return Point3D{X: p.X, Y: p.Y, Z: 0}
}
// 메서드로 구현
func (p Point2D) To3D() Point3D {
return Point3D{X: p.X, Y: p.Y, Z: 0}
}
func main() {
p2d := Point2D{X: 1, Y: 2}
p3d := p2d.To3D()
fmt.Println(p3d) // {1 2 0}
}
변환 시 주의사항
1. 데이터 손실
오버플로우
var large int64 = 2147483648 // int32 최대값 + 1
var small int32 = int32(large)
fmt.Println(small) // -2147483648 (오버플로우)
// 안전한 변환 확인
if large > math.MaxInt32 || large < math.MinInt32 {
fmt.Println("int32 범위 초과")
}
정밀도 손실
var precise float64 = 1.23456789012345
var less float32 = float32(precise)
fmt.Println(precise) // 1.23456789012345
fmt.Println(less) // 1.2345679 (정밀도 손실)
소수점 절삭
var f float64 = 3.99
var i int = int(f)
fmt.Println(i) // 3 (반올림 아님, 절삭)
// 반올림이 필요하면 math 패키지 사용
import "math"
rounded := int(math.Round(f))
fmt.Println(rounded) // 4
2. 문자열 변환 실수
// 잘못된 예: 숫자를 문자로 변환
num := 65
s1 := string(num) // "A" (ASCII 코드로 변환됨)
// 올바른 예: 숫자를 문자열로 변환
s2 := strconv.Itoa(num) // "65"
// 잘못된 예: 바이트 슬라이스를 숫자로
bytes := []byte("42")
// n := int(bytes) // 컴파일 에러!
// 올바른 예
s := string(bytes)
n, _ := strconv.Atoi(s)
3. 타입 단언 실패
var i interface{} = 42
// 위험: 패닉 발생 가능
s := i.(string) // 런타임 패닉!
// 안전: ok 패턴 사용
if s, ok := i.(string); ok {
fmt.Println(s)
} else {
fmt.Println("문자열이 아닙니다")
}
변환 패턴 및 베스트 프랙티스
1. 에러 처리
// 문자열 변환 시 항상 에러 확인
func safeAtoi(s string) (int, error) {
return strconv.Atoi(s)
}
value, err := safeAtoi("123")
if err != nil {
log.Printf("변환 실패: %v", err)
return
}
2. 범위 검증
func safeInt64ToInt32(i64 int64) (int32, error) {
if i64 > math.MaxInt32 || i64 < math.MinInt32 {
return 0, fmt.Errorf("범위 초과: %d", i64)
}
return int32(i64), nil
}
3. 타입 변환 함수
// 명확한 함수명 사용
type UserID int64
func (u UserID) String() string {
return strconv.FormatInt(int64(u), 10)
}
func ParseUserID(s string) (UserID, error) {
id, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0, err
}
return UserID(id), nil
}
4. 제네릭 변환 함수 (Go 1.18+)
func ConvertSlice[T, U any](slice []T, convert func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = convert(v)
}
return result
}
// 사용 예
ints := []int{1, 2, 3, 4, 5}
floats := ConvertSlice(ints, func(i int) float64 {
return float64(i)
})
fmt.Println(floats) // [1 2 3 4 5]
일반적인 실수
1. 암묵적 변환 기대
var i int64 = 100
var j int32 = 200
// result := i + j // 컴파일 에러! 타입 불일치
// 올바른 방법
result := i + int64(j) // 명시적 변환
2. 문자열-숫자 변환 혼동
// 잘못됨
num := 5
s := string(num) // "\x05" (제어 문자)
// 올바름
s := strconv.Itoa(num) // "5"
3. nil 인터페이스 타입 단언
var i interface{} = nil
// s := i.(string) // 패닉!
if s, ok := i.(string); ok {
fmt.Println(s)
} else {
fmt.Println("nil 또는 타입 불일치")
}
4. 포인터 nil 체크 누락
func processString(s *string) {
// if s == nil { // nil 체크 필수
// return
// }
fmt.Println(*s)
}
성능 고려사항
1. 문자열 변환 비용
// 느림: 반복적인 문자열 변환
var result string
for i := 0; i < 1000; i++ {
result += strconv.Itoa(i) // 매번 새 문자열 생성
}
// 빠름: strings.Builder 사용
var builder strings.Builder
for i := 0; i < 1000; i++ {
builder.WriteString(strconv.Itoa(i))
}
result := builder.String()
2. 타입 단언 캐싱
// 반복되는 타입 단언은 한 번만
func process(items []interface{}) {
for _, item := range items {
if s, ok := item.(string); ok {
// s를 재사용
processString(s)
}
}
}
실전 예제
종합 예제
package main
import (
"fmt"
"reflect"
"strconv"
)
func main() {
// 1. 숫자 타입 변환
i := 42
fmt.Println("정수:", i)
fmt.Println("실수:", float64(i))
fmt.Println("타입:", reflect.TypeOf(i))
fmt.Println("타입:", reflect.TypeOf(float64(i)))
fmt.Println("------")
// 2. 실수에서 정수로 (소수점 절삭)
f := 3.99
fmt.Println("실수:", f)
fmt.Println("정수:", int(f))
fmt.Println("타입:", reflect.TypeOf(f))
fmt.Println("타입:", reflect.TypeOf(int(f)))
fmt.Println("------")
// 3. 문자열 변환
num := 123
str := strconv.Itoa(num)
fmt.Println("문자열:", str)
back, err := strconv.Atoi(str)
if err == nil {
fmt.Println("정수로 복원:", back)
}
fmt.Println("------")
// 4. 타입 단언
var i interface{} = "hello"
if s, ok := i.(string); ok {
fmt.Println("문자열 값:", s)
}
// 5. 타입 스위치
describe := func(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("정수: %d\n", v)
case string:
fmt.Printf("문자열: %s\n", v)
default:
fmt.Printf("알 수 없는 타입: %T\n", v)
}
}
describe(42)
describe("world")
describe(true)
}
실행 결과
정수: 42
실수: 42
타입: int
타입: float64
------
실수: 3.99
정수: 3
타입: float64
타입: int
------
문자열: 123
정수로 복원: 123
------
문자열 값: hello
정수: 42
문자열: world
알 수 없는 타입: bool
요약
| 변환 유형 | 방법 | 예제 | 주의사항 |
|---|---|---|---|
| 숫자 타입 | T(v) |
int(3.14) |
데이터 손실 가능 |
| 문자열 → 숫자 | strconv.Atoi/ParseInt/ParseFloat |
strconv.Atoi("42") |
에러 처리 필수 |
| 숫자 → 문자열 | strconv.Itoa/FormatInt/FormatFloat |
strconv.Itoa(42) |
string(num) 사용 금지 |
| 바이트 ↔ 문자열 | []byte(s), string(b) |
[]byte("hello") |
UTF-8 인코딩 |
| 타입 단언 | v.(T) |
i.(string) |
ok 패턴 사용 권장 |
| 타입 스위치 | switch v := i.(type) |
- | 다중 타입 확인 |
베스트 프랙티스
- 명시적 변환: 항상 명시적으로 타입 변환
- 에러 처리: 문자열 파싱 시 에러 확인
- 범위 검증: 큰 타입 → 작은 타입 변환 시 범위 확인
- 타입 단언 안전성: ok 패턴 사용
- 적절한 도구 선택: strconv vs fmt 상황에 맞게 사용
- 데이터 손실 인지: 정밀도/범위 손실 가능성 이해
- 성능 고려: 반복 변환 시 최적화
- 명확한 함수명: 변환 함수는 의도를 명확히
- 문서화: 복잡한 변환은 주석 추가
- 테스트: 경계값 및 에러 케이스 테스트