본문으로 건너뛰기

기본 타입

Go의 타입 시스템

Go는 정적 타입 언어 입니다. 모든 변수는 컴파일 타임에 타입이 결정되며, 암묵적인 타입 변환이 없습니다. 서로 다른 타입의 값을 연산하려면 반드시 명시적으로 변환해야 합니다.

이 철학은 타입 관련 버그를 컴파일 시점에 잡아주어 런타임 오류를 크게 줄입니다.


정수 타입 (Integer Types)

부호 있는 정수 (Signed Integer)

타입크기범위
int81 byte-128 ~ 127
int162 bytes-32,768 ~ 32,767
int324 bytes-2,147,483,648 ~ 2,147,483,647
int648 bytes-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
int플랫폼 의존32비트 시스템: int32, 64비트 시스템: int64

부호 없는 정수 (Unsigned Integer)

타입크기범위
uint81 byte0 ~ 255
uint162 bytes0 ~ 65,535
uint324 bytes0 ~ 4,294,967,295
uint648 bytes0 ~ 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)

타입크기정밀도
float324 bytes약 7자리 십진수
float648 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)

타입크기구성
complex648 bytesfloat32 실수부 + float32 허수부
complex12816 bytesfloat64 실수부 + 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 타입

booltrue 또는 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
}