Go

예상치 못한 untyped int의 동작, 왜 이런 일이?

드리프트2 2025. 1. 12. 13:39

예상치 못한 untyped int의 동작, 왜 이런 일이?

Go에서 const로 선언된 정수는 기본적으로 "untyped" 상태입니다.


즉, 특정한 타입(int, int64 등)이 정해지지 않은 상태로, 사용되는 문맥에 따라 타입이 결정됩니다.

 

이런 특성 때문에 32비트 환경과 64비트 환경에서 다르게 동작하거나,


컴파일 에러가 발생하는 경우가 있어 주의가 필요합니다.

 

이 글에서는 untyped int가 어떻게 동작하는지,


그리고 예상치 못한 동작이 발생하는 이유를 예제와 함께 알아보겠습니다.


1.  untyped int의 기본적인 동작

다음과 같은 코드가 있다고 해볼까요?

package main

import "log"

const x = 9876543210

func main() {
    log.Printf("%v", x) // 32비트 환경에서는 컴파일 에러 발생
}

 

이 코드는 64비트 환경에서는 정상적으로 동작합니다.


하지만 32비트 환경에서는 컴파일 에러가 발생합니다.

왜 이런 일이 발생할까요?

  • xuntyped int 상태입니다.
  • log.Printf("%v", x)에서 xany(interface{})로 전달됩니다.
  • 하지만 Go에서는 untyped int를 자동으로 int로 변환하려고 시도합니다.
  • 문제는 9876543210이 32비트 int 범위를 초과하기 때문에 컴파일 에러 발생!

즉, Go에서는 untyped int가 기본적으로 int로 변환되므로,


32비트 환경에서는 int의 최대값(2,147,483,647)을 초과하는 숫자는 사용할 수 없습니다.

 

이를 해결하려면 타입을 명시적으로 지정해야 합니다.

const x int64 = 9876543210 // 명시적으로 int64 지정
log.Printf("%v", x)         // 32비트 환경에서도 정상 동작

2. 연산에 따라 달라지는 untyped int의 동작

untyped int는 문맥에 따라 타입이 결정됩니다.


그렇다면 연산을 수행하면 어떻게 될까요?

비트 시프트 연산 (Shift 연산자 >>)

const x = 9876543210
const s = int(3)

func main() {
    log.Printf("%v", x>>s) // ✅ 정상 동작
}
  • xuntyped int이지만,
    비트 시프트(>>) 연산에서는 untyped 상태가 유지됩니다.
  • 따라서 32비트 환경에서도 컴파일 에러 없이 동작합니다.

나눗셈 연산 (/)

const x = 9876543210
const s = int(1 << 3)

func main() {
    log.Printf("%v", x/s) // ❌ 32비트 환경에서 컴파일 에러 발생
}
  • sint 타입입니다.
  • untyped intxint와 함께 연산되면서 자동으로 int로 변환됩니다.
  • 하지만 xint의 범위를 초과하기 때문에 컴파일 에러 발생!

즉, 비트 시프트 연산은 안전하지만, 나눗셈 연산은 위험할 수 있습니다.

해결 방법

const x int64 = 9876543210
const s = int64(1 << 3)

func main() {
    log.Printf("%v", x/s) // ✅ 정상 동작
}

명시적으로 int64 타입을 지정하면 안전하게 동작합니다.


3. switch 문에서 예상치 못한 변환

 switch 문에서 untyped int 사용

func main() {
    switch 9876543210 { // ❌ 32비트 환경에서 컴파일 에러 발생
    case 1:
        log.Println("one")
    default:
        log.Println("not one")
    }
}
  • switch 문에서 9876543210untyped int입니다.
  • 하지만 switch는 내부적으로 기본 타입(int)로 변환하려고 합니다.
  • 32비트 환경에서는 int의 범위를 초과하므로 컴파일 에러 발생!

해결 방법

func main() {
    switch int64(9876543210) { // ✅ int64로 변환
    case 1:
        log.Println("one")
    default:
        log.Println("not one")
    }
}

int64로 변환하면 정상적으로 동작합니다.

case 값의 타입이 다를 때

func main() {
    switch 9876543210 { // ❌ 32비트 환경에서 에러 발생
    case int64(1): // mismatched types int64 and int
        log.Println("one")
    default:
        log.Println("not one")
    }
}
  • 9876543210이 기본적으로 int로 변환되고,
  • case int64(1)int64 타입이므로 타입 불일치 오류 발생!

해결 방법

func main() {
    switch int64(9876543210) { // ✅ int64로 변환
    case int64(1):
        log.Println("one")
    default:
        log.Println("not one")
    }
}

switch의 값과 case 값을 같은 타입으로 맞춰야 합니다.


4. 컴파일 에러 없이 위험한 동작

컴파일 에러가 나지 않아도 환경에 따라 다르게 동작하는 코드가 있을 수 있습니다.

const x = 100000001
s := 10

func main() {
    log.Println((x << s) % 1000) // 64비트: 24, 32비트: -80
}
  • 64비트 환경에서는 24가 출력되지만,
  • 32비트 환경에서는 -80이 출력됩니다.
  • 같은 코드인데 결과가 다르게 나오는 위험한 상황! 😨

이 문제를 해결하려면?

const x int64 = 100000001
s := 10

func main() {
    log.Println((x << s) % 1000) // ✅ 모든 환경에서 24 출력
}

타입을 명확하게 지정하면 환경에 관계없이 예상한 값이 나옵니다.


5. Zig 언어와 비교

Go 말고도 Zig라는 언어에서도 comptime_int라는 개념이 있습니다.


하지만 Zig는 컴파일 시간에만 존재하는 정수 타입이므로,


print 같은 함수에 넘겨도 문제가 발생하지 않습니다.

const x = 987654321098765432109876543210;
try stdout.print("{}\n", .{x}); // ✅ 정상 동작

 

Go와 다르게 int로 변환되는 과정이 없어서 안전합니다.


결론: 안전한 코드 작성법

  1. 큰 숫자는 명시적으로 int64uint64 타입을 지정하자.
  2. 연산을 할 때 다른 타입(int vs int64)을 섞지 말자.
  3. switch 문에서는 case의 타입과 switch 값의 타입을 맞추자.
  4. 환경(32비트 vs 64비트)에 따라 결과가 달라질 수 있는 코드를 주의하자.

Go의 untyped int는 편리하지만, 예상치 못한 버그를 유발할 수 있습니다.


위의 원칙을 잘 기억하고 안전한 코드를 작성하세요!