본문으로 건너뛰기

실전 고수 팁

정수 타입 선택 기준

Go에서 어떤 정수 타입을 사용할지는 맥락에 따라 달라집니다.

기본 원칙

package main

import (
"fmt"
"unsafe"
)

func main() {
// 1. 일반 목적 (루프, 카운터, 인덱스) — int 사용
for i := 0; i < 10; i++ {
_ = i
}
items := []string{"a", "b", "c"}
for i := range items {
fmt.Println(i, items[i])
}

// 2. 명시적 크기 필요 — int64/int32 사용
var fileSize int64 = 5_368_709_120 // 5 GB (int32 범위 초과)
var timestamp int64 = 1_700_000_000 // Unix 타임스탬프

// 3. 외부 프로토콜/바이너리 — 크기 보장된 타입 사용
var ipByte uint8 = 192 // IP 주소 한 바이트
var portNum uint16 = 8080 // 포트 번호
var fileFlag uint32 = 0x04000000 // 파일 플래그

// 타입 크기 확인
fmt.Printf("int: %d bytes\n", unsafe.Sizeof(int(0)))
fmt.Printf("int32: %d bytes\n", unsafe.Sizeof(int32(0)))
fmt.Printf("int64: %d bytes\n", unsafe.Sizeof(int64(0)))

fmt.Println(fileSize, timestamp, ipByte, portNum, fileFlag)
}

언제 어떤 타입을 쓰는가

상황권장 타입이유
배열 인덱스, 루프 카운터int언어 관례, slice는 int 인덱스
파일 크기, 메모리 크기int642GB 이상 처리 가능
Unix 타임스탬프int642038년 문제 방지
네트워크 포트uint16프로토콜 정의 (0~65535)
바이트 단위 데이터uint8 / byte1바이트 단위 처리
비트 플래그uint32 / uint64음수 없이 비트 연산
개수/크기 반환값int표준 라이브러리 관례

float32 vs float64 — 부동소수점 비교의 함정

float64를 선택해야 하는 이유

package main

import (
"fmt"
"math"
)

func main() {
// float32는 정밀도 손실이 심함
var f32 float32 = 0.1 + 0.2
var f64 float64 = 0.1 + 0.2
fmt.Printf("float32: %.20f\n", f32) // 0.30000001192092895508
fmt.Printf("float64: %.20f\n", f64) // 0.30000000000000004441

// == 비교는 절대 금지
fmt.Println(f64 == 0.3) // false! (부동소수점 비교 불가)

// epsilon 비교 패턴 (권장)
const epsilon = 1e-9

almostEqual := func(a, b float64) bool {
return math.Abs(a-b) < epsilon
}

fmt.Println(almostEqual(0.1+0.2, 0.3)) // true

// 상대적 epsilon (큰 숫자 비교 시 더 정확)
relativeEqual := func(a, b, eps float64) bool {
if a == b {
return true
}
diff := math.Abs(a - b)
avg := (math.Abs(a) + math.Abs(b)) / 2
return diff/avg < eps
}

a, b := 1_000_000.0, 1_000_000.001
fmt.Println(relativeEqual(a, b, 1e-6)) // true (상대 오차 1백만 분의 1)
}

float32는 언제 쓰는가

package main

import (
"fmt"
"unsafe"
)

func main() {
// float32는 메모리가 중요할 때만 사용
// 예: 3D 그래픽스, ML 가중치, 대용량 수치 배열

// float64 배열 vs float32 배열 메모리 비교
f64Array := [1_000_000]float64{}
f32Array := [1_000_000]float32{}

fmt.Printf("float64 배열: %d MB\n", unsafe.Sizeof(f64Array)/1024/1024) // 8 MB
fmt.Printf("float32 배열: %d MB\n", unsafe.Sizeof(f32Array)/1024/1024) // 4 MB

// Go의 math 패키지는 모두 float64 기반
// float32를 math 함수와 쓰면 변환 비용 발생
var f float32 = 3.14
result := float32(math.Sin(float64(f))) // 변환 두 번 필요
fmt.Println(result)
}

숫자 오버플로 감지

Go에서 정수 오버플로는 자동 감지되지 않습니다. 수동으로 확인하거나 math/bits 패키지를 사용해야 합니다.

package main

import (
"fmt"
"math"
"math/bits"
)

func main() {
// 오버플로 발생 — 조용히 감싸짐 (wrap around)
var x int8 = 127
x++
fmt.Println(x) // -128 (오버플로 발생!)

// 덧셈 오버플로 수동 체크
safeAdd := func(a, b int64) (int64, bool) {
if b > 0 && a > math.MaxInt64-b {
return 0, false // 오버플로
}
if b < 0 && a < math.MinInt64-b {
return 0, false // 언더플로
}
return a + b, true
}

result, ok := safeAdd(math.MaxInt64, 1)
fmt.Printf("MaxInt64 + 1: %d, ok: %v\n", result, ok) // 0, false

result, ok = safeAdd(100, 200)
fmt.Printf("100 + 200: %d, ok: %v\n", result, ok) // 300, true

// math/bits 패키지 활용 (Go 1.9+)
a, b := uint64(math.MaxUint64), uint64(1)
sum, carry := bits.Add64(a, b, 0)
fmt.Printf("MaxUint64 + 1 = %d, carry = %d\n", sum, carry) // 0, 1

// 곱셈 오버플로 체크
hi, lo := bits.Mul64(1_000_000_000, 1_000_000_000)
fmt.Printf("1e9 * 1e9: hi=%d, lo=%d\n", hi, lo) // hi=0, lo=10^18
}

문자열 메모리 최적화

strings.Builder vs bytes.Buffer vs []byte

package main

import (
"bytes"
"fmt"
"strings"
"time"
)

const iterations = 100_000

func withStringPlus() string {
s := ""
for i := 0; i < iterations; i++ {
s += "x"
}
return s
}

func withStringsBuilder() string {
var b strings.Builder
b.Grow(iterations) // 미리 용량 확보 (선택적)
for i := 0; i < iterations; i++ {
b.WriteByte('x')
}
return b.String()
}

func withBytesBuffer() string {
var b bytes.Buffer
b.Grow(iterations)
for i := 0; i < iterations; i++ {
b.WriteByte('x')
}
return b.String()
}

func withByteSlice() string {
b := make([]byte, 0, iterations)
for i := 0; i < iterations; i++ {
b = append(b, 'x')
}
return string(b)
}

func benchmark(name string, fn func() string) {
start := time.Now()
_ = fn()
fmt.Printf("%-25s %v\n", name+":", time.Since(start))
}

func main() {
benchmark("string + (plus)", withStringPlus)
benchmark("strings.Builder", withStringsBuilder)
benchmark("bytes.Buffer", withBytesBuffer)
benchmark("[]byte + string()", withByteSlice)

// strings.Builder 고급 사용
var sb strings.Builder
sb.Grow(256) // 예상 크기 미리 할당 — 재할당 방지

items := []string{"apple", "banana", "cherry"}
for i, item := range items {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(item)
}
fmt.Println(sb.String()) // apple, banana, cherry
}

[]byte 재사용 패턴

package main

import (
"fmt"
"strings"
)

func processLines(lines []string) []string {
// 버퍼 재사용으로 GC 압력 감소
buf := make([]byte, 0, 256) // 초기 용량 256바이트
results := make([]string, 0, len(lines))

for _, line := range lines {
buf = buf[:0] // 길이를 0으로 초기화 (용량은 유지)
// 처리 로직: 앞뒤 공백 제거 + 대문자 변환
trimmed := strings.TrimSpace(line)
buf = append(buf, strings.ToUpper(trimmed)...)
results = append(results, string(buf))
}
return results
}

func main() {
lines := []string{
" hello world ",
" go language ",
" programming ",
}

results := processLines(lines)
for _, r := range results {
fmt.Println(r)
}
}

구조체 필드 정렬 — 메모리 절약

CPU는 메모리를 자신의 워드 크기(64비트 시스템: 8바이트)에 맞게 정렬된 주소에서 읽을 때 더 효율적입니다. Go 컴파일러는 이를 위해 구조체 필드 사이에 패딩(padding) 을 삽입합니다. 필드 순서를 최적화하면 패딩을 줄여 메모리를 절약할 수 있습니다.

package main

import (
"fmt"
"unsafe"
)

// 비효율적인 레이아웃: 패딩이 많음
type BadLayout struct {
A bool // 1 byte + 7 byte padding
B float64 // 8 bytes
C bool // 1 byte + 7 byte padding
D int32 // 4 bytes + 4 byte padding
}

// 효율적인 레이아웃: 큰 타입 먼저, 작은 타입 나중에
type GoodLayout struct {
B float64 // 8 bytes
D int32 // 4 bytes
A bool // 1 byte
C bool // 1 byte + 2 byte padding
}

func main() {
bad := BadLayout{}
good := GoodLayout{}

fmt.Printf("BadLayout 크기: %d bytes\n", unsafe.Sizeof(bad)) // 32 bytes
fmt.Printf("GoodLayout 크기: %d bytes\n", unsafe.Sizeof(good)) // 16 bytes

// 각 필드의 오프셋 확인
fmt.Println("\nBadLayout 필드 오프셋:")
fmt.Printf(" A(bool): %d\n", unsafe.Offsetof(bad.A))
fmt.Printf(" B(float64): %d\n", unsafe.Offsetof(bad.B))
fmt.Printf(" C(bool): %d\n", unsafe.Offsetof(bad.C))
fmt.Printf(" D(int32): %d\n", unsafe.Offsetof(bad.D))

fmt.Println("\nGoodLayout 필드 오프셋:")
fmt.Printf(" B(float64): %d\n", unsafe.Offsetof(good.B))
fmt.Printf(" D(int32): %d\n", unsafe.Offsetof(good.D))
fmt.Printf(" A(bool): %d\n", unsafe.Offsetof(good.A))
fmt.Printf(" C(bool): %d\n", unsafe.Offsetof(good.C))

// 대용량 슬라이스에서의 차이
nElements := 1_000_000
badSlice := make([]BadLayout, nElements)
goodSlice := make([]GoodLayout, nElements)

fmt.Printf("\n백만 개 슬라이스:\n")
fmt.Printf(" BadLayout: %d MB\n", unsafe.Sizeof(badSlice[0])*uintptr(nElements)/1024/1024)
fmt.Printf(" GoodLayout: %d MB\n", unsafe.Sizeof(goodSlice[0])*uintptr(nElements)/1024/1024)
}

출력:

BadLayout 크기:  32 bytes
GoodLayout 크기: 16 bytes

string([]byte) 변환 비용과 회피 패턴

string[]byte 간 변환은 메모리 복사를 유발합니다. 핫 경로(hot path)에서는 이를 피하는 패턴을 사용합니다.

package main

import (
"fmt"
"strings"
)

func main() {
// 문자열 비교 — []byte 변환 없이 직접 비교
b := []byte("hello world")

// 나쁜 예: 변환 비용 발생
if string(b) == "hello world" {
fmt.Println("일치 (변환 비용 발생)")
}

// 좋은 예: bytes 패키지 사용
// import "bytes"
// if bytes.Equal(b, []byte("hello world")) { ... }

// strings 함수를 []byte에 직접 적용하는 패턴
// strings.HasPrefix는 string만 받지만, 변환 피하기
prefix := "hello"
hasPrefix := len(b) >= len(prefix) &&
string(b[:len(prefix)]) == prefix
fmt.Println("prefix:", hasPrefix) // true

// 실전: HTTP 헤더 파싱에서 자주 사용
header := []byte("Content-Type: application/json")
colonIdx := strings.IndexByte(string(header), ':')
if colonIdx >= 0 {
key := strings.TrimSpace(string(header[:colonIdx]))
value := strings.TrimSpace(string(header[colonIdx+1:]))
fmt.Printf("키: %q, 값: %q\n", key, value)
}
}

큰 수 처리 — math/big 패키지

기본 정수 타입의 범위를 초과하는 수를 처리할 때는 math/big 패키지를 사용합니다.

package main

import (
"fmt"
"math/big"
)

func main() {
// BigInt — 임의 정밀도 정수
a := new(big.Int).SetString("123456789012345678901234567890", 10)
// SetString의 두 번째 반환값은 성공 여부 (bool)
a, _ = new(big.Int).SetString("123456789012345678901234567890", 10)
b, _ := new(big.Int).SetString("987654321098765432109876543210", 10)

sum := new(big.Int).Add(a, b)
product := new(big.Int).Mul(a, b)

fmt.Println("합:", sum)
fmt.Println("곱:", product)

// 팩토리얼 계산 (일반 int64로는 20! 이후 오버플로)
factorial := func(n int64) *big.Int {
result := big.NewInt(1)
for i := int64(2); i <= n; i++ {
result.Mul(result, big.NewInt(i))
}
return result
}

fmt.Printf("100! = %s\n", factorial(100).String()[:20]+"...") // 앞 20자리만

// BigFloat — 임의 정밀도 부동소수점
pi := new(big.Float).SetPrec(256) // 256비트 정밀도
pi.SetString("3.14159265358979323846264338327950288")
fmt.Printf("π (256-bit): %s\n", pi.Text('f', 30)) // 소수점 30자리
}

상수 표현식으로 컴파일 타임 계산

package main

import (
"fmt"
"time"
)

const (
// 컴파일 타임에 계산됨 — 런타임 비용 없음
KB = 1 << 10 // 1024
MB = 1 << 20 // 1048576
GB = 1 << 30 // 1073741824

MaxPacketSize = 64 * KB // 65536
DefaultBuffer = 4 * MB // 4194304
MaxFileSize = 2*GB - 1 // 2147483647

// 시간 상수
OneDay = 24 * time.Hour
OneWeek = 7 * OneDay
)

func main() {
fmt.Printf("MaxPacketSize: %d bytes (%.1f KB)\n",
MaxPacketSize, float64(MaxPacketSize)/KB)
fmt.Printf("DefaultBuffer: %d bytes (%.1f MB)\n",
DefaultBuffer, float64(DefaultBuffer)/MB)
fmt.Printf("MaxFileSize: %d bytes (%.1f GB)\n",
MaxFileSize, float64(MaxFileSize)/GB)

fmt.Printf("OneDay: %v\n", OneDay) // 24h0m0s
fmt.Printf("OneWeek: %v\n", OneWeek) // 168h0m0s

// 타임아웃 설정
timeout := 5 * time.Second
fmt.Println("타임아웃:", timeout)
}

타입 정의로 의미 있는 타입 만들기

타입 정의는 단순한 별칭이 아니라 도메인 개념을 코드로 표현 하는 강력한 도구입니다.

package main

import (
"fmt"
"strings"
)

// 의미 있는 타입 정의
type UserID int64
type OrderID int64
type ProductID int64
type Email string
type PhoneNumber string
type Cents int64 // 금액을 정수(센트)로 표현

// Email 타입에 유효성 검사 메서드 추가
func (e Email) IsValid() bool {
return strings.Contains(string(e), "@") &&
strings.Contains(string(e), ".")
}

func (e Email) Domain() string {
parts := strings.Split(string(e), "@")
if len(parts) != 2 {
return ""
}
return parts[1]
}

// Cents 타입에 변환 메서드 추가
func (c Cents) ToDollars() float64 {
return float64(c) / 100
}

func (c Cents) String() string {
return fmt.Sprintf("$%.2f", c.ToDollars())
}

// 함수 시그니처가 명확해짐 — UserID와 OrderID를 바꿔 넣으면 컴파일 에러
func processOrder(uid UserID, oid OrderID, amount Cents) {
fmt.Printf("사용자 %d의 주문 %d: %s 처리\n", uid, oid, amount)
}

func main() {
uid := UserID(1001)
oid := OrderID(5001)
price := Cents(2999) // $29.99

processOrder(uid, oid, price)

// processOrder(int64(oid), int64(uid), price) // 컴파일 에러!

email := Email("user@example.com")
fmt.Printf("이메일 유효: %v\n", email.IsValid())
fmt.Printf("도메인: %s\n", email.Domain())

// Cents 연산
total := Cents(1500 + 750 + 299) // 복수의 상품
fmt.Printf("합계: %s\n", total) // $25.49
}

실무 Anti-patterns

Anti-pattern 1: int를 bool 대신 사용

// 나쁜 예: C 스타일
func isActiveOld(status int) int {
return status // 0 = inactive, 1 = active (모호함)
}

// 좋은 예: 명확한 bool
func isActive(status string) bool {
return status == "active"
}

// 좋은 예: 의미 있는 타입
type UserStatus string

const (
UserStatusActive UserStatus = "active"
UserStatusInactive UserStatus = "inactive"
UserStatusBanned UserStatus = "banned"
)

func (s UserStatus) IsActive() bool {
return s == UserStatusActive
}

Anti-pattern 2: 불필요한 string ↔ []byte 변환

package main

import (
"bytes"
"fmt"
"strings"
)

func main() {
data := []byte("Hello, World!")

// 나쁜 예: 변환 두 번
if strings.Contains(string(data), "World") {
fmt.Println("Found (with conversion)")
}

// 좋은 예: bytes 패키지 직접 사용
if bytes.Contains(data, []byte("World")) {
fmt.Println("Found (no conversion)")
}

// 또 다른 나쁜 예: 루프 안에서 반복 변환
messages := [][]byte{
[]byte("hello"),
[]byte("world"),
[]byte("go"),
}

// 나쁜 예
var result1 []string
for _, msg := range messages {
result1 = append(result1, strings.ToUpper(string(msg)))
}

// 좋은 예: 변환을 최소화 (bytes.ToUpper 사용)
var result2 []string
for _, msg := range messages {
upper := bytes.ToUpper(msg) // string 변환 없이 처리
result2 = append(result2, string(upper)) // 최종에만 변환
}

fmt.Println(result1)
fmt.Println(result2)
}

Anti-pattern 3: 정밀도가 필요한 금액 계산에 float 사용

package main

import (
"fmt"
"math/big"
)

func main() {
// 나쁜 예: float64로 금액 계산
price := 0.1
quantity := 3
totalBad := price * float64(quantity)
fmt.Printf("float64 합계: %.20f\n", totalBad) // 0.30000000000000004441

// 좋은 예 1: 정수(센트) 사용
priceCents := 10 // 10센트 = $0.10
totalCents := priceCents * quantity
fmt.Printf("정수 합계: %d센트 = $%.2f\n", totalCents, float64(totalCents)/100)

// 좋은 예 2: math/big.Float 사용
priceF := new(big.Float).SetPrec(64).SetFloat64(0.1)
quantityF := new(big.Float).SetPrec(64).SetInt64(int64(quantity))
totalBigF := new(big.Float).Mul(priceF, quantityF)
fmt.Printf("big.Float 합계: %s\n", totalBigF.Text('f', 10))
}

Anti-pattern 4: 전역 변수 남용

package main

import "fmt"

// 나쁜 예: 전역 상태 — 테스트와 동시성에 위험
var globalConfig struct {
Host string
Port int
}

// 좋은 예: 설정을 함수나 구조체로 전달
type Config struct {
Host string
Port int
}

type Server struct {
config Config
}

func NewServer(cfg Config) *Server {
return &Server{config: cfg}
}

func (s *Server) Address() string {
return fmt.Sprintf("%s:%d", s.config.Host, s.config.Port)
}

func main() {
cfg := Config{Host: "localhost", Port: 8080}
srv := NewServer(cfg)
fmt.Println("서버 주소:", srv.Address())
}

전체 정리 — Ch2 핵심 요약

주제핵심 포인트
변수 선언:=는 함수 내부에서만, 타입 없는 변수는 Zero 값
타입 시스템암묵적 변환 없음, 명시적 T(v) 변환 필수
상수iota로 열거형 구현, 타입 없는 상수가 더 유연
문자열range로 rune 순회, strings.Builder로 효율적 연결
메모리구조체 필드 정렬, []byte 재사용, float64 우선
타입 정의도메인 타입으로 컴파일 타임 안전성 확보