기본 타입
Go의 타입 시스템
Go는 정적 타입 언어 입니다. 모든 변수는 컴파일 타임에 타입이 결정되며, 암묵적인 타입 변환이 없습니다. 서로 다른 타입의 값을 연산하려면 반드시 명시적으로 변환해야 합니다.
이 철학은 타입 관련 버그를 컴파일 시점에 잡아주어 런타임 오류를 크게 줄입니다.
정수 타입 (Integer Types)
부호 있는 정수 (Signed Integer)
| 타입 | 크기 | 범위 |
|---|---|---|
| int8 | 1 byte | -128 ~ 127 |
| int16 | 2 bytes | -32,768 ~ 32,767 |
| int32 | 4 bytes | -2,147,483,648 ~ 2,147,483,647 |
| int64 | 8 bytes | -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 |
| int | 플랫폼 의존 | 32비트 시스템: int32, 64비트 시스템: int64 |
부호 없는 정수 (Unsigned Integer)
| 타입 | 크기 | 범위 |
|---|---|---|
| uint8 | 1 byte | 0 ~ 255 |
| uint16 | 2 bytes | 0 ~ 65,535 |
| uint32 | 4 bytes | 0 ~ 4,294,967,295 |
| uint64 | 8 bytes | 0 ~ 18,446,744,073,709,551,615 |
| uint | 플랫폼 의존 | 32비트 시스템: uint32, 64비트 시스템: uint64 |
| uintptr | 플랫폼 의존 | 포인터를 저장하기에 충분한 크기 |
package main
import (
"fmt"
"math"
)
func main() {
// 각 타입의 최댓값 출력
fmt.Println("int8 max:", math.MaxInt8) // 127
fmt.Println("int16 max:", math.MaxInt16) // 32767
fmt.Println("int32 max:", math.MaxInt32) // 2147483647
fmt.Println("int64 max:", math.MaxInt64) // 9223372036854775807
fmt.Println("uint8 max:", math.MaxUint8) // 255
fmt.Println("uint16 max:", math.MaxUint16) // 65535
fmt.Println("uint32 max:", math.MaxUint32) // 4294967295
// 실제 사용 예시
var counter int = 0
var fileSize int64 = 1_234_567_890
var flags uint8 = 0b11001010
var port uint16 = 8080
fmt.Println(counter, fileSize, flags, port)
}
int vs int64 — 플랫폼 의존 크기
int는 플랫폼(운영체제와 CPU 아키텍처)에 따라 크기가 달라집니다. 현대적인 64비트 시스템에서는 8바이트지만, 크로스 플랫폼 코드에서 명시적인 크기가 필요하다면 int64를 사용해야 합니다.
package main
import (
"fmt"
"unsafe"
)
func main() {
var i int
var i64 int64
fmt.Printf("int 크기: %d bytes\n", unsafe.Sizeof(i)) // 64비트: 8
fmt.Printf("int64 크기: %d bytes\n", unsafe.Sizeof(i64)) // 항상 8
// 일반 카운터, 인덱스 — int 사용 (관례)
for i := 0; i < 10; i++ {
_ = i
}
// 파일 크기, 타임스탬프 — int64 사용 (명시적 크기 보장)
var timestamp int64 = 1_700_000_000
var fileOffset int64 = 4_294_967_296 // 4GB 이상
fmt.Println(timestamp, fileOffset)
}
부동소수점 타입 (Floating-Point Types)
| 타입 | 크기 | 정밀도 |
|---|---|---|
| float32 | 4 bytes | 약 7자리 십진수 |
| float64 | 8 bytes | 약 15~16자리 십진수 |
Go에서 소수 리터럴의 기본 타입은 float64입니다.
package main
import (
"fmt"
"math"
)
func main() {
var f32 float32 = 3.14159265358979
var f64 float64 = 3.14159265358979
// float32는 정밀도 손실 발생
fmt.Printf("float32: %.10f\n", f32) // 3.1415927410
fmt.Printf("float64: %.10f\n", f64) // 3.1415926536
// 수학 상수
fmt.Println("π:", math.Pi)
fmt.Println("e:", math.E)
fmt.Println("최대 float64:", math.MaxFloat64)
// 특수 값
inf := math.Inf(1) // 양의 무한대
nan := math.NaN() // NaN (Not a Number)
fmt.Println("inf:", inf)
fmt.Println("nan:", nan)
fmt.Println("IsNaN:", math.IsNaN(nan))
fmt.Println("IsInf:", math.IsInf(inf, 1))
}
복소수 타입 (Complex Types)
| 타입 | 크기 | 구성 |
|---|---|---|
| complex64 | 8 bytes | float32 실수부 + float32 허수부 |
| complex128 | 16 bytes | float64 실수부 + float64 허수부 |
package main
import (
"fmt"
"math/cmplx"
)
func main() {
// 복소수 리터럴
c1 := 3 + 4i // complex128 (기본)
c2 := complex(1, 2) // complex(실수부, 허수부)
fmt.Println("c1:", c1)
fmt.Println("c2:", c2)
// 실수부, 허수부 추출
fmt.Println("실수부:", real(c1))
fmt.Println("허수부:", imag(c1))
// 복소수 연산
fmt.Println("절댓값:", cmplx.Abs(c1)) // 5 (3-4-5 피타고라스)
fmt.Println("제곱근:", cmplx.Sqrt(-1)) // (0+1i)
}
bool 타입
bool은 true 또는 false 두 값만 가집니다.
package main
import "fmt"
func main() {
var isReady bool = true
var hasError = false
// 논리 연산자
fmt.Println(true && false) // AND: false
fmt.Println(true || false) // OR: true
fmt.Println(!true) // NOT: false
// 비교 연산자 결과는 bool
x, y := 10, 20
fmt.Println(x < y) // true
fmt.Println(x == y) // false
fmt.Println(x != y) // true
// 조건식
if isReady && !hasError {
fmt.Println("시스템 준비 완료")
}
// Go에서 bool과 int는 다른 타입 — 변환 불가
// var n int = true // 컴파일 에러!
var n int
if isReady {
n = 1
}
fmt.Println("n:", n)
}
string 타입
Go의 문자열은 불변(immutable)한 바이트 시퀀스 입니다. UTF-8로 인코딩되어 있으며, 바이트 배열([]byte)로 취급할 수 있습니다.
package main
import "fmt"
func main() {
// 큰따옴표 문자열 — 이스케이프 시퀀스 처리
s1 := "Hello, 세계!\n"
fmt.Print(s1) // 개행 처리됨
// 백틱 문자열 (raw string) — 이스케이프 없이 있는 그대로
s2 := `첫 번째 줄
두 번째 줄
경로: C:\Users\Go`
fmt.Println(s2)
// 문자열은 불변 — 인덱스로 개별 바이트에 접근 가능 (읽기만)
s3 := "Hello"
fmt.Printf("s3[0] = %d (%c)\n", s3[0], s3[0]) // 72 (H)
// s3[0] = 'h' // 컴파일 에러! 문자열은 불변
// 문자열 연결
hello := "Hello"
world := "World"
greeting := hello + ", " + world + "!"
fmt.Println(greeting)
// 문자열 길이 — 바이트 수
korean := "안녕"
fmt.Printf("len(%q) = %d bytes\n", korean, len(korean)) // 6 bytes (UTF-8에서 한글 1자 = 3bytes)
}
byte와 rune
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
// byte = uint8 (ASCII 문자 한 개)
var b byte = 'A'
fmt.Printf("byte: %d (%c)\n", b, b) // 65 (A)
// rune = int32 (유니코드 코드 포인트)
var r rune = '한'
fmt.Printf("rune: %d (%c)\n", r, r) // 54620 (한)
fmt.Printf("rune hex: U+%04X\n", r) // U+D55C
// 문자열의 바이트 수 vs 문자 수
s := "Hello, 世界"
fmt.Printf("바이트 수: %d\n", len(s)) // 13
fmt.Printf("문자(rune) 수: %d\n", utf8.RuneCountInString(s)) // 9
// rune 슬라이스로 변환하면 각 문자에 접근 가능
runes := []rune(s)
fmt.Printf("runes[7]: %c\n", runes[7]) // 世
fmt.Printf("runes[8]: %c\n", runes[8]) // 界
// range로 rune 순회
for i, r := range s {
fmt.Printf("인덱스 %2d: %c (U+%04X)\n", i, r, r)
}
}
실행 결과 (일부):
바이트 수: 13
문자(rune) 수: 9
runes[7]: 世
인덱스 0: H (U+0048)
인덱스 1: e (U+0065)
...
인덱스 7: 世 (U+4E16)
인덱스 10: 界 (U+754C)
숫자 리터럴 표기법
Go 1.13부터 다양한 진법과 가독성 향상을 위한 구분자를 지원합니다.
package main
import "fmt"
func main() {
// 10진수 (기본)
decimal := 1_000_000 // 언더스코어로 가독성 향상
fmt.Println("십진수:", decimal) // 1000000
// 2진수 (0b 또는 0B 접두사)
binary := 0b1010_1100
fmt.Printf("2진수: %b = %d\n", binary, binary) // 10101100 = 172
// 8진수 (0o 또는 0O 접두사, 또는 0 접두사)
octal := 0o755
fmt.Printf("8진수: %o = %d\n", octal, octal) // 755 = 493
// 16진수 (0x 또는 0X 접두사)
hex := 0xFF_EC_D0_12
fmt.Printf("16진수: %X = %d\n", hex, hex)
// 부동소수점 리터럴
f1 := 1_234.567_89
f2 := 1.5e10 // 1.5 × 10^10
f3 := 0x1p-2 // 16진수 부동소수점 (0.25)
fmt.Println(f1, f2, f3)
}
명시적 타입 변환
Go는 암묵적 타입 변환이 없습니다. 서로 다른 타입 간 연산은 반드시 명시적으로 변환해야 합니다.
package main
import "fmt"
func main() {
var i int = 42
var f float64 = 3.14
// int → float64
result := float64(i) + f
fmt.Printf("%.2f\n", result) // 45.14
// float64 → int (소수점 이하 절삭)
truncated := int(f)
fmt.Println(truncated) // 3
// int → string (주의: 유니코드 코드포인트 변환!)
var code int = 65
wrongWay := string(code) // "A" (코드포인트 65 = 'A')
fmt.Println(wrongWay) // A (숫자 "65"가 아님!)
// 숫자를 문자열로 변환하려면 fmt.Sprintf 또는 strconv 사용
// rightWay := fmt.Sprintf("%d", code) // "65"
// string ↔ []byte 변환 (메모리 복사 발생)
s := "Hello, Go"
b := []byte(s) // string → []byte
b[0] = 'h' // []byte는 수정 가능!
s2 := string(b) // []byte → string
fmt.Println(s2) // hello, Go
// string ↔ []rune 변환
korean := "안녕하세요"
r := []rune(korean)
fmt.Printf("첫 글자: %c\n", r[0]) // 안
fmt.Printf("글자 수: %d\n", len(r)) // 5
}
타입 변환 실전 예시
package main
import (
"fmt"
"unsafe"
)
func main() {
// 다른 정수 타입 간 변환
var small int8 = 100
var large int64 = int64(small) // int8 → int64
fmt.Println(large)
// 오버플로 주의: 큰 타입 → 작은 타입
var big int = 300
var tiny int8 = int8(big) // 300은 int8 범위 초과!
fmt.Println(tiny) // 44 (300 mod 256 = 44, 예상치 못한 결과!)
// unsafe.Sizeof로 타입 크기 확인
fmt.Println("int8 크기:", unsafe.Sizeof(int8(0))) // 1
fmt.Println("int16 크기:", unsafe.Sizeof(int16(0))) // 2
fmt.Println("int32 크기:", unsafe.Sizeof(int32(0))) // 4
fmt.Println("int64 크기:", unsafe.Sizeof(int64(0))) // 8
fmt.Println("float32 크기:", unsafe.Sizeof(float32(0))) // 4
fmt.Println("float64 크기:", unsafe.Sizeof(float64(0))) // 8
}
타입 별칭과 타입 정의
Go에서 새로운 타입을 만드는 두 가지 방법이 있습니다.
타입 정의 (Type Definition)
package main
import "fmt"
// 타입 정의: 완전히 새로운 타입 생성
type Celsius float64
type Fahrenheit float64
type Meter float64
type Kilogram float64
func (c Celsius) ToFahrenheit() Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
func main() {
temp := Celsius(100.0)
fmt.Printf("%.1f°C = %.1f°F\n", temp, temp.ToFahrenheit())
// 타입 안전성: 다른 타입 간 직접 연산 불가
var c Celsius = 100
// var f Fahrenheit = c // 컴파일 에러! 타입이 다름
var f Fahrenheit = Fahrenheit(c) // 명시적 변환은 OK
fmt.Println(f)
// 기반 타입으로도 변환 가능
raw := float64(c)
fmt.Println(raw)
}
타입 별칭 (Type Alias)
package main
import "fmt"
// 타입 별칭: 동일한 타입에 다른 이름 부여 (Go 1.9+)
type MyString = string // MyString은 string과 완전히 동일
func main() {
var s MyString = "Hello"
var t string = s // 별칭이므로 변환 없이 대입 가능
fmt.Println(s, t)
// byte와 rune은 실제로 타입 별칭
// type byte = uint8
// type rune = int32
var b byte = 255
var u uint8 = b // 별칭이므로 바로 대입 가능
fmt.Println(b, u)
}
타입 정의 vs 타입 별칭 비교
| 구분 | 타입 정의 (type T U) | 타입 별칭 (type T = U) |
|---|---|---|
| 새 타입 생성 | 예 | 아니오 (동일 타입) |
| 메서드 추가 | 가능 | 불가능 |
| 암묵적 변환 | 불가 (명시적 변환 필요) | 가능 (같은 타입) |
| 주요 용도 | 도메인 타입, 안전한 API | 호환성, 리팩터링 |
실전 예제: 타입 변환 종합
package main
import (
"fmt"
"math"
"strconv"
)
// 온도 변환기
type Celsius float64
type Fahrenheit float64
type Kelvin float64
func celsiusToFahrenheit(c Celsius) Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
func celsiusToKelvin(c Celsius) Kelvin {
return Kelvin(c + 273.15)
}
func main() {
// 문자열 → 숫자 변환 (strconv 사용)
input := "36.6"
tempF64, err := strconv.ParseFloat(input, 64)
if err != nil {
fmt.Println("변환 에러:", err)
return
}
temp := Celsius(tempF64)
fmt.Printf("체온: %.1f°C = %.1f°F = %.2fK\n",
temp,
celsiusToFahrenheit(temp),
celsiusToKelvin(temp))
// 숫자 → 문자열 변환
score := 98
scoreStr := strconv.Itoa(score)
fmt.Println("점수 문자열:", scoreStr + "점")
// 수치 계산에서의 타입 변환
total := 7
parts := 3
// 정수 나눗셈은 소수점 버림
intDivision := total / parts
// float64로 변환 후 나눗셈
floatDivision := float64(total) / float64(parts)
fmt.Printf("정수 나눗셈: %d\n", intDivision) // 2
fmt.Printf("실수 나눗셈: %.4f\n", floatDivision) // 2.3333
fmt.Printf("반올림: %.0f\n", math.Round(floatDivision)) // 2
}
고수 팁
팁 1: 일반 목적에는 int, 명시적 크기가 필요할 때는 int64
표준 라이브러리와 관례상 인덱스, 카운터에는 int를 씁니다. 파일 크기, Unix 타임스탬프, 네트워크 프로토콜처럼 크기가 보장되어야 할 때는 int64를 사용합니다.
팁 2: float64가 float32보다 대부분 더 좋다
Go의 수학 함수(math 패키지)는 float64 기반입니다. float32는 GPU 연산이나 대용량 수치 배열처럼 메모리 절약이 중요한 경우에만 사용합니다.
팁 3: 타입 정의로 단위 실수를 방지하라
type UserID int64
type OrderID int64
func processOrder(uid UserID, oid OrderID) { ... }
// 이렇게 하면 실수로 uid와 oid를 바꿔서 넣는 버그를 컴파일 타임에 잡을 수 있음
팁 4: 큰 타입에서 작은 타입으로의 변환 시 오버플로 확인
func safeInt64ToInt32(n int64) (int32, error) {
if n > math.MaxInt32 || n < math.MinInt32 {
return 0, fmt.Errorf("overflow: %d does not fit in int32", n)
}
return int32(n), nil
}