11 분 소요

개요

Go는 정적 타입 언어로, 타입 안정성을 중요시합니다. 암묵적(implicit) 타입 변환을 지원하지 않으며, 모든 타입 변환은 명시적(explicit)으로 수행해야 합니다. 이는 예상치 못한 버그를 방지하고 코드의 명확성을 높입니다.


타입 변환의 종류

Go에서는 두 가지 주요 타입 변환 메커니즘을 제공합니다:

  1. 타입 변환 (Type Conversion): 기본 타입 간의 변환
  2. 타입 단언 (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) - 다중 타입 확인


베스트 프랙티스

  1. 명시적 변환: 항상 명시적으로 타입 변환
  2. 에러 처리: 문자열 파싱 시 에러 확인
  3. 범위 검증: 큰 타입 → 작은 타입 변환 시 범위 확인
  4. 타입 단언 안전성: ok 패턴 사용
  5. 적절한 도구 선택: strconv vs fmt 상황에 맞게 사용
  6. 데이터 손실 인지: 정밀도/범위 손실 가능성 이해
  7. 성능 고려: 반복 변환 시 최적화
  8. 명확한 함수명: 변환 함수는 의도를 명확히
  9. 문서화: 복잡한 변환은 주석 추가
  10. 테스트: 경계값 및 에러 케이스 테스트


참고 자료