Updated:

11 minute read

개요

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. 테스트: 경계값 및 에러 케이스 테스트


참고 자료