navigation Navigation


Inhaltsverzeichnis

String


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.

Inhaltsverzeichnis

    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.

    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)
    }
    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:

    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.
    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)
    
    }
    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 Deklarationi (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.

    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)
    
    }
    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
    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)
    
    }
    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.
    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)
    
    }
    Rune 65 zu String: 'A'
    Byte-Slice [72 101 108 108 111] zu String: 'Hello'
    Rune-Slice zu String: 'Hello 🌍'
    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)
    
    }
    string(42) ergibt: '*' (ASCII 42)
    strconv.Itoa(42) ergibt: '42'
    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)
    
    }
    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.
    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)
    }
    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.

    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)
    
    }
    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.

    Strings - Pass by value
    package main
    
    import "fmt"
    
    func passByValue(s string) {
        s += " - modified in func"
        fmt.Printf("In Funkion: '%s'\n", s)
    }
    
    func main() {
    
        originalStr := "Original"
        passByValue(originalStr)
        fmt.Printf("Nach Funktionsaufruf: '%s'\n", originalStr)
    
    }
    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).

    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)
    
    }
    Ineffiziente Konkatenation: 1.621167ms

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

    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)
    
    }
    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.

    UFT-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.

    Beispiel - UFT-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))
        }
    
    }
    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.

    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()
        }
    
    }
    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.

    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)
    
    }
    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.

    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))
        }
    
    }
    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.

    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)
        }
        
    }
    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 ist 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 es auch keine ASCII-Bytes in einem String vorkommen können.

    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")
            }
        }
    }
    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.

    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)
        }
    }
    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.

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

    Beispiel - Unsicheres Slicing
    package main
    
    import (
        "fmt"
        "unicode/utf8"
    )
    
    func main() {
    
        text := "Hello 🌍 World"
    
        fmt.Printf("Origina: %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))
    
    }
    Origina: 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.

    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))
    }
    Sicheres Rune-Slicing
    Rune-Slice [6:8]: 🌍 
    Gültiger UTF-8: true