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 (
""), nichtnilwie 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.
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': trueInterne Struktur
Intern wird ein Go-String durch eine Datenstruktur repräsentiert, die konzeptionell folgendermaßen aussieht:
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.
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: trueSpeicher 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.
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
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.
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 🌍'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'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: 42Unverä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.
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: trueIm nächsten Beispiel wird gezeigt, wie String-Sharing funktionieren kann.
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.
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).
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.621167msNun wird im nächsten Beispiel eine effiziente Verwendungsmöglichkeit demonstriert.
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µsUTF-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.
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 BytesDas nächste Beispiel soll die Unterschiede zwischen verschiedenen Zeichen verdeutlichen.
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: 127757Im nächsten Beispiel gehen wir ein wenig auf Runes ein.
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.
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.
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
rangeSchleife iteriert über Runes (Zeichen), nicht über Bytes - Für sicheren Character-Zugriff sollte man den String zu
[]runekonvertieren
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.
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.
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.
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: falseHier 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.
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