logo hsb.horse
← Zur Snippets-Übersicht

Snippets

Speichergröße von Go-Datentypen

Umfassende Liste der Speichergrößen für jeden Go-Datentyp und praktische Tipps für speichereffizientes Programmieren.

Veröffentlicht: Aktualisiert:

Das Verständnis des Speicherbedarfs von Go-Datentypen ist entscheidend für das Schreiben speichereffizienter Programme. Dies wird besonders wichtig beim Umgang mit großen Datenmengen, in eingebetteten Systemen oder Hochleistungsanwendungen, wo die Wahl der Datenstrukturen direkt die Performance beeinflusst.

Speichergröße nach Datentyp

DatentypGröße (Bytes)Hinweise
int / uint8 / 88 Bytes auf 64-Bit-Systemen, 4 Bytes auf 32-Bit
int8 / uint81 / 1
int16 / uint162 / 2
int32 / uint324 / 4
int64 / uint648 / 8
float324IEEE 754 Einzelpräzisions-Gleitkommazahl
float648IEEE 754 Doppelpräzisions-Gleitkommazahl
complex648float32 × 2 (Real- und Imaginärteil)
complex12816float64 × 2 (Real- und Imaginärteil)
byte (Alias für uint8)1
rune (Alias für int32)4Unicode-Codepunkt
bool1Verwendet intern 1 Byte
string16Zeiger(8) + Länge(8), tatsächliche Daten separat gespeichert
Slice (z.B. []int)24Zeiger(8) + Länge(8) + Kapazität(8)
Map (z.B. map[string]int)8Zeiger auf interne Struktur
Channel (z.B. chan int)8Zeiger auf interne Struktur
Struct (z.B. struct{})00 ohne Felder
Interface16Typinformation(8) + Zeiger auf Wert(8)

※ Größen sind Standardwerte für 64-Bit-Systeme. Auf 32-Bit-Systemen sind Zeiger 4 Bytes groß.

Beispielimplementierungen

Größen überprüfen

Sie können unsafe.Sizeof verwenden, um die Größe von Datentypen zur Laufzeit zu überprüfen.

package main
import (
"fmt"
"unsafe"
)
func main() {
// Grundlegende Typen
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))
}

Struct-Padding überprüfen

Die Speichereffizienz von Structs variiert je nach Feldreihenfolge. Padding kann zu unerwartetem Speicherverbrauch führen.

package main
import (
"fmt"
"unsafe"
)
// Ineffizientes Layout
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
}
// Effizientes Layout
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
}

Slice-Kapazität und Speicherzuweisung

Die ordnungsgemäße Verwaltung der Slice-Kapazität vermeidet unnötige Neuzuweisungen.

package main
import (
"fmt"
)
func main() {
// Ohne angegebene Kapazität können bei jedem Append Neuzuweisungen auftreten
s1 := []int{}
for i := 0; i < 1000; i++ {
s1 = append(s1, i)
}
// Vorabzuweisung der Kapazität vermeidet Neuzuweisungen
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))
}

Maps vorab zuweisen

Maps profitieren ebenfalls von der Angabe der Anfangskapazität, wodurch die Kosten für dynamische Erweiterung reduziert werden.

package main
import (
"fmt"
)
func main() {
// Ohne angegebene Kapazität
m1 := make(map[int]string)
for i := 0; i < 1000; i++ {
m1[i] = fmt.Sprintf("value%d", i)
}
// Mit vorab zugewiesener Kapazität
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))
}

Anwendungsideen und Optimierungstechniken

1. Struct-Feldreihenfolge optimieren

Platzieren Sie größere Felder zuerst und kleinere Felder zuletzt, um Padding zu minimieren.

// Vor Optimierung: 32 bytes
type Before struct {
flag1 bool // 1 + 7 padding
num1 int64 // 8
flag2 bool // 1 + 7 padding
num2 int64 // 8
}
// Nach Optimierung: 24 bytes
type After struct {
num1 int64 // 8
num2 int64 // 8
flag1 bool // 1
flag2 bool // 1 + 6 padding
}

2. Angemessene Slice-Kapazität festlegen

Wenn die endgültige Elementanzahl vorhersehbar ist, geben Sie die Kapazität mit make an, um Speicher-Neuzuweisungen zu vermeiden.

// Beim Hinzufügen von 1000 Elementen
items := make([]Item, 0, 1000)

3. Zwischen Arrays und Slices wählen

Wenn die Elementanzahl fest ist und sich nicht ändert, sind Arrays speichereffizienter als Slices.

// Slice: 24 bytes (Header) + tatsächliche Daten
s := []int{1, 2, 3}
// Array: 24 bytes (int × 3, nur Daten)
a := [3]int{1, 2, 3}

4. Verwendung leerer Interfaces minimieren

interface{} verbraucht 16 Bytes für Typinformationen und Zeiger. Verwenden Sie Generics oder konkrete Typen, wenn der Typ bekannt ist.

// Leeres Interface verwenden (16 bytes)
var v interface{} = 42
// Konkreten Typ verwenden (8 bytes)
var v int = 42

5. Builder für String-Konkatenation verwenden

Strings sind unveränderlich, daher weist der +-Operator jedes Mal neuen Speicher zu. Verwenden Sie strings.Builder für Massenkonkatenation.

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

6. Objekte mit sync.Pool wiederverwenden

Häufig erstellte und zerstörte Objekte können mit sync.Pool wiederverwendet werden, um die Garbage-Collection-Last zu reduzieren.

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

7. Zeiger verwenden, um Kopieren großer Structs zu vermeiden

Beim Übergeben großer Structs an Funktionen verwenden Sie Zeiger statt Wertübergabe, um Kopierkosten zu reduzieren.

// Wertübergabe: gesamtes Struct wird kopiert
func ProcessValue(data BigStruct) {
// ...
}
// Zeigerübergabe: nur Zeiger (8 bytes) wird kopiert
func ProcessPointer(data *BigStruct) {
// ...
}

8. Problembereiche mit Speicherprofiling identifizieren

Verwenden Sie pprof, um die Speichernutzung zu visualisieren und Optimierungsmöglichkeiten zu identifizieren.

import _ "net/http/pprof"
import "net/http"
func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// Anwendungslogik
}

Greifen Sie auf http://localhost:6060/debug/pprof/heap in einem Browser zu oder analysieren Sie Profile mit go tool pprof.

Zusammenfassung

  • Grundtypen haben explizite Größen; Strings, Slices, Maps und Interfaces sind zeigerbasiert
  • Die Struct-Feldreihenfolge beeinflusst Padding und Speichereffizienz
  • Die Angabe der Anfangskapazität für Slices und Maps reduziert Neuzuweisungskosten
  • Übergeben Sie große Structs per Zeiger und verwenden Sie häufig erstellte Objekte mit sync.Pool wieder
  • Verwenden Sie pprof für Speicherprofiling zur Identifizierung von Optimierungsmöglichkeiten

Durch Anwendung dieses Wissens können Sie speichereffiziente Go-Programme schreiben.