Strings in Go sind eine grundlegende Datenstruktur, die zur Darstellung von Text verwendet wird. Sie sind unveränderlich (immutable) und bieten eine Vielzahl von Funktionen zur Manipulation und Verarbeitung von Text. In diesem Artikel werden die Grundlagen von Strings in Go behandelt, einschließlich ihrer Deklaration, Verwendung und der wichtigsten Funktionen zur String-Manipulation. Praktische Beispiele verdeutlichen, wie Strings in der Go-Programmierung effektiv eingesetzt werden können.

Einführung

In Go ist ein String ein unveränderlicher (immutable) Datentyp, der eine Sequenz von Bytes darstellt. Strings werden häufig verwendet, um Textinformationen zu speichern und zu verarbeiten. In Go sind Strings UTF-8 kodiert, was bedeutet, dass sie eine breite Palette von Zeichen aus verschiedenen Sprachen und Symbolen unterstützen.

Fundamentale Eigenschaften von Go-Strings

  • Unveränderlichkeit: Einmal erstellt, kann der Inhalt eines Strings nie verändert werden. Jede scheinbare “Änderung” erstellt einen neuen String.
  • UTF-8 Encoding: Go-Strings sind standardmäßig UTF-8 kodiert, was bedeutet, dass sie effizient internationale Zeichen und Emojis speichern können.
  • Byte-Orientierung: Intern bestehen Strings aus einer Sequenz von Bytes, nicht aus Zeichen. Die len() Funktion gibt die Anzahl der Bytes zurück, nicht die Anzahl der sichtbaren Zeichen.
  • Zero-Value: Der Zero-Value eines Strings ist ein leerer String (""), nicht nil wie bei Pointern oder Slices.
  • Vergleichbarkeit: Strings können mit ==, !=, <, >, <=, => verglichen werden. Der Vergleich erfolgt lexikografisch auf den UTF-8 Byte-Werten.

Demonstration der Eigenschaften

Das folgende Beispiel zeigt die Ausgabe von einigen Eigenschaften eines Strings in Go.

Go Beispiel - Eigenschaften
package main

import "fmt"

func main() {
    str := "Hello world"

    // Ausgabe der Eigenschaften
    fmt.Printf("String: %s\n", str)
    fmt.Printf("Länge in Bytes: %d\n", len(str))
    fmt.Printf("Typ: %T\n", str)
    fmt.Printf("Zero-Value: '%s'\n", "")

    // String-Vergleich
    strOne := "apple"
    strTwo := "banana"
    fmt.Printf("'%s' < '%s': %t\n", strOne, strTwo, strOne < strTwo)
}
Output
String: Hello world
Länge in Bytes: 11
Typ: string
Zero-Value: ''
'apple' < 'banana': true

Interne Struktur

Intern wird ein Go-String durch eine Datenstruktur repräsentiert, die konzeptionell folgendermaßen aussieht:

Go Interne Struktur
type StringHeader struct {
    Data uintptr // Pointer zu den eigentlichen Byte-Daten
    Len int // Länge der Daten in Bytes
}

Diese Struktur ist extrem effizient, da sie nur 16 Bytes (auf 64-Bit-Systemen) für den String-Header benötigt. Unabhängig von der tatsächlichen Länge des Strings. Die eigentlichen String-Daten werden separat im Heap gespeichert und sind unveränderlich.

Speicher-Layout und String-Pool

Go implementiert eine Form von String-Pooling für String-Literale. Das bedeutet, dass identische String-Literale im selben Programm den gleichen Speicherbereich teilen können. Diese Optimierung reduziert den Speicherverbrauch und verbessert die Performance bei String-Vergleichen.

Wichtige Implikationen

  • String-Literale werden zur Compile-Zeit in einem schreibgeschützten Speicherbereich abgelegt.
  • Verschiedene Variablen können auf denselben String-Datenbereich zeigen.
  • String-Slicing erstellt neue String-Header, teilt aber oft die zugrunde liegenden Daten.
Go Beispiel - Verdeutlichung
package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func main() {

    // Interne String-Struktur
    strOne := "Hello world"
    strTwo := "Hello world"

    // Beide Strings teilen sich die gleichen Daten
    // String-Pooling
    headerOne := (*reflect.StringHeader)(unsafe.Pointer(&strOne))
    headerTwo := (*reflect.StringHeader)(unsafe.Pointer(&strTwo))

    fmt.Printf("strOne Data-Pointer: %v\n", headerOne.Data)
    fmt.Printf("strTwo Data-Pointer: %v\n", headerTwo.Data)

    fmt.Printf("Gleiche Daten-Adresse: %t\n", headerOne.Data == headerTwo.Data)

    // String-Slicing teilt Daten
    subStr := strOne[0:5]
    headerSub := (*reflect.StringHeader)(unsafe.Pointer(&subStr))

    fmt.Printf("SubStr Data-Pointer: %v\n", headerSub.Data)
    fmt.Printf("SubStr teilt Daten: %t\n", headerSub.Data == headerOne.Data)

}
Output
strOne Data-Pointer: 4331272931
strTwo Data-Pointer: 4331272931
Gleiche Daten-Adresse: true
SubStr Data-Pointer: 4331272931
SubStr teilt Daten: true

Speicher Management für Strings

Da Strings unveränderlich sind, muss Go bei jeder scheinbaren “Änderung” eines Strings neuen Speicher allozieren. Dies kann bei häufigen String-Operationen zu Performance-Problemen führen, weshalb Go spezielle Tools wie strings.Builder für effiziente String-Konstruktion bereitstellt.

Speicher-Lifecycle eines Strings:

  • 1. Allozierung: Speicher wird für String-Daten reserviert
  • 2. Initialisierung: Daten werden in den Speicher geschrieben
  • 3. Nutzung: String-Header zeigt auf die Daten
  • 4. Garbage Collection: Wenn keine Referenzen mehr existieren, werden die Daten freigegeben

String-Erstellung und Deklarationsarten

Verschiedene Deklarationsmethoden

Go bietet mehrere Möglichkeiten, Strings zu erstellen und zu deklarieren. Jede Methode hat ihre spezifische Anwendungsfälle und Charakteristika.

1. Explizite Typ-Deklaration Bei der expliziten Deklaration wird der Typ string explizit angegeben. Diese Methode ist besonders nützlich, wenn der String später initialisiert werden soll oder wenn die Typ-Information für die Lesbarkeit wichtig ist.

2. Kurze Deklaration (Short Declaration) Die kurze Deklaration mit := ist die häufigste Art, Strings zu erstellen, da der Compiler den Typ automatisch ableitet.

3. Zero-Value Initialisierung Strings haben einen definierten Zero-Value (leerer String), was sie von Pointern oder Slices unterscheidet, die nil als Zero-Value haben.

Go Beispiel - Deklarationen
package main

import "fmt"

func main() {

    // 1. Explizite Deklaration
    var strOne string = "Explizite Deklaration"
    fmt.Printf("Explizit: '%s' | Typ: %T\n", strOne, strOne)

    // 2. Kurze Deklaration
    strTwo := "Kurz deklarierter String"
    fmt.Printf("Kurz: '%s' | Typ: %T\n", strTwo, strTwo)

    // 3. Zero-Value Initialisierung
    var strThree string
    fmt.Printf("Zero-Value: '%s' | Ist leer: %t | Länge: %d\n", strThree, strThree == "", len(strThree))

    // 4. Nachträgliche Zuweisung
    var strFour string
    strFour = "Später zugewiesener String"
    fmt.Printf("Später: '%s'\n", strFour)

}
Output
Explizit: 'Explizite Deklaration' | Typ: string
Kurz: 'Kurz deklarierter String' | Typ: string
Zero-Value: '' | Ist leer: true | Länge: 0
Später: 'Später zugewiesener String'

Raw-Strings

Raw Strings sind eine besondere Form der String-Deklaration in Go, die mit Backticks (`) anstelle von Anführungszeichen definiert werden. Sie haben einige wichtige Eigenschaften, die sie für bestimmte Anwendungsfälle unverzichtbar machen.

Eigenschaften von Raw-Strings:

  • 1. Keine Escape-Sequenz-Interpolation: Alle Zeichen werden literal übernommen.
  • 2. Mehrzeiligkeit: Raw Strings können sich über mehrere Zeilen erstrecken.
  • 3. Backslash-Behandlung: Backslashes werden nicht als Escape-Zeichen interpretiert.
  • 4. Anführungszeichen-Behandlung: Normale Anführungszeichen können ohne Escaping verwendet werden.

Anwendungsfälle für Raw Strings:

  • Reguläre Ausdrücke (wegen der vielen Backslashes)
  • Dateipfade (besonders Windows-Pfade)
  • JSON-Templates oder andere Formate mit Anführungszeichen
  • Mehrzeilige Template-Strings
  • SQL-Queries mit komplexer Formatierung
Go Beispiel - Backticks
package main

import (
    "fmt"
    "regexp"
)

func main() {

    // Normaler String mit Escape-Sequenzen
    normalStr := "Zeile 1\nZeile 2\tTab-getrennt\\"
    fmt.Println("Normaler String:")
    fmt.Printf("%s\n\n", normalStr)

    // Raw String - keine Interpretation
    rawStr := `Zeile 1\nZeile 2\tTab-getrennt\
    Dies ist eine neue Zeile im Raw String
    "Anführungszeichen" benötigen kein Escaping
    aber Backticks können nicht verwendet werden`
    fmt.Println("Raw Strings:")
    fmt.Printf("%s\n\n", rawStr)

    // Praktisches Beispiel - Regulärer Ausdruck
    // Ohne Raw-String müsste man \\ schreiben
    regexPattern := `\d{4}-\d{2}-\d{2}` // Datum-Pattern
    fmt.Printf("Regex-Pattern: %s\n", regexPattern)

    // Anwendung der Regex
    re := regexp.MustCompile(regexPattern)
    testDate := "Heute ist 2025-09-21"
    match := re.FindString(testDate)
    fmt.Printf("Gefundenes Datum: %s\n", match)

    // Mehrzeiliger Raw String für JSON-Template
    jsonTemplate := `{
        "name": "John Doe",
        "age": 30,
        "city": "New York",
        "hobbies": ["reading", "coding", "traveling"]
    }`
    fmt.Println("\nJSON Template:")
    fmt.Println(jsonTemplate)

}
Output
Normaler String:
Zeile 1
Zeile 2	Tab-getrennt\

Raw Strings:
Zeile 1\nZeile 2\tTab-getrennt\
    Dies ist eine neue Zeile im Raw String
    "Anführungszeichen" benötigen kein Escaping
    aber Backticks können nicht verwendet werden

Regex-Pattern: \d{4}-\d{2}-\d{2}
Gefundenes Datum: 2025-09-21

JSON Template:
{
        "name": "John Doe",
        "age": 30,
        "city": "New York",
        "hobbies": ["reading", "coding", "traveling"]
    }

String-Konvertierung von anderen Datentypen

Go bietet verschiedene Mechanismen zur Konvertierung von anderen Datentypen zu Strings. Es ist wichtig zu verstehen, dass es verschiedene Arten der Konvertierung gibt, die zu unterschiedlichen Ergebnissen führen können.

Arten der Konvertierung:

  • 1. Direkte Typ-Konvertierung mit string(): Interpretiert den Wert als Unicode-Codepoint oder Byte-Sequenz.
  • 2. Formatierte Konvertierung mit fmt.Sprintf(): Erstellt eine textuelle Repräsentation.
  • 3. Spezialisierte Konvertierung mit strconv: Bietet kontrollierte Konvertierung mit Fehlerbehandlung.
Go Beispiel - Direkte Konvertierung
package main

import (
    "fmt"
)

func main() {

    // Direkte Konvertierung von Bytes und Runes

    // Einzelnes Byte/Rune zu String
    var charCode rune = 65 // ASCII 'A'
    charStr := string(charCode)
    fmt.Printf("Rune %d zu String: '%s'\n", charCode, charStr)

    // Byte-Slice zu String
    byteSlice := []byte{'H', 'e', 'l', 'l', 'o'}
    byteStr := string(byteSlice)
    fmt.Printf("Byte-Slice %v zu String: '%s'\n", byteSlice, byteStr)

    // Rune-Slice zu String (unterstützt Unicode)
    runeSlice := []rune{'H', 'e', 'l', 'l', 'o', ' ', '🌍'}
    runeStr := string(runeSlice)
    fmt.Printf("Rune-Slice zu String: '%s'\n", runeStr)

}
Output
Rune 65 zu String: 'A'
Byte-Slice [72 101 108 108 111] zu String: 'Hello'
Rune-Slice zu String: 'Hello 🌍'
Go Beispiel - Fehler bei int
package main

import (
    "fmt"
    "strconv"
)

func main() {

    // Achtung: Häufiger Fehler bei int-Konvertierung
    num := 42
    wrongConversion := string(num)
    fmt.Printf("string(42) ergibt: '%s' (ASCII 42)\n", wrongConversion)

    // Korrekte Zahlen-zu-String Konvertierung
    correctConversion := strconv.Itoa(num)
    fmt.Printf("strconv.Itoa(42) ergibt: '%s'\n", correctConversion)

}
Output
string(42) ergibt: '*' (ASCII 42)
strconv.Itoa(42) ergibt: '42'
Go Beispiel - Formatierte Konvertierung
package main

import "fmt"

func main() {

    // Formatierte Konvertierung für komplexe Typen
    complexVar := 3.14 + 2.71i
    boolVar := true
    num := 42

    formattedStr := fmt.Sprintf("Complex: %v, Bool: %v, Number: %d", complexVar, boolVar, num)
    fmt.Printf("Formatiert: %s\n", formattedStr)

}
Output
Formatiert: Complex: (3.14+2.17i), Bool: true, Number: 42

Unveränderlichkeit (Immutability)

Konzept der Unveränderlichkeit

Die Unveränderlichkeit von Strings hat weitreichende Auswirkungen auf Performance, Speichernutzung und Programmdesign. Unveränderlichkeit bedeutet, dass einmal erstellte String-Daten niemals modifiert werden können. Jede Operation, die scheinbar einen String “ändert”, erstellt tatsächlich einen völlig neuen String.

Warum sind Strings unveränderlich?

  • 1. Thread-Sicherheit: Unveränderliche Objekte sind automatisch thread-sicher, da sie nicht verändert werden können.
  • 2. Performance bei String-Sharing: Verschiedene Variablen können sicher dieselben String-Daten teilen.
  • 3. Einfachheit: Keine komplexe Synchronisation oder Deep-Copy-Operationen nötig.
  • 4. Konsistenz: String-Hashes bleiben konstant, wichtig für Maps und Sets.

Konsequenzen der Unveränderlichkeit:

  • Jede Konkatenation erstellt einen neuen String.
  • String-Slicing kann Daten teilen, da die Original-Daten nie verändert werden.
  • Häufige String-Manipulationen können speicherintensiv sein.
  • Funktionen können Strings sicher als Werte übergeben, ohne Kopien erstellen zu müssen.
Go Beispiel - Unveränderlichkeit
package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func main() {
    original := "Hello"
    fmt.Printf("Original: '%s' (Adresse: %p)\n", original, &original)

    // Änderung erstellt einen neuen String
    modified := original + " world"
    fmt.Printf("Modifiziert: '%s' (Adresse: %p)\n", modified, &modified)
    fmt.Printf("Original unverändert: '%s'\n", original)

    // Interne Daten-Adresse
    origHeader := (*reflect.StringHeader)(unsafe.Pointer(&original))
    modHeader := (*reflect.StringHeader)(unsafe.Pointer(&modified))

    fmt.Printf("Original Daten-Adresse: %v\n", origHeader.Data)
    fmt.Printf("Modifiziert Daten-Adresse: %v\n", modHeader.Data)
    fmt.Printf("Verschiedene Daten-Bereiche: %t\n", origHeader.Data != modHeader.Data)
}
Output
Original: 'Hello' (Adresse: 0x14000090030)
Modifiziert: 'Hello world' (Adresse: 0x14000090050)
Original unverändert: 'Hello'
Original Daten-Adresse: 4332728372
Modifiziert Daten-Adresse: 1374390206466
Verschiedene Daten-Bereiche: true

Im nächsten Beispiel wird gezeigt, wie String-Sharing funktionieren kann.

Go Beispiel - String Sharing
package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func main() {

    strOne := "Shared string"
    strTwo := strOne // Sharing der Daten - keine Kopie

    fmt.Printf("strOne: '%s'\n", strOne)
    fmt.Printf("strTwo: '%s'\n", strTwo)

    // Beide teilen sich die gleichen Daten
    headerOne := (*reflect.StringHeader)(unsafe.Pointer(&strOne))
    headerTwo := (*reflect.StringHeader)(unsafe.Pointer(&strTwo))
    fmt.Printf("Gleiche Daten geteilt: %t\n", headerOne.Data == headerTwo.Data)

    // Änderung von strTwo beeinflusst nicht strOne
    strTwo += " modified"
    fmt.Printf("Nach Modifikation - strOne: '%s' | strTwo: '%s'\n", strOne, strTwo)

}
Output
strOne: 'Shared string'
strTwo: 'Shared string'
Gleiche Daten geteilt: true
Nach Modifikation - strOne: 'Shared string' | strTwo: 'Shared string modified'

Strings werden an Funktionen als Werte übergeben. Das ist dennoch effizient, da nur der Header (16 Bytes) kopiert wird, nicht die Daten.

Go Strings - Pass by value
package main

import "fmt"

func passByValue(s string) {
    s += " - modified in func"
    fmt.Printf("In Funktion: '%s'\n", s)
}

func main() {

    originalStr := "Original"
    passByValue(originalStr)
    fmt.Printf("Nach Funktionsaufruf: '%s'\n", originalStr)

}
Output
In Funktion: 'Original modified in func'
Nach Funktionsaufruf: 'Original'

Auswirkungen auf String-Operationen

Die Unveränderlichkeit hat direkte Auswirkungen auf die Performance verschiedener String-Operationen.

Effiziente Operationen:

  • String-Vergleiche (können auf Pointer-Ebene optimiert werden)
  • String-Slicing (teilt Daten mit dem Original)
  • String-Zuweisung (nur Header wird kopiert)

Teure Operationen:

  • Wiederholte Konkatenation (erstellt viele temporäre Strings)
  • Frequente “Modifikationen” (jede erstellt neue Strings)
  • Character-by-Character Manipulation

Hier ein Beispiel für eine ineffiziente Operation (Konkatenation).

Go Beispiel - Ineffiziente Konkatenation
package main

import (
    "fmt"
    "time"
)

func inefficientConcat(words []string) string {
    // Ineffizient: Jede Konkatenation erstellt neue Strings
    result := ""

    for _, word := range words {
        result += word + " " // Zwei neue Strings pro Iteration
    }

    return result
}

func main() {

    words := make([]string, 1000)
    for i := range words {
        words[i] = fmt.Sprintf("word%d", i)
    }

    start := time.Now()
    inefficientConcat(words)
    resTime := time.Since(start)

    fmt.Printf("Ineffiziente Konkatenation: %v\n", resTime)

}
Output
Ineffiziente Konkatenation: 1.621167ms

Nun wird im nächsten Beispiel eine effiziente Verwendungsmöglichkeit demonstriert.

Go Beispiel - Effiziente Konkatenation
package main

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

func efficientConcat(words []string) string {
    var builder strings.Builder
    for _, word := range words {
        builder.WriteString(word)
        builder.WriteString(" ")
    }

    return builder.String()
}

func main() {

    words := make([]string, 1000)
    for i := range words {
        words[i] = fmt.Sprintf("word%d", i)
    }

    start := time.Now()
    efficientConcat(words)
    resTime := time.Since(start)

    fmt.Printf("Effiziente Konkatenation: %v\n", resTime)

}
Output
Effiziente Konkatenation: 51.5µs

UTF-8 Encoding und Unicode-Handling

Grundlagen von UTF-8 in Go

UTF-8 ist ein variabler Längen-Encoding für Unicode-Zeichen, das von 1 bis 4 Bytes pro Zeichen verwendet. Go hat UTF-8 als Standard-String-Encoding.

Folgende Vorteile sind dabei gegeben.

  • 1. Rückwärtskompatibilität zu ASCII: ASCII-Zeichen sind identisch in UTF-8.
  • 2. Speichereffizienz: Für westliche Sprachen.
  • 3. Selbstsynchronisation: Man kann von jedem Punkt aus den nächsten Zeichen-Beginn finden.
  • 4. Byte-orientiert: Passend zu Go’s Philosophie.

UTF-8 Encoding Regeln:

  • 1 Byte: ASCII-Zeichen (0-127)
  • 2 Byte: Erweiterte lateinische, griechische und kyrillische Zeichen
  • 3 Bytes: Die meisten anderen Sprachen (CJK-Grundzeichen)
  • 4 Bytes: Emojis, seltene Zeichen, mathematische Symbole

Unterschied zwischen Bytes und Runes

In Go gibt es eine wichtige Unterscheidung zwischen Bytes (die physische Speichereinheit) und Runes (die logischen Unicode-Zeichen).

  • Byte: Eine 8-Bit Speichereinheit (uint8)
  • Rune: Ein Unicode-Codepoint (int32)

Das nachfolgende Beispiel soll dies verdeutlichen. Zuerst analysieren wir ein paar Strings.

Go Beispiel - UTF-8 Analyse
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {

    // Ein String mit verschiedenen UTF-8 Zeichen
    text := "Hello 🌍 Café"

    fmt.Printf("String: %s\n", text)
    fmt.Printf("Länge in Bytes: %d\n", len(text))
    fmt.Printf("Anzahl Runes (Zeichen): %d\n", utf8.RuneCountInString(text))
    fmt.Printf("Gültiger UTF-8: %t\n", utf8.ValidString(text))

    // Byte-Analyse
    fmt.Printf("\n=== Byte analyzing ===\n")
    for i := 0; i < len(text); i++ {
        b := text[i]
        fmt.Printf("Byte %d: %d (0x%02x) '%c'\n", i, b, b, b)
    }

    // Rune-Analyse
    fmt.Printf("\n=== Rune analyzing ===\n")
    for i, r := range text {
        fmt.Printf("Byte-Index %d: Rune %U ('%c') - %d Bytes\n", i, r, r, utf8.RuneLen(r))
    }

}
Output
String: Hello 🌍 Café
Länge in Bytes: 16
Anzahl Runes (Zeichen): 12
Gültiger UTF-8: true

=== Byte analyzing ===
Byte 0: 72 (0x48) 'H'
Byte 1: 101 (0x65) 'e'
Byte 2: 108 (0x6c) 'l'
Byte 3: 108 (0x6c) 'l'
Byte 4: 111 (0x6f) 'o'
Byte 5: 32 (0x20) ' '
Byte 6: 240 (0xf0) 'ð'
Byte 7: 159 (0x9f) 'Ÿ'
Byte 8: 140 (0x8c) 'Œ'
Byte 9: 141 (0x8d) ''
Byte 10: 32 (0x20) ' '
Byte 11: 67 (0x43) 'C'
Byte 12: 97 (0x61) 'a'
Byte 13: 102 (0x66) 'f'
Byte 14: 195 (0xc3) 'Ã'
Byte 15: 169 (0xa9) '©'

=== Rune analyzing ===
Byte-Index 0: Rune U+0048 ('H') - 1 Bytes
Byte-Index 1: Rune U+0065 ('e') - 1 Bytes
Byte-Index 2: Rune U+006C ('l') - 1 Bytes
Byte-Index 3: Rune U+006C ('l') - 1 Bytes
Byte-Index 4: Rune U+006F ('o') - 1 Bytes
Byte-Index 5: Rune U+0020 (' ') - 1 Bytes
Byte-Index 6: Rune U+1F30D ('🌍') - 4 Bytes
Byte-Index 10: Rune U+0020 (' ') - 1 Bytes
Byte-Index 11: Rune U+0043 ('C') - 1 Bytes
Byte-Index 12: Rune U+0061 ('a') - 1 Bytes
Byte-Index 13: Rune U+0066 ('f') - 1 Bytes
Byte-Index 14: Rune U+00E9 ('é') - 2 Bytes

Das nächste Beispiel soll die Unterschiede zwischen verschiedenen Zeichen verdeutlichen.

Go Beispiel - Unterschiede Byte-Längen
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    
    examples := []string{
        "A",  // 1 Byte (ASCII)
        "ä",  // 2 Bytes (Erweiterter lateinischer Satz)
        "中", // 3 Bytes (CJK-Zeichen)
        "🌍", // 4 Bytes (Emoji)
    }

    for _, char := range examples {
        bytes := []byte(char)
        runes := []rune(char)

        fmt.Printf("Zeichen: %s\n", char)
        fmt.Printf("\tUTF-8 Bytes: %v\n", bytes)
        fmt.Printf("\tAnzahl Bytes: %d\n", len(bytes))
        fmt.Printf("\tUnicode Codepoint: U+%04X\n", runes[0])
        fmt.Printf("\tRune-Wert: %d\n", runes[0])
        fmt.Println()
    }

}
Output
Zeichen: A
    UTF-8 Bytes: [65]
    Anzahl Bytes: 1
    Unicode Codepoint: U+0041
    Rune-Wert: 65

Zeichen: ä
    UTF-8 Bytes: [195 164]
    Anzahl Bytes: 2
    Unicode Codepoint: U+00E4
    Rune-Wert: 228

Zeichen: 中
    UTF-8 Bytes: [228 184 173]
    Anzahl Bytes: 3
    Unicode Codepoint: U+4E2D
    Rune-Wert: 20013

Zeichen: 🌍
    UTF-8 Bytes: [240 159 140 141]
    Anzahl Bytes: 4
    Unicode Codepoint: U+1F30D
    Rune-Wert: 127757

Im nächsten Beispiel gehen wir ein wenig auf Runes ein.

Go Beispiel - Runes
package main

import "fmt"

func main() {

    text := "Hëllö Wörld 🌍"

    // Konvertierung zu Rune-Slice für sicheren Zugriff
    runes := []rune(text)

    fmt.Printf("Original: %s\n", text)
    fmt.Printf("Als Runes: %v\n", runes)

    // Sicherer Zugriff auf einzelne Zeichen
    fmt.Printf("Erstes Zeichen: %c\n", runes[0])
    fmt.Printf("Letztes Zeichen: %c\n", runes[len(runes)-1])

    // Manipulation auf Rune-Ebene
    runes[1] = 'e' // ë -> e
    runes[4] = 'o' // ö -> o
    runes[7] = 'o' // ö -> o

    modified := string(runes)
    fmt.Printf("Modifiziert: %s\n", modified)

}
Output
Original: Hëllö Wörld 🌍
Als Runes: [72 235 108 108 246 32 87 246 114 108 100 32 127757]
Erstes Zeichen: H
Letztes Zeichen: 🌍
Modifiziert: Hello World 🌍

Unicode-Paket und erweiterte Funktionalität

Go bietet das unicode Paket für erweiterte Unicode-Operationen. Dieses Package enthält Funktionen zur Klassifizierung von Unicode-Zeichen und zur Durchführung von Unicode-bewussten Operationen.

Hier ein Beispiel mit Verwendung von unicode Paket.

Go Beispiel - Unicode-Paket
package main

import (
    "fmt"
    "unicode"
    "unicode/utf8"
)

func main() {

    text := "Hello123 Wörld! 🌍αβγ"

    fmt.Printf("Text: %s\n\n", text)

    for i, r := range text {
        fmt.Printf("Position %d: '%c' (U+%04X)\n", i, r, r)
        fmt.Printf("\tIst Buchstabe: %t\n", unicode.IsLetter(r))
        fmt.Printf("\tIst Ziffer: %t\n", unicode.IsDigit(r))
        fmt.Printf("\tIst Leerzeichen: %t\n", unicode.IsSpace(r))
        fmt.Printf("\tIst Satzzeichen: %t\n", unicode.IsPunct(r))
        fmt.Printf("\tIst Symbol: %t\n", unicode.IsSymbol(r))
        fmt.Printf("\tIst Großbuchstabe: %t\n", unicode.IsUpper(r))
        fmt.Printf("\tIst Kleinbuchstabe: %t\n", unicode.IsLower(r))
        fmt.Printf("\tZu Großbuchstabe: '%c'\n", unicode.ToUpper(r))
        fmt.Printf("\tZu Kleinbuchstabe: '%c'\n", unicode.ToLower(r))
    }

}
Output
Text: Hello123 Wörld! 🌍αβγ

Position 0: 'H' (U+0048)
    Ist Buchstabe: true
    Ist Ziffer: false
    Ist Leerzeichen: false
    Ist Satzzeichen: false
    Ist Symbol: false
    Ist Großbuchstabe: true
    Ist Kleinbuchstabe: false
    Zu Großbuchstabe: 'H'
    Zu Kleinbuchstabe: 'h'
Position 1: 'e' (U+0065)
    Ist Buchstabe: true
    Ist Ziffer: false
    Ist Leerzeichen: false
    Ist Satzzeichen: false
    Ist Symbol: false
    Ist Großbuchstabe: false
    Ist Kleinbuchstabe: true
    Zu Großbuchstabe: 'E'
    Zu Kleinbuchstabe: 'e'
Position 2: 'l' (U+006C)
    Ist Buchstabe: true
    Ist Ziffer: false
    Ist Leerzeichen: false
    Ist Satzzeichen: false
    Ist Symbol: false
    Ist Großbuchstabe: false
    Ist Kleinbuchstabe: true
    Zu Großbuchstabe: 'L'
    Zu Kleinbuchstabe: 'l'
Position 3: 'l' (U+006C)
    Ist Buchstabe: true
    Ist Ziffer: false
    Ist Leerzeichen: false
    Ist Satzzeichen: false
    Ist Symbol: false
    Ist Großbuchstabe: false
    Ist Kleinbuchstabe: true
    Zu Großbuchstabe: 'L'
    Zu Kleinbuchstabe: 'l'
Position 4: 'o' (U+006F)
    Ist Buchstabe: true
    Ist Ziffer: false
    Ist Leerzeichen: false
    Ist Satzzeichen: false
    Ist Symbol: false
    Ist Großbuchstabe: false
    Ist Kleinbuchstabe: true
    Zu Großbuchstabe: 'O'
    Zu Kleinbuchstabe: 'o'
Position 5: '1' (U+0031)
    Ist Buchstabe: false
    Ist Ziffer: true
    Ist Leerzeichen: false
    Ist Satzzeichen: false
    Ist Symbol: false
    Ist Großbuchstabe: false
    Ist Kleinbuchstabe: false
    Zu Großbuchstabe: '1'
    Zu Kleinbuchstabe: '1'
Position 6: '2' (U+0032)
    Ist Buchstabe: false
    Ist Ziffer: true
    Ist Leerzeichen: false
    Ist Satzzeichen: false
    Ist Symbol: false
    Ist Großbuchstabe: false
    Ist Kleinbuchstabe: false
    Zu Großbuchstabe: '2'
    Zu Kleinbuchstabe: '2'
Position 7: '3' (U+0033)
    Ist Buchstabe: false
    Ist Ziffer: true
    Ist Leerzeichen: false
    Ist Satzzeichen: false
    Ist Symbol: false
    Ist Großbuchstabe: false
    Ist Kleinbuchstabe: false
    Zu Großbuchstabe: '3'
    Zu Kleinbuchstabe: '3'
Position 8: ' ' (U+0020)
    Ist Buchstabe: false
    Ist Ziffer: false
    Ist Leerzeichen: true
    Ist Satzzeichen: false
    Ist Symbol: false
    Ist Großbuchstabe: false
    Ist Kleinbuchstabe: false
    Zu Großbuchstabe: ' '
    Zu Kleinbuchstabe: ' '
Position 9: 'W' (U+0057)
    Ist Buchstabe: true
    Ist Ziffer: false
    Ist Leerzeichen: false
    Ist Satzzeichen: false
    Ist Symbol: false
    Ist Großbuchstabe: true
    Ist Kleinbuchstabe: false
    Zu Großbuchstabe: 'W'
    Zu Kleinbuchstabe: 'w'
Position 10: 'ö' (U+00F6)
    Ist Buchstabe: true
    Ist Ziffer: false
    Ist Leerzeichen: false
    Ist Satzzeichen: false
    Ist Symbol: false
    Ist Großbuchstabe: false
    Ist Kleinbuchstabe: true
    Zu Großbuchstabe: 'Ö'
    Zu Kleinbuchstabe: 'ö'
Position 12: 'r' (U+0072)
    Ist Buchstabe: true
    Ist Ziffer: false
    Ist Leerzeichen: false
    Ist Satzzeichen: false
    Ist Symbol: false
    Ist Großbuchstabe: false
    Ist Kleinbuchstabe: true
    Zu Großbuchstabe: 'R'
    Zu Kleinbuchstabe: 'r'
Position 13: 'l' (U+006C)
    Ist Buchstabe: true
    Ist Ziffer: false
    Ist Leerzeichen: false
    Ist Satzzeichen: false
    Ist Symbol: false
    Ist Großbuchstabe: false
    Ist Kleinbuchstabe: true
    Zu Großbuchstabe: 'L'
    Zu Kleinbuchstabe: 'l'
Position 14: 'd' (U+0064)
    Ist Buchstabe: true
    Ist Ziffer: false
    Ist Leerzeichen: false
    Ist Satzzeichen: false
    Ist Symbol: false
    Ist Großbuchstabe: false
    Ist Kleinbuchstabe: true
    Zu Großbuchstabe: 'D'
    Zu Kleinbuchstabe: 'd'
Position 15: '!' (U+0021)
    Ist Buchstabe: false
    Ist Ziffer: false
    Ist Leerzeichen: false
    Ist Satzzeichen: true
    Ist Symbol: false
    Ist Großbuchstabe: false
    Ist Kleinbuchstabe: false
    Zu Großbuchstabe: '!'
    Zu Kleinbuchstabe: '!'
Position 16: ' ' (U+0020)
    Ist Buchstabe: false
    Ist Ziffer: false
    Ist Leerzeichen: true
    Ist Satzzeichen: false
    Ist Symbol: false
    Ist Großbuchstabe: false
    Ist Kleinbuchstabe: false
    Zu Großbuchstabe: ' '
    Zu Kleinbuchstabe: ' '
Position 17: '🌍' (U+1F30D)
    Ist Buchstabe: false
    Ist Ziffer: false
    Ist Leerzeichen: false
    Ist Satzzeichen: false
    Ist Symbol: true
    Ist Großbuchstabe: false
    Ist Kleinbuchstabe: false
    Zu Großbuchstabe: '🌍'
    Zu Kleinbuchstabe: '🌍'
Position 21: 'α' (U+03B1)
    Ist Buchstabe: true
    Ist Ziffer: false
    Ist Leerzeichen: false
    Ist Satzzeichen: false
    Ist Symbol: false
    Ist Großbuchstabe: false
    Ist Kleinbuchstabe: true
    Zu Großbuchstabe: 'Α'
    Zu Kleinbuchstabe: 'α'
Position 23: 'β' (U+03B2)
    Ist Buchstabe: true
    Ist Ziffer: false
    Ist Leerzeichen: false
    Ist Satzzeichen: false
    Ist Symbol: false
    Ist Großbuchstabe: false
    Ist Kleinbuchstabe: true
    Zu Großbuchstabe: 'Β'
    Zu Kleinbuchstabe: 'β'
Position 25: 'γ' (U+03B3)
    Ist Buchstabe: true
    Ist Ziffer: false
    Ist Leerzeichen: false
    Ist Satzzeichen: false
    Ist Symbol: false
    Ist Großbuchstabe: false
    Ist Kleinbuchstabe: true
    Zu Großbuchstabe: 'Γ'
    Zu Kleinbuchstabe: 'γ'

In diesem Beispiel betrachten wir ein paar erweiterte UTF-8 Operationen.

Go Beispiel - Erweiterte UTF-8 Operationen
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {

    text := "🌍🌎🌏"

    fmt.Printf("Emoji-String: %s\n", text)
    fmt.Printf("Byte-Länge: %d\n", len(text))
    fmt.Printf("Rune-Anzahl: %d\n", utf8.RuneCountInString(text))

    // Iteration mit expliziter UTF8-Dekodierung
    fmt.Println("\nExplizite UTF-8 Dekodierung")
    for i := 0; i < len(text); {
        r, size := utf8.DecodeRuneInString(text[i:])
        fmt.Printf("Byte-Position: %d: Rune '%c' (%d Bytes)\n", i, r, size)
        i += size
    }

    // Umkehr-Iteration
    fmt.Println("\nRückwärts-Iteration")
    for i := len(text); i > 0; {
        r, size := utf8.DecodeLastRuneInString(text[:i])
        i -= size
        fmt.Printf("Byte-Position: %d: Rune '%c' (%d Bytes)\n", i, r, size)
    }
    
}
Output
Emoji-String: 🌍🌎🌏
Byte-Länge: 12
Rune-Anzahl: 3

Explizite UTF-8 Dekodierung
Byte-Position: 0: Rune '🌍' (4 Bytes)
Byte-Position: 4: Rune '🌎' (4 Bytes)
Byte-Position: 8: Rune '🌏' (4 Bytes)

Rückwärts-Iteration
Byte-Position: 8: Rune '🌏' (4 Bytes)
Byte-Position: 4: Rune '🌎' (4 Bytes)
Byte-Position: 0: Rune '🌍' (4 Bytes)

String-Indexierung und Slicing

Byte-Level Indexierung vs. Character-Level Zugriff

String-Indexierung in Go erfolgt immer auf Byte-Ebene, nicht auf Character-Ebene. Dies ist ein fundamentaler Unterschied zu vielen anderen Programmiersprachen und kann zu unerwarteten Ergebnissen führen, wenn man mit Nicht-ASCII-Zeichen arbeitet.

Wichtige Konzepte:

  • str[i] gibt das i-te Byte zurück, nicht das i-te Zeichen
  • Bei Multi-Byte UTF-8 Zeichen kann str[i] ein ungültiges Zeichen-Fragment zurückgeben
  • Die range Schleife iteriert über Runes (Zeichen), nicht über Bytes
  • Für sicheren Character-Zugriff sollte man den String zu []rune konvertieren

Im folgenden Beispiel werden die möglichen Fallen beim Zugriff auf einzelne String-Elemente gezeigt. Die Ausgabe bei diesem Beispiel zeigt, dass auch Nicht-ASCII-Bytes in einem String vorkommen können — und dass ein einzelner Index dann nur ein Bruchstück eines Zeichens liefert.

Go Beispiel - Fallen
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    text := "Café"

    fmt.Printf("String: %s\n", text)
    fmt.Printf("len(): %d (Bytes)\n", len(text))
    fmt.Printf("RuneCount: %d (Zeichen)\n", utf8.RuneCountInString(text))

    fmt.Println("\nByte-Indexierung (Gefährlich bei UTF-8)")
    for i := 0; i < len(text); i++ {
        b := text[i]
        fmt.Printf("text[%d] = %d (0x%02X)", i, b, b)

        if b < 128 {
            fmt.Printf(" = '%c'\n", b)
        } else {
            fmt.Printf(" = <Nicht-ASCII-Byte>\n")
        }
    }
}
Output
String: Café
len(): 5 (Bytes)
RuneCount: 4 (Zeichen)

Byte-Indexierung (Gefährlich bei UTF-8)
text[0] = 67 (0x43) = 'C'
text[1] = 97 (0x61) = 'a'
text[2] = 102 (0x66) = 'f'
text[3] = 195 (0xC3) = <Nicht-ASCII-Byte>
text[4] = 169 (0xA9) = <Nicht-ASCII-Byte>

Hier ein Beispiel für eine sichere Rune-Iteration.

Go Beispiel - Sichere Iteration
package main

import (
    "fmt"
)

func main() {
    text := "Café"

    // Sichere Iteration
    for i, r := range text {
        fmt.Printf("Byte-Index: %d | Rune: '%c' (U+%04X)\n", i, r, r)
    }

    // Sicherer Zugriff
    runes := []rune(text)
    for i, r := range runes {
        fmt.Printf("Zeichen %d: '%c'\n", i, r)
    }
}
Output
Byte-Index: 0 | Rune: 'C' (U+0043)
Byte-Index: 1 | Rune: 'a' (U+0061)
Byte-Index: 2 | Rune: 'f' (U+0066)
Byte-Index: 3 | Rune: 'é' (U+00E9)
Zeichen 0: 'C'
Zeichen 1: 'a'
Zeichen 2: 'f'
Zeichen 3: 'é'

Man soll also auf einzelne Zeichen in einem String in Go mittels Runes zugreifen. Wenn man versucht über Bytes den Zugriff durchzuführen, kann es vorkommen, dass Bytes gebrochen werden, da manche Zeichen mehr als 1 Byte für die Darstellung benötigen.

In den nächsten zwei Beispielen werden unsichere und sichere Methoden für Slicing eines Strings gezeigt. Die Gründe sind die gleichen. Byte-Abfolge kann unter Umständen gebrochen werden.

Zuerst schauen wir uns an, wie ein unsicheres Slicing aussehen kann.

Go Beispiel - Unsicheres Slicing
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {

    text := "Hello 🌍 World"

    fmt.Printf("Original: %s\n", text)

    // Gefährlich: Byte-Slicing kann UTF-8 Zeichen zerbrechen
    fmt.Println("\nByte-Slicing (kann gefährlich sein)")
    unsafeSlice := text[6:9]

    fmt.Printf("text[6:9]: %s (möglicherweise ungültig)\n", unsafeSlice)
    fmt.Printf("Gültiger UTF-8: %t\n", utf8.ValidString(unsafeSlice))

}
Output
Original: Hello 🌍 World

Byte-Slicing (kann gefährlich sein)
text[6:9]: ? (möglicherweise ungültig)
Gültiger UTF-8: false

Hier sieht man, dass wir kein gültiges Emoji erhalten und auch die Prüfung der UTF-8-Gültigkeit mit utf8.ValidString() schlägt fehl. Wenn man text[6:10] verwenden würde, würde man eine ausreichende Anzahl an Bytes einschließen, um das Emoji vollständig zu erhalten.

Eine sichere Variante beinhaltet die Verwendung von Runes.

Go Beispiel - Sicheres Slicing
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    text := "Hello 🌍 World"

    fmt.Println("\nSicheres Rune-Slicing")

    runes := []rune(text)
    safeSlice := string(runes[6:8])

    fmt.Printf("Rune-Slice [6:8]: %s\n", safeSlice)
    fmt.Printf("Gültiger UTF-8: %t\n", utf8.ValidString(safeSlice))
}
Output
Sicheres Rune-Slicing
Rune-Slice [6:8]: 🌍 
Gültiger UTF-8: true

Häufige Stolperfallen

len(s) zählt Bytes, nicht Zeichen.

Das ist der Klassiker. len("Café") ergibt 5, nicht 4. Für die Anzahl sichtbarer Zeichen brauchst du utf8.RuneCountInString(s) aus dem Paket unicode/utf8 — und auch das stimmt nur, solange du Codepoints meinst (Grapheme-Cluster wie e + ́ zählen anders).

s[i] liefert ein Byte, kein Zeichen.

Bei reinem ASCII funktioniert das wie erwartet. Sobald ein Mehrbyte-Zeichen im String steht, bekommst du ein einzelnes Byte aus der UTF-8-Sequenz zurück — keinen sichtbaren Buchstaben. Sicher iterieren: for i, r := range s liefert Rune-Werte.

string(num) ist fast nie das, was du willst.

string(42) ergibt nicht "42", sondern den Unicode-Codepoint U+002A — also "*". Für Zahlen-zu-String nimm strconv.Itoa(n) oder fmt.Sprintf("%d", n). Der Compiler warnt seit Go 1.15 bei string(int), kompiliert es aber weiterhin.

Konkatenation in Schleifen ist O(n²).

Jedes result += word allokiert einen neuen String und kopiert die alten Daten. Bei tausenden Iterationen wird das richtig teuer. Lösung: strings.Builder mit WriteString — eine einzige wachsende Allokation, am Ende .String().

reflect.StringHeader ist seit Go 1.20 deprecated.

Die obigen Beispiele zur internen Struktur funktionieren weiterhin, sind aber als Lehrbeispiele zu verstehen. Modern: unsafe.StringData(s) und unsafe.String(ptr, len) aus dem unsafe-Paket.

Slicing über UTF-8-Grenzen erzeugt ungültige Strings.

text[6:9] über einem Emoji liefert eine kaputte Byte-Sequenz. utf8.ValidString deckt es auf, verhindert es aber nicht. Wenn du zeichenweise schneiden willst: erst []rune konvertieren, dann slicen, dann zurück zum String.

Vergleich mit == ist byteweise, nicht Unicode-normalisiert.

"é" aus einem Codepoint und "é" aus e + ́ (zwei Codepoints) sind beide gültiges UTF-8, aber == sagt false. Für menschliche Gleichheit brauchst du Unicode-Normalisierung über golang.org/x/text/unicode/norm. strings.EqualFold deckt nur Groß-/Kleinschreibung ab, nicht Normalisierung.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Datentypen

Zur Übersicht