[The Go Programming Language] 3장 기본 데이터 타입 - 3.1 정수

in #kr-dev6 years ago

modolee_logo
안녕하세요. 개발자 모도리입니다.
The Go Programming Language 라는 책으로 Go를 공부하고 있으며, 해당 책의 내용을 요약 정리해서 올리려고 합니다. 저는 번역본을 구매해서 공부하고 있습니다.
게시물에 예제코드 라고 나오는 것들은 https://github.com/modolee/tgpl.git 에서 다운 받으실 수 있습니다.

지난 게시물


3장 기본 데이터 타입

3.1 정수

값의 크기와 부호 여부

  • 정수는 부호 있는(signed) 정수와 부호 없는(unsigned) 정수를 모두 제공합니다.
  • 각 각 네 가지 크기의 정수를 제공합니다.
    • signed : int8, int16, int32, int64
    • unsigned : uint8 uint16, uint32, uint64
  • 특정 플랫폼의 기본 타입이거나 가장 효율적인 크기인 부호 있는 정수나 부호 없는 정수로서 그냥 int와 uint로 불리는 두 가지 타입도 있습니다.
    • 32비트 또는 64비트 이지만, 컴파일러에 따라 다른 선택을 하므로 특정 크기로 가정할 수 없습니다.
  • rune 타입은 int32와 같지만 통상적으로 유니코드 값을 담는데 사용합니다.
  • byte 타입은 uint8과 같지만 작은 양의 숫자가 아닌 원시 데이터의 일부임을 강조합니다.
  • uintptr은 길이가 지정돼 있지 않지만 포인터 값의 모든 비트를 저장할 수 있는 부호 없는 타입입니다.
  • int, uint, uintptr은 실제 크기에 관계없이 명시적으로 크기가 주어진 타입들과는 다릅니다.
    • 정수의 기본 크기가 32비트일 때에도 int와 int32는 다른 타입으로 취급됩니다.

값의 범위

  • 부호 있는 숫자 : -2^(n-1) ~ 2^(n-1) - 1
  • 부호 없는 숫자 : 0 ~ 2^(n) - 1 (번역서에는 잘못 표기되어 있어요)
  • 예 : int8 : -128 ~ 127 | uint8 : 0 ~ 255

이항 연산자

  • 우선순위
*   /   %   <<   >>   &   &^
+   -   |   ^
==  !=  <   <=   >    >=
&&
||
* 이항 연산자의 우선순위는 다섯 단계이며, 동일한 수준의 연산자는 왼쪽으로 연관됩니다. * 보다 명확하게 연산순서를 지정하고 싶으면 괄호를 사용해야 합니다. * 예 : `mask & (1 << 28)` 동일한 수준이지만 괄호를 먼저 수행

산술 연산자

  • +, -, *, /는 정수, 부동소수점 수와 복소수에 사용할 수 있지만, 나머지 연산자 %는 정수에만 사용할 수 있습니다.
  • 음수에 대한 %의 동작은 프로그래밍 언어별로 다양합니다.
  • Go에서는 부호는 항상 피제수(나누어지는 수)와 같아서, -5%3, -5%-3 모두 -2입니다.
  • / 연산은 피연산자의 타입에 따라 달라서, 5.0/4.0 == 1.25, 5/4 == 1 와 같은 결과가 나옵니다. 정수 나눗셈은 소수점 이하는 버립니다.
  • 산술 연산의 겨로가가 부호 여부와 상관없이 결과 타입에서 표현할 수 있는 비트 수보다 많은 경우 오버플로우가 발생합니다.
package main

import "fmt"

func main() {
 var u uint8 = 255
 fmt.Println(u, u+1, u*u) // "255 0 1"
 var i int8 = 127
 fmt.Println(i, i+1, i*i) // "127 -128 1"
}

예제코드 [tgpl/ch3/overflow/main.go]

코드 설명
부호없는 255를 비트로 표현하면 11111111 입니다.
u+1 : 255에 1을 더하면 100000000이 됩니다. 맨 왼쪽 1자리를 제외한 8자리만 uint8에 담기게 되어 00000000이 됩니다.
u*u : 255 곱하기 255를 하게 되면 오른쪽 8자리가 00000001이 됩니다.
부호있는 127을 비트로 표현하면 01111111입니다.
i+1 : 127에 1을 더하면 오른쪽 8자리가 10000000이 되는데, 부호 있는 숫자 표현에서는 -128을 나타냅니다.
i*i : 127 곱하기 127을 하게 되면 오른쪽 8자리가 00000001가 되어 1이 됩니다.

이항 비교 연산자

==   일치
!=   불일치
<    미만
<=   이하
>    초과
>=   이상 

단항 연산자

  • 정수의 경우 +x0+x의 축약형이고, -x0-x의 축약형입니다.
  • 부동소수점 수와 복소수의 경우 +x는 그냥 x이고, -xx의 부정입니다.
+   단항 긍정 (효과 없음)
-   단항 부정

비트 단위 이항 연산

&   비트 단위 AND
|   비트 단위 OR
^   비트 단위 XOR
&^  비트 제거 (AND NOT)
<<  왼쪽 시프트
>>  오른쪽 시프트
package main

import "fmt"

func main() {
    var x uint8 = 1<<1 | 1<<5
    var y uint8 = 1<<1 | 1<<2

    fmt.Printf("%08b\n", x) // "00100010", 집합 {1, 5}
    fmt.Printf("%08b\n", y) // "00000110", 집합 {1, 2}

    fmt.Printf("%08b\n", x&y)  // "00000010", 교집합 {1}
    fmt.Printf("%08b\n", x|y)  // "00100110", 집합 {1, 2, 5}
    fmt.Printf("%08b\n", x^y)  // "00100100", 교집합 {2, 5}
    fmt.Printf("%08b\n", x&^y) // "00000010", 차집합 {5}

    for i := uint(0); i < 8; i++ {
        if x&(1<<i) != 0 { // 멤버 확인
            fmt.Println(i) // "1", "5"
        }
    }

    fmt.Printf("%08b\n", x<<1) // "01000100", 집합 {2, 6}
    fmt.Printf("%08b\n", x>>1) // "00010001", 집합 {0, 4}
}

예제 코드 [tgpl/ch3/bit_op/main.go]

실행 결과
$ go run tgpl/ch3/bit_op
00100010
00000110
00000010
00100110
00100100
00100000
1
5
01000100
00010001

  • 왼쪽 시프트는 부호 없는 숫자의 오른쪽 시프트와 마찬가지로 빈 비트를 0으로 채우지만, 부호 있는 숫자를 오른쪽으로 시프트하면 빈 비트를 부호 비트를 복사본으로 채웁니다.
  • 정수를 비트 패턴으로 사용할 때는 반드시 부호 없는 산술 연산을 사용해야 합니다.

부호 없는 숫자

  • Go에는 부호 없는 숫자와 산술 연산이 있으며, 배열의 길이과 같이 일반적으로 음수가 될 수 없는 양의 숫자에 uint가 더 정확한 선택인 것 처럼 보임에도 불구하고 부호 있는 int를 쓰는 경향이 있습니다.
medals := []string{"gold", "silver", "bronze"}
for i := len(medals) - 1; i >= 0; i-- {
  fmt.Println(medals[i]) // "bronze", "silver", "gold"
}
  • len이 부호 없는 숫자를 반환한다면 큰 문제가 발생합니다. i도 uint가 되고 조건문 i >=0은 정의에 의해 항상 참이 됩니다.
  • 이 때문에 부호 없는 숫자는 비트 집합 구현, 이진 파일 포맷 분석, 해시, 암호화 등의 비트 단위 연산자, 또는 고유의 산술 연산이 필요한 경우에만 쓰이는 경향이 있습니다.

타입 변환

  • 공통 타입으로 변환
var apples int32 = 1
var oranges int16 = 2
var compote int = apples + oranges // 컴파일 오류

var compote = int(apples) + int(oranges) // 오류 없음
  • 정밀도가 변하는 타입 변환

    • 큰 정수를 작은 정수로 줄이거나 정수를 부동소수점 숫자로, 또는 그 반대로 하면 값이 바뀌거나 정밀도가 떨어질 수 있습니다.
    f := 3.141 // a float64
    i := int(f)
    fmt.Println(f, i) // "3.141 3"
    f = 1.99
    fmt.Println(int(f)) // "1"
    
    • 피연산자가 대상 타입 범위를 벗어나는 경우에는 그 동작이 구현에 의존하기 때문에 변환을 피해야 합니다.
    f := 1e100 // a float64
    i := int(f) // 결과는 구현 별로 다름
    

출력 포맷

  • fmt 패키지로 숫자를 출력할 때는 %d, %o, %x 포매터로 진법과 포맷을 제어할 수 있습니다.
package main

import "fmt"

func main() {
    o := 0666
    fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666"
    x := int64(0xdeadbeef)
    fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
    // output:
    // 3735928559 deadbeef 0xdeadbeef 0xDEADBEEF
}

예제 코드 [tgpl/ch3/main.go]

  • 보통 여러 % 포매터가 있는 Printf 포맷 문자열에는 동일한 개수의 부가적인 피연산자가 필요하지만, % 다음의 [1] '포매터'는 Printf가 첫 번째 피연산자를 반복하게 합니다.

이어보기

3.2 부동소수점 수 - 포스팅 후 연결 예정