상수 표현식

상수 표현식 (Constant expressions)

Go에서 상수(constant)는 컴파일 타임에 이미 값이 확정되는 값이에요. 이번에는 상수들로만 이루어진 표현식, 즉 상수 표현식이 어떻게 평가되는지 규칙을 하나씩 살펴볼게요. 상수로 할 수 있는 일과 못 하는 일을 정확히 알아야 나중에 코드에서 뜬금없는 컴파일 오류를 만났을 때도 "아, 이게 그 규칙 때문이었지" 하고 이해할 수 있어요.

출처: Go Specification

본문

상수 표현식은 오직 상수(constant) 피연산자만 담을 수 있고, 컴파일 타임에 평가돼요. 실행 중에 값이 바뀔 수 있는 일반 변수 같은 건 상수 표현식에 들어갈 수 없죠.

타입 없는(untyped) boolean, numeric, string 상수는 각각 boolean, numeric, string 타입의 피연산자를 쓸 수 있는 곳이면 어디든 그대로 피연산자로 쓸 수 있어요. 굳이 타입을 붙이지 않아도 문맥에 맞게 쓰인다는 뜻이에요.

상수 비교(comparison)는 언제나 타입 없는 boolean 상수를 만들어 내요. 상수 시프트 표현식의 왼쪽 피연산자가 타입 없는 상수라면 결과는 정수 상수이고, 그렇지 않으면 왼쪽 피연산자와 같은 타입의 상수예요. 이때 그 왼쪽 피연산자는 정수 타입이어야 해요.

타입 없는 상수에 대한 그 밖의 다른 연산은 전부 같은 종류(kind)의 타입 없는 상수를 결과로 줘요. 즉 boolean, integer, floating-point, complex, string 상수 중 하나가 되는 거죠. 한 가지 재미있는 규칙이 있는데, 이진 연산(시프트는 제외)에서 타입 없는 피연산자들의 종류가 서로 다르면 결과는 이 목록에서 더 뒤에 등장하는 종류를 따라가요: integer, rune, floating-point, complex. 예를 들어 타입 없는 정수 상수를 타입 없는 complex 상수로 나누면 타입 없는 complex 상수가 나와요.

const a = 2 + 3.0          // a == 5.0   (untyped floating-point constant)
const b = 15 / 4           // b == 3     (untyped integer constant)
const c = 15 / 4.0         // c == 3.75  (untyped floating-point constant)
const Θ float64 = 3/2      // Θ == 1.0   (type float64, 3/2 is integer division)
const Π float64 = 3/2.     // Π == 1.5   (type float64, 3/2. is float division)
const d = 1 << 3.0         // d == 8     (untyped integer constant)
const e = 1.0 << 3         // e == 8     (untyped integer constant)
const f = int32(1) << 33   // illegal    (constant 8589934592 overflows int32)
const g = float64(2) >> 1  // illegal    (float64(2) is a typed floating-point constant)
const h = "foo" > "bar"    // h == true  (untyped boolean constant)
const j = true             // j == true  (untyped boolean constant)
const k = 'w' + 1          // k == 'x'   (untyped rune constant)
const l = "hi"             // l == "hi"  (untyped string constant)
const m = string(k)        // m == "x"   (type string)
const Σ = 1 - 0.707i       //            (untyped complex constant)
const Δ = Σ + 2.0e-4       //            (untyped complex constant)
const Φ = iota*1i - 1/1i   //            (untyped complex constant)

내장 함수 complex를 타입 없는 integer, rune, floating-point 상수에 적용하면 타입 없는 complex 상수가 돼요.

const ic = complex(0, c)   // ic == 3.75i  (untyped complex constant)
const iΘ = complex(0, Θ)   // iΘ == 1i     (type complex128)

상수 표현식은 항상 정확하게(exactly) 평가돼요. 그래서 중간 계산 값이나 상수 자체가 언어에 미리 선언된 어떤 타입이 지지하는 것보다 훨씬 큰 정밀도를 요구할 수 있어요. 아래 선언들은 그 덕분에 성립하는 예시예요. Huge1 << 100이라 값이 어마어마하게 크지만, 타입 없는 상수라서 자유롭게 표현할 수 있죠.

const Huge = 1 << 100         // Huge == 1267650600228229401496703205376  (untyped integer constant)
const Four int8 = Huge >> 98  // Four == 4                                (type int8)

다만 상수 나눗셈이나 나머지 연산의 제수(divisor)는 0이면 안 돼요:

3.14 / 0.0   // illegal: division by zero

타입이 붙은(typed) 상수의 값은 항상 그 상수의 타입으로 정확히 표현 가능(representable)해야 해요. 그렇지 않은 상수 표현식들은 전부 컴파일 오류이고, 아래가 대표적인 예시예요.

uint(-1)     // -1 cannot be represented as a uint
int(3.14)    // 3.14 cannot be represented as an int
int64(Huge)  // 1267650600228229401496703205376 cannot be represented as an int64
Four * 300   // operand 300 cannot be represented as an int8 (type of Four)
Four * 100   // product 400 cannot be represented as an int8 (type of Four)

단항 비트 보수 연산자 ^가 쓰는 마스크 값은 비상수일 때의 규칙을 그대로 따라요. 즉 unsigned 상수에서는 마스크가 전부 1이고, signed 및 타입 없는 상수에서는 -1이에요.

^1         // untyped integer constant, equal to -2
uint8(^1)  // illegal: same as uint8(-2), -2 cannot be represented as a uint8
^uint8(1)  // typed uint8 constant, same as 0xFF ^ uint8(1) = uint8(0xFE)
int8(^1)   // same as int8(-2)
^int8(1)   // same as -1 ^ int8(1) = -2

구현 제약(implementation restriction) 하나를 짚고 갈게요. 컴파일러는 타입 없는 floating-point나 complex 상수 표현식을 계산할 때 반올림(rounding)을 쓸 수 있어요. 이와 관련된 자세한 내용은 상수 절의 구현 제약 부분을 보면 돼요. 이 반올림 때문에, 무한 정밀도로 계산하면 정수 값이 될 floating-point 상수 표현식이라도 정수 문맥에서 쓰면 유효하지 않게 될 수 있고, 그 반대의 경우도 생길 수 있어요. 아주 드문 경계 상황이지만 이런 가능성이 있다는 걸 알아두면 좋아요.

더 알아보기