안녕하세요. 개발자 모도리입니다.
The Go Programming Language 라는 책으로 Go를 공부하고 있으며, 해당 책의 내용을 요약 정리해서 올리려고 합니다. 저는 번역본을 구매해서 공부하고 있습니다.
게시물에 예제코드 라고 나오는 것들은 https://github.com/modolee/tgpl.git 에서 다운 받으실 수 있습니다.
지난 게시물
- [Go] Mac에서 Atom으로 Go 개발 환경 구축하기
- [The Go Programming Language] 1장 튜토리얼
- [The Go Programming Language] 2장 프로그램 구조
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이 됩니다.
- 음수 표현에 대한 자세한 내용은 음수 표현법 - 2의 보수 이 글을 참고해 주세요
이항 비교 연산자
== 일치 != 불일치 < 미만 <= 이하 > 초과 >= 이상
단항 연산자
- 정수의 경우
+x
는0+x
의 축약형이고,-x
는0-x
의 축약형입니다. - 부동소수점 수와 복소수의 경우
+x
는 그냥x
이고,-x
는x
의 부정입니다.
+ 단항 긍정 (효과 없음) - 단항 부정
비트 단위 이항 연산
& 비트 단위 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가 첫 번째 피연산자를 반복하게 합니다.