변환
변환 (Conversions)
Go에서 "타입을 바꾼다"는 말은 대개 변환(conversion)이 담당해요. 변환은 표현식의 타입을, 그 변환이 가리키는 타입으로 바꿔주는 작업이에요. 이 변환이 소스 코드에 글자 그대로 나타날 수도 있고, 표현식이 놓인 문맥에 의해 암시적으로 생길 수도 있어요. 이 섹션에서 그 기준과 규칙들을 하나씩 풀어볼게요.
출처: Go Specification
본문
명시적 변환(explicit conversion) 은 T(x) 형태의 표현식이에요. 여기서 T는 타입, x는 타입 T로 변환될 수 있는 표현식이에요. 형태를 EBNF로 보면 이렇게 돼요.
Conversion = Type "(" Expression [ "," ] ")" .
타입이 *나 <- 연산자로 시작하거나, func 키워드로 시작하면서 결과 리스트가 없는 경우에는, 모호함을 피하기 위해 필요할 때 괄호로 감싸줘야 해요.
*Point(p) // same as *(Point(p))
(*Point)(p) // p is converted to *Point
<-chan int(c) // same as <-(chan int(c))
(<-chan int)(c) // c is converted to <-chan int
func()(x) // function signature func() x
(func())(x) // x is converted to func()
(func() int)(x) // x is converted to func() int
func() int(x) // x is converted to func() int (unambiguous)
상수 값 x는, x가 T의 값으로 나타낼 수 있으면(representable) 타입 T로 변환할 수 있어요. 특별한 경우로, 정수 상수 x는 상수가 아닌 x에 적용되는 것과 같은 규칙을 써서 문자열 타입으로 명시적으로 변환할 수 있기도 해요.
상수를 타입 파라미터가 아닌 타입으로 변환하면, 결과는 타입이 붙은 상수(typed constant)가 돼요.
uint(iota) // iota value of type uint
float32(2.718281828) // 2.718281828 of type float32
complex128(1) // 1.0 + 0.0i of type complex128
float32(0.49999999) // 0.5 of type float32
float64(-1e-1000) // 0.0 of type float64
string('x') // "x" of type string
string(0x266c) // "♬" of type string
myString("foo" + "bar") // "foobar" of type myString
string([]byte{'a'}) // not a constant: []byte{'a'} is not a constant
(*int)(nil) // not a constant: nil is not a constant, *int is not a boolean, numeric, or string type
int(1.2) // illegal: 1.2 cannot be represented as an int
string(65.0) // illegal: 65.0 is not an integer constant
반대로, 상수를 타입 파라미터로 변환하면 그 타입의 비상수 값이 나와요. 이때 값은 타입 파라미터가 인스턴스화되는 타입 인자의 값으로 표현돼요. 예를 들어 다음 함수를 볼게요.
func f[P ~float32|~float64]() {
… P(1.1) …
}
여기서 변환 P(1.1)은 타입 P의 비상수 값을 만들고, 값 1.1은 f의 타입 인자가 float32냐 float64냐에 따라 float32 또는 float64로 표현돼요. 그래서 f가 float32 타입으로 인스턴스화되면, 표현식 P(1.1) + 1.2의 숫자 값도 상응하는 비상수 float32 덧셈과 같은 정밀도로 계산돼요.
비상수 값 x는 다음 경우들 중 하나에 해당하면 타입 T로 변환할 수 있어요.
x가T에 할당 가능(assignable)하다.- struct 태그를 무시했을 때(아래 참고),
x의 타입과T가 타입 파라미터가 아니면서 동일한 underlying types를 가진다. - struct 태그를 무시했을 때(아래 참고),
x의 타입과T가 named types가 아닌 포인터 타입이고, 그 포인터 베이스 타입이 타입 파라미터가 아니면서 동일한 underlying types를 가진다. x의 타입과T가 모두 정수 또는 부동소수점 타입이다.x의 타입과T가 모두 복소수 타입이다.x가 정수 또는 bytes/runes 슬라이스이고T가 문자열 타입이다.x가 문자열이고T가 bytes 또는 runes 슬라이스이다.x가 슬라이스이고T가 배열 [Go 1.20] 또는 배열 포인터 [Go 1.17]이며, 슬라이스와 배열 타입이 동일한 요소 타입을 가진다.
추가로, T나 x의 타입 V가 타입 파라미터라면, 다음 조건 중 하나가 성립할 때 x를 타입 T로 변환할 수 있어요.
V와T가 모두 타입 파라미터이고,V의 타입 집합에 있는 각 타입의 값을T의 타입 집합에 있는 각 타입으로 변환할 수 있다.V만 타입 파라미터이고,V의 타입 집합에 있는 각 타입의 값을T로 변환할 수 있다.T만 타입 파라미터이고,x를T의 타입 집합에 있는 각 타입으로 변환할 수 있다.
변환을 위한 타입 일치(identity)를 비교할 때 struct 태그는 무시돼요.
type Person struct {
Name string
Address *struct {
Street string
City string
}
}
var data *struct {
Name string `json:"name"`
Address *struct {
Street string `json:"street"`
City string `json:"city"`
} `json:"address"`
}
var person = (*Person)(data) // ignoring tags, the underlying types are identical
수치 타입 사이, 또는 문자열 타입과의 (비상수) 변환에는 특별한 규칙이 적용돼요. 이런 변환들은 x의 표현(representation)을 바꿀 수 있고 실행 시간 비용이 들기도 해요. 그 외의 모든 변환은 타입만 바꾸고 x의 표현은 바꾸지 않아요.
포인터와 정수 사이를 변환해 주는 언어 차원의 메커니즘은 없어요. unsafe 패키지가 제한된 상황에서 이 기능을 구현해 주고 있어요.
수치 타입 사이의 변환
비상수 수치 값의 변환에서는 다음 규칙들이 적용돼요.
- 정수 타입 사이의 변환: 값이 부호 있는 정수라면 암묵적으로 무한 정밀도로 부호 확장(sign extended)되고, 그렇지 않으면 0으로 확장돼요. 그다음 결과 타입의 크기에 맞게 잘려요(truncate). 예를 들어
v := uint16(0x10F0)라면uint32(int8(v)) == 0xFFFFFFF0이 돼요. 이 변환은 항상 유효한 값을 만들고, 오버플로우를 알려주는 표시는 없어요. - 부동소수점을 정수로 변환: 소수 부분이 버려져요(0을 향한 잘림).
- 정수 또는 부동소수점을 부동소수점 타입으로, 또는 복소수를 다른 복소수 타입으로 변환: 결과 값이 목적지 타입이 지정하는 정밀도로 반올림돼요. 예를 들어
float32타입 변수x의 값은 IEEE 754 32비트 숫자보다 더 많은 정밀도로 저장될 수 있지만,float32(x)는x의 값을 32비트 정밀도로 반올림한 결과를 나타내요. 마찬가지로x + 0.1은 32비트보다 더 많은 정밀도를 쓸 수 있지만,float32(x + 0.1)은 그러지 않아요.
부동소수점 또는 복소수 값이 관련된 모든 비상수 변환에서, 결과 타입이 값을 나타낼 수 없으면 변환 자체는 성공하지만 결과 값은 구현에 따라 달라질 수 있어요(implementation-dependent).
문자열 타입과의 변환
- bytes 슬라이스를 문자열 타입으로: 슬라이스의 연속된 바이트들을 요소로 갖는 문자열이 나와요.
string([]byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}) // "hellø"
string([]byte{}) // ""
string([]byte(nil)) // ""
type bytes []byte
string(bytes{'h', 'e', 'l', 'l', '\xc3', '\xb8'}) // "hellø"
type myByte byte
string([]myByte{'w', 'o', 'r', 'l', 'd', '!'}) // "world!"
myString([]myByte{'\xf0', '\x9f', '\x8c', '\x8d'}) // "🌍"
- runes 슬라이스를 문자열 타입으로: 각 rune 값을 문자열로 변환해 이어 붙인 문자열이 나와요.
string([]rune{0x767d, 0x9d6c, 0x7fd4}) // "\u767d\u9d6c\u7fd4" == "白鵬翔"
string([]rune{}) // ""
string([]rune(nil)) // ""
type runes []rune
string(runes{0x767d, 0x9d6c, 0x7fd4}) // "\u767d\u9d6c\u7fd4" == "白鵬翔"
type myRune rune
string([]myRune{0x266b, 0x266c}) // "\u266b\u266c" == "♫♬"
myString([]myRune{0x1f30e}) // "\U0001f30e" == "🌎"
- 문자열 타입의 값을 bytes 슬라이스 타입으로: 문자열의 연속된 바이트들을 요소로 갖는, nil이 아닌 슬라이스가 나와요. 결과 슬라이스의 capacity는 구현에 따라 다르고 슬라이스 길이보다 클 수 있어요.
[]byte("hellø") // []byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}
[]byte("") // []byte{}
bytes("hellø") // []byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}
[]myByte("world!") // []myByte{'w', 'o', 'r', 'l', 'd', '!'}
[]myByte(myString("🌏")) // []myByte{'\xf0', '\x9f', '\x8c', '\x8f'}
- 문자열 타입의 값을 runes 슬라이스 타입으로: 문자열의 개별 Unicode 코드 포인트들을 담은 슬라이스가 나와요. 결과 슬라이스의 capacity는 구현에 따라 다르고 슬라이스 길이보다 클 수 있어요.
[]rune(myString("白鵬翔")) // []rune{0x767d, 0x9d6c, 0x7fd4}
[]rune("") // []rune{}
runes("白鵬翔") // []rune{0x767d, 0x9d6c, 0x7fd4}
[]myRune("♫♬") // []myRune{0x266b, 0x266c}
[]myRune(myString("🌐")) // []myRune{0x1f310}
- 마지막으로, 역사적인 이유로 정수 값을 문자열 타입으로 변환할 수 있어요. 이 형태의 변환은 주어진 정수 값을 가진 Unicode 코드 포인트의 (여러 바이트일 수 있는) UTF-8 표현을 담은 문자열을 만들어요. 유효한 Unicode 코드 포인트의 범위를 벗어난 값은
"\uFFFD"로 변환돼요.
string('a') // "a"
string(65) // "A"
string('\xf8') // "\u00f8" == "ø" == "\xc3\xb8"
string(-1) // "\ufffd" == "\xef\xbf\xbd"
type myString string
myString('\u65e5') // "\u65e5" == "日" == "\xe6\x97\xa5"
참고: 이 형태의 변환은 언젠가 언어에서 제거될 수 있어요. go vet 도구가 특정 정수-문자열 변환을 잠재적 오류로 표시해 주고, 대신 utf8.AppendRune이나 utf8.EncodeRune 같은 라이브러리 함수를 쓰는 편이 좋아요.
슬라이스에서 배열 또는 배열 포인터로의 변환
슬라이스를 배열로 변환하면 슬라이스의 underlying array의 요소들을 담은 배열이 나와요. 마찬가지로 슬라이스를 배열 포인터로 변환하면 슬라이스의 underlying array를 가리키는 포인터가 나와요. 두 경우 모두, 슬라이스의 길이가 배열의 길이보다 짧으면 실행 시간 패닉(run-time panic)이 발생해요.
s := make([]byte, 2, 4)
a0 := [0]byte(s)
a1 := [1]byte(s[1:]) // a1[0] == s[1]
a2 := [2]byte(s) // a2[0] == s[0]
a4 := [4]byte(s) // panics: len([4]byte) > len(s)
s0 := (*[0]byte)(s) // s0 != nil
s1 := (*[1]byte)(s[1:]) // &s1[0] == &s[1]
s2 := (*[2]byte)(s) // &s2[0] == &s[0]
s4 := (*[4]byte)(s) // panics: len([4]byte) > len(s)
var t []string
t0 := [0]string(t) // ok for nil slice t
t1 := (*[0]string)(t) // t1 == nil
t2 := (*[1]string)(t) // panics: len([1]string) > len(t)
u := make([]byte, 0)
u0 := (*[0]byte)(u) // u0 != nil
더 알아보기
- 타입 (Types) — 변환 결과로 만들어지는 타입의 개념
- 할당 가능성 (Assignability) —
x가T에 할당 가능한 조건 - 표현 가능성 (Representability) — 상수가 타입의 값으로 나타낼 수 있는지
- underlying types — 변환에서 타입 일치를 판단하는 기준
- unsafe 패키지 — 포인터와 정수 사이의 제한적 변환