상수의 타입과 한계
명시적이지 않은 듯 명시적인 Go에서 한 것처럼 reflect 패키지를 이용해 타입을 명시하지 않은 상수의 타입에 대해 알아보자.
package main
import "fmt"
import "reflect"
func main() {
const A = 1
const B = 3.14
const C = 1.0 + 1.0i
const (
D = iota
E
)
fmt.Println(reflect.TypeOf(A))
fmt.Println(reflect.TypeOf(B))
fmt.Println(reflect.TypeOf(C))
fmt.Println(reflect.TypeOf(D))
fmt.Println(reflect.TypeOf(E))
}
위 코드의 실행 결과는 다음과 같다.
int
float64
complex128
int
int
변수의 경우와 같은 규칙이 적용된다.
그런데 정말 상수가 그 타입인 것일까, 상수를 읽을 때 그 타입이 되는 것일까? 무슨 소린고 하니,
|
|
이 코드는 다음과 같은 오류가 발생한다.
./main.go:6:13: constant 324518553658426726783156020576256 overflows int
int에 담을 수 있는 한계를 벗어났다는 것이다. 100비트나 왼쪽으로 옮겼으니—\( 256 \times 2^{100} \)— 당연한 결과다.
그런데 var를 const로 바꾸면 다음과 같은 오류가 발생한다.
./main.go:7:16: constant 324518553658426726783156020576256 overflows int
오류 발생 시점이 달라졌다. 7라인, Println()을 호출할 때 오류가 발생한다.
그렇다면 const i = 256 << 100 코드로 값을 할당할 때는 오류가 발생하지 않는다는 의미일까?
package main
import "fmt"
func main() {
const i = 256 << 100
const j = 2 << 100
fmt.Println(i / j)
}
위 코드는 다음과 같이 정상적인 결과가 나온다.
128
i, j 각각에는 정상적으로 의도한 값이 할당돼 있는 것처럼 보인다.
이 결과는 int보다 큰 값을 표시할 수 있는 float을 이용해 i를 출력해 보면 좀 더 명확해 진다.
package main
import "fmt"
func main() {
const i = 256 << 100
fmt.Println(float64(i))
}
3.2451855365842673e+32
위 결과에서처럼 i에는 의도한 \(256 \times 2^{100}\) 값이 정확히 담겨있다. 내부적으로 const 처리를 어떻게 하는지에 대해서는 숙제로 남겨두고, 결과만 봤을 때는 const를 읽을 때 타입이 결정되는 것 같다.
iota에 대해 좀 더 자세히
iota 초기값
iota는 0부터 시작해 호출할 때마다 1씩 증가한다.
아래 코드의 결과를 보면 이 문구는 명확해 보인다.
package main
import "fmt"
func main() {
const (
Zero = iota
One
Two
)
fmt.Println(Zero)
fmt.Println(One)
fmt.Println(Two)
}
0
1
2
그런데 C에서 enum은 초기값을 달리해 다음과 같이 쓸 수 있다.
enum {
minus_one = -1, /* = -1 */
zero, /* = 0 */
one /* = 1 */
};
Go의 iota는 어떨까?
package main
import "fmt"
func main() {
const (
minusOne = -1
zero = iota
one
)
fmt.Println(zero)
fmt.Println(one)
}
iota를 zero에서 처음 호출했으니 zero가 0이 되면 좋을텐데 위 코드의 결과는 다음과 같다.
1
2
zero의 값이 0이 아닌 1이다. 좀 더 명확히 하기 위해 minusTwo = -2를 맨 앞에 추가하면 결과는 다음과 같다.
const (
minusTwo = -2
minusOne = -1
zero = iota
one
)
2
3
즉, iota는 상수 그룹 안에서 어딘가에 쓰이면, 그 앞뒤로는 모두 암시적으로(implicitly) 모두 iota가 쓰인 것으로 본다. 따라서 위 예에서는 zero에 iota가 세 번째 쓰인 것으로 처리돼 2의 값을 갖게 된다. 이 규칙은 타입이 달라도 유효하다.
const (
minusTwo = "-2"
minusOne = "-1"
zero = iota
one
)
2
3
한 줄에 두 번 쓴 iota
상수도 변수처럼 comma(,)를 이용해 한 줄에 복수개를 선언할 수 있다. 이 경우 iota의 값은 어떻게 될까?
package main
import "fmt"
func main() {
const (
zero, one = iota, iota
two, three
)
fmt.Println(zero)
fmt.Println(one)
fmt.Println(two)
fmt.Println(three)
}
0
0
1
1
iota는 호출할 때마다 값이 증가한다고 했는데 예상과 결과가 다르지 않은가.
이에 대해 Go spec.에는 다음과 같이 기술돼 있다.
Multiple uses of
iotain the sameConstSpecall have the same value.
comma(,)로 구분해 붙여 쓴 복수개의 상수는 모두 하나의 ConstSpec에 속하기 때문에 같은 iota값을 갖게 된다. 단, 똑같이 한 줄에 썼더라도 세미콜론을 이용해 ConstSpec을 나눈 경우에는 iota값이 증가한다.
const (
zero, one = iota, iota; two, three
)
0
0
1
1
중간값 지정
C에서 enum을 쓸 때는 중간값을 지정하는 것도 가능하다.
enum {
zero = 0,
one, /* = 1 */
two, /* = 2 */
five = 5,
six, /* = 6 */
seven /* = 7 */
};
Go에서 iota를 쓸 때는 이 부분에서 주의해야 한다.
package main
import "fmt"
func main() {
const (
zero = iota
one
two
five = 5
six
seven
)
fmt.Println(six)
fmt.Println(seven)
}
5
5
상수 그룹에서 literal로 값을 지정하고 이후 상수들에는 값을 지정하지 않을 경우 이전 상수의 값을 그대로 따른다. 따라서 six와 seven은 모두 5가 된다. 그렇다고 six에 iota로 값을 지정해 주면… 4가 된다. 이러한 경우에는 상수 그룹을 나누는 것이 현명하다.
iota 유지 범위
위에서 상수 그룹이라는 용어를 사용했는데, const 뒤 괄호로 묶여 있는 단위를 의미한다. iota의 값은 이 단위 안에서만 유지된다. 따라서 아래 상수들의 값은 모두 0이다.
const zero = iota
const one = iota
const two = iota
복잡한 상수 그룹
지금까지 확인한 Go의 상수 그룹과 iota는 enum에 비해 뭔가 불편해 보인다. 하지만 실망하지 마시라. 이런 것도 가능하다.
package main
import "fmt"
func main() {
const (
bytes = 1 << (iota * 10)
kilobytes
megabytes
gigabytes
)
fmt.Println(bytes)
fmt.Println(kilobytes)
fmt.Println(megabytes)
fmt.Println(gigabytes)
}
1
1024
1048576
1073741824