Understanding the memory footprint of Go’s data types is crucial for writing memory-efficient programs. This becomes especially important when dealing with large datasets, embedded systems, or high-performance applications where data structure choices directly impact performance.
Memory Size by Data Type
| Data Type | Size (Bytes) | Notes |
|---|---|---|
| int / uint | 8 / 8 | 8 bytes on 64-bit systems, 4 bytes on 32-bit |
| int8 / uint8 | 1 / 1 | |
| int16 / uint16 | 2 / 2 | |
| int32 / uint32 | 4 / 4 | |
| int64 / uint64 | 8 / 8 | |
| float32 | 4 | IEEE 754 single-precision float |
| float64 | 8 | IEEE 754 double-precision float |
| complex64 | 8 | float32 × 2 (real and imaginary parts) |
| complex128 | 16 | float64 × 2 (real and imaginary parts) |
| byte (alias for uint8) | 1 | |
| rune (alias for int32) | 4 | Unicode code point |
| bool | 1 | Internally uses 1 byte |
| string | 16 | Pointer(8) + length(8), actual data stored separately |
| Slice (e.g., []int) | 24 | Pointer(8) + length(8) + capacity(8) |
| Map (e.g., map[string]int) | 8 | Pointer to internal structure |
| Channel (e.g., chan int) | 8 | Pointer to internal structure |
| Struct (e.g., struct{}) | 0 | 0 when no fields |
| Interface | 16 | Type info(8) + pointer to value(8) |
※ Sizes are standard values for 64-bit systems. On 32-bit systems, pointer sizes are 4 bytes.
Sample Implementations
Checking Sizes
You can use unsafe.Sizeof to check the size of data types at runtime.
package main
import ( "fmt" "unsafe")
func main() { // Basic types 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))
// String, slice, map 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))
// Interface var iface interface{} fmt.Printf("interface{}: %d bytes\n", unsafe.Sizeof(iface))}Checking Struct Padding
Struct memory efficiency varies based on field ordering. Padding can cause unexpected memory consumption.
package main
import ( "fmt" "unsafe")
// Inefficient layouttype BadLayout struct { a bool // 1 byte + 7 bytes padding b int64 // 8 bytes c bool // 1 byte + 7 bytes padding d int64 // 8 bytes}
// Efficient layouttype 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}Slice Capacity and Memory Allocation
Managing slice capacity properly avoids unnecessary reallocations.
package main
import ( "fmt")
func main() { // Without capacity specified, reallocations may occur with each append s1 := []int{} for i := 0; i < 1000; i++ { s1 = append(s1, i) }
// Pre-allocating capacity avoids reallocations 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))}Pre-allocating Maps
Maps also benefit from initial capacity specification, reducing dynamic expansion costs.
package main
import ( "fmt")
func main() { // Without capacity specified m1 := make(map[int]string) for i := 0; i < 1000; i++ { m1[i] = fmt.Sprintf("value%d", i) }
// With pre-allocated capacity 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))}Application Ideas and Optimization Techniques
1. Optimize Struct Field Ordering
Place larger fields first and smaller fields last to minimize padding.
// Before optimization: 32 bytestype Before struct { flag1 bool // 1 + 7 padding num1 int64 // 8 flag2 bool // 1 + 7 padding num2 int64 // 8}
// After optimization: 24 bytestype After struct { num1 int64 // 8 num2 int64 // 8 flag1 bool // 1 flag2 bool // 1 + 6 padding}2. Set Appropriate Slice Capacity
When the final element count is predictable, specify capacity with make to avoid memory reallocations.
// When adding 1000 elementsitems := make([]Item, 0, 1000)3. Choose Between Arrays and Slices
When element count is fixed and won’t change, arrays are more memory-efficient than slices.
// Slice: 24 bytes (header) + actual datas := []int{1, 2, 3}
// Array: 24 bytes (int × 3, data only)a := [3]int{1, 2, 3}4. Minimize Empty Interface Usage
interface{} consumes 16 bytes for type information and pointer. Use generics or concrete types when type is known.
// Using empty interface (16 bytes)var v interface{} = 42
// Using concrete type (8 bytes)var v int = 425. Use Builder for String Concatenation
Strings are immutable, so the + operator allocates new memory each time. Use strings.Builder for bulk concatenation.
import "strings"
var b strings.Builderfor i := 0; i < 1000; i++ { b.WriteString("item")}result := b.String()6. Reuse Objects with sync.Pool
Frequently created and destroyed objects can be reused with sync.Pool to reduce garbage collection pressure.
import "sync"
var bufferPool = sync.Pool{ New: func() interface{} { return make([]byte, 1024) },}
// Usagebuf := bufferPool.Get().([]byte)defer bufferPool.Put(buf)7. Use Pointers to Avoid Copying Large Structs
When passing large structs to functions, use pointers instead of value passing to reduce copy costs.
// Value passing: entire struct is copiedfunc ProcessValue(data BigStruct) { // ...}
// Pointer passing: only pointer (8 bytes) is copiedfunc ProcessPointer(data *BigStruct) { // ...}8. Identify Problem Areas with Memory Profiling
Use pprof to visualize memory usage and identify areas for optimization.
import _ "net/http/pprof"import "net/http"
func main() { go func() { http.ListenAndServe("localhost:6060", nil) }() // Application logic}Access http://localhost:6060/debug/pprof/heap in a browser or analyze profiles with go tool pprof.
Summary
- Basic types have explicit sizes; strings, slices, maps, and interfaces are pointer-based
- Struct field ordering affects padding and memory efficiency
- Specifying initial capacity for slices and maps reduces reallocation costs
- Pass large structs by pointer and reuse frequently created objects with
sync.Pool - Use
pproffor memory profiling to identify optimization opportunities
By applying this knowledge, you can write memory-efficient Go programs.
hsb.horse