logo hsb.horse
← 스니펫 목록으로 돌아가기

Snippets

Go 데이터 타입별 메모리 크기

Go의 각 데이터 타입이 차지하는 메모리 크기 목록과 메모리 효율적인 코딩을 위한 실용적인 팁.

게시일: 수정일:

Go 데이터 타입의 메모리 사용량을 이해하는 것은 메모리 효율적인 프로그램을 작성하는 데 매우 중요합니다. 특히 대용량 데이터를 처리하거나 임베디드 시스템, 고성능 애플리케이션에서는 데이터 구조 선택이 성능에 직접적인 영향을 미칩니다.

데이터 타입별 메모리 크기

데이터 타입크기 (바이트)비고
int / uint8 / 864비트 시스템에서는 8바이트, 32비트에서는 4바이트
int8 / uint81 / 1
int16 / uint162 / 2
int32 / uint324 / 4
int64 / uint648 / 8
float324IEEE 754 단정밀도 부동소수점
float648IEEE 754 배정밀도 부동소수점
complex648float32 × 2 (실수부와 허수부)
complex12816float64 × 2 (실수부와 허수부)
byte (uint8의 별칭)1
rune (int32의 별칭)4유니코드 코드 포인트
bool1내부적으로 1바이트 사용
string16포인터(8) + 길이(8), 실제 데이터는 별도 영역
슬라이스 (예: []int)24포인터(8) + 길이(8) + 용량(8)
맵 (예: map[string]int)8내부 구조체로의 포인터
채널 (예: chan int)8내부 구조체로의 포인터
구조체 (예: struct{})0필드가 없으면 0
인터페이스16타입 정보(8) + 값의 포인터(8)

※ 크기는 64비트 시스템의 표준 값입니다. 32비트 시스템에서는 포인터 크기가 4바이트입니다.

샘플 구현

크기 확인

unsafe.Sizeof를 사용하여 런타임에 데이터 타입의 크기를 확인할 수 있습니다.

package main
import (
"fmt"
"unsafe"
)
func main() {
// 기본 타입
var i int
var i8 int8
var f32 float32
var f64 float64
var b bool
var r rune
fmt.Printf("int: %d bytes\n", unsafe.Sizeof(i))
fmt.Printf("int8: %d bytes\n", unsafe.Sizeof(i8))
fmt.Printf("float32: %d bytes\n", unsafe.Sizeof(f32))
fmt.Printf("float64: %d bytes\n", unsafe.Sizeof(f64))
fmt.Printf("bool: %d bytes\n", unsafe.Sizeof(b))
fmt.Printf("rune: %d bytes\n", unsafe.Sizeof(r))
// 문자열, 슬라이스, 맵
var s string
var slice []int
var m map[string]int
fmt.Printf("string: %d bytes\n", unsafe.Sizeof(s))
fmt.Printf("slice: %d bytes\n", unsafe.Sizeof(slice))
fmt.Printf("map: %d bytes\n", unsafe.Sizeof(m))
// 인터페이스
var iface interface{}
fmt.Printf("interface{}: %d bytes\n", unsafe.Sizeof(iface))
}

구조체 패딩 확인

구조체는 필드 배치 순서에 따라 메모리 효율성이 달라집니다. 패딩으로 인해 예상보다 많은 메모리를 소비할 수 있습니다.

package main
import (
"fmt"
"unsafe"
)
// 비효율적인 배치
type BadLayout struct {
a bool // 1 byte + 7 bytes padding
b int64 // 8 bytes
c bool // 1 byte + 7 bytes padding
d int64 // 8 bytes
}
// 효율적인 배치
type GoodLayout struct {
b int64 // 8 bytes
d int64 // 8 bytes
a bool // 1 byte
c bool // 1 byte + 6 bytes padding
}
func main() {
fmt.Printf("BadLayout: %d bytes\n", unsafe.Sizeof(BadLayout{})) // 32 bytes
fmt.Printf("GoodLayout: %d bytes\n", unsafe.Sizeof(GoodLayout{})) // 24 bytes
}

슬라이스 용량과 메모리 할당

슬라이스 용량을 적절히 관리하면 불필요한 재할당을 피할 수 있습니다.

package main
import (
"fmt"
)
func main() {
// 용량을 지정하지 않으면 추가할 때마다 재할당이 발생할 수 있음
s1 := []int{}
for i := 0; i < 1000; i++ {
s1 = append(s1, i)
}
// 사전에 용량을 확보하여 재할당 방지
s2 := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
s2 = append(s2, i)
}
fmt.Printf("s1 len: %d, cap: %d\n", len(s1), cap(s1))
fmt.Printf("s2 len: %d, cap: %d\n", len(s2), cap(s2))
}

맵 사전 할당

맵도 초기 용량을 지정하면 동적 확장으로 인한 비용을 줄일 수 있습니다.

package main
import (
"fmt"
)
func main() {
// 용량을 지정하지 않은 경우
m1 := make(map[int]string)
for i := 0; i < 1000; i++ {
m1[i] = fmt.Sprintf("value%d", i)
}
// 용량을 사전에 확보
m2 := make(map[int]string, 1000)
for i := 0; i < 1000; i++ {
m2[i] = fmt.Sprintf("value%d", i)
}
fmt.Printf("m1 length: %d\n", len(m1))
fmt.Printf("m2 length: %d\n", len(m2))
}

응용 아이디어 및 최적화 기법

1. 구조체 필드 배치 최적화

큰 필드를 먼저 배치하고 작은 필드를 나중에 배치하여 패딩을 최소화합니다.

// 최적화 전: 32 bytes
type Before struct {
flag1 bool // 1 + 7 padding
num1 int64 // 8
flag2 bool // 1 + 7 padding
num2 int64 // 8
}
// 최적화 후: 24 bytes
type After struct {
num1 int64 // 8
num2 int64 // 8
flag1 bool // 1
flag2 bool // 1 + 6 padding
}

2. 적절한 슬라이스 용량 설정

최종 요소 수를 예측할 수 있으면 make로 용량을 지정하여 메모리 재할당을 피합니다.

// 1000개 요소를 추가할 경우
items := make([]Item, 0, 1000)

3. 배열과 슬라이스 구분 사용

요소 수가 고정되어 변경되지 않으면 슬라이스보다 배열이 메모리 효율적입니다.

// 슬라이스: 24 bytes (헤더) + 실제 데이터
s := []int{1, 2, 3}
// 배열: 24 bytes (int × 3, 데이터만)
a := [3]int{1, 2, 3}

4. 빈 인터페이스 사용 최소화

interface{}는 타입 정보와 포인터로 16바이트를 소비하므로, 타입이 명확하면 제네릭이나 구체적 타입을 사용합니다.

// 빈 인터페이스 사용 (16 bytes)
var v interface{} = 42
// 구체적 타입 사용 (8 bytes)
var v int = 42

5. 문자열 연결에 Builder 사용

문자열은 불변이므로 + 연산자로 연결하면 매번 새 메모리 영역을 할당합니다. 대량 연결에는 strings.Builder를 사용합니다.

import "strings"
var b strings.Builder
for i := 0; i < 1000; i++ {
b.WriteString("item")
}
result := b.String()

6. sync.Pool로 객체 재사용

자주 생성/삭제되는 객체는 sync.Pool로 재사용하여 가비지 컬렉션 부하를 줄입니다.

import "sync"
var bufferPool = sync.Pool{
New: func() interface{} {
return make([]byte, 1024)
},
}
// 사용
buf := bufferPool.Get().([]byte)
defer bufferPool.Put(buf)

7. 포인터로 큰 구조체 복사 방지

큰 구조체를 함수에 전달할 때는 값 전달 대신 포인터 전달로 복사 비용을 줄입니다.

// 값 전달: 구조체 전체가 복사됨
func ProcessValue(data BigStruct) {
// ...
}
// 포인터 전달: 포인터(8 bytes)만 복사됨
func ProcessPointer(data *BigStruct) {
// ...
}

8. 메모리 프로파일링으로 문제 영역 식별

pprof를 사용하여 메모리 사용량을 시각화하고 최적화할 부분을 식별합니다.

import _ "net/http/pprof"
import "net/http"
func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// 애플리케이션 로직
}

브라우저에서 http://localhost:6060/debug/pprof/heap에 접속하거나 go tool pprof로 프로파일을 분석할 수 있습니다.

요약

  • 기본 타입은 명시적인 크기를 가지며, 문자열·슬라이스·맵·인터페이스는 포인터 기반
  • 구조체 필드 배치 순서가 패딩에 영향을 미쳐 메모리 효율성이 달라짐
  • 슬라이스와 맵에 초기 용량을 지정하면 재할당 비용을 줄일 수 있음
  • 큰 구조체는 포인터 전달하고, 자주 생성되는 객체는 sync.Pool로 재사용
  • pprof로 메모리 프로파일링을 수행하여 최적화 대상 파악

이러한 지식을 활용하면 메모리 효율적인 Go 프로그램을 작성할 수 있습니다.