상수의 타입과 한계
명시적이지 않은 듯 명시적인 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
iota
in the sameConstSpec
all 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