navigation Navigation


Inhaltsverzeichnis

Rune


Der Datentyp rune in Go repräsentiert einen Unicode-Codepoint und wird als Alias für int32 implementiert. Er ermöglicht die Verarbeitung und Darstellung einzelner Zeichen unabhängig von deren Kodierung. rune ist besonders nützlich beim Umgang mit Unicode-Zeichenfolgen, da er eine korrekte Interpretation und Manipulation von Zeichen außerhalb des ASCII-Bereichs gewährleistet. In diesem Artikel werden die Eigenschaften, die Verwendung und typische Anwendungsfälle des rune-Typs in Go anhand von Beispielen erläutert.

Inhaltsverzeichnis

    Einführung

    Ein Rune in Go ist ein Alias für den Typ int32 und repräsentiert einen Unicode-Codepoint. Dies ist ein Konzept in Go’s Textverarbeitung, das es ermöglicht, mit internationalen Zeichen und Symbolen korrekt umzugehen.

    type rune int32

    Runes existieren, um eine wichtige Herausforderung der modernen Programmierung zu lösen: Die korrekte Verarbeitung internationaler Texte. In der frühen Computerzeit reichte ASCII (7-bit) aus, um englische Texte zu kodieren. Heute muss man jedoch Texte in allen Sprachen der Welt verarbeiten können.

    Runes werden in Go mit einfachen Anführungszeichen definiert.

    Hier ein grundlegendes Beispiel.

    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        // Verschiedene Runes definieren
        var r1 rune = 'A'
        var r2 rune = 'ä'
        var r3 rune = 'Ω'
        var r4 rune = ''
        var r5 rune = '🚀'
    
        fmt.Printf("ASCII: %c (Unicode: %U, Dezimal: %d)\n", r1, r1, r1)
        fmt.Printf("Umlaut: %c (Unicode: %U, Dezimal: %d)\n", r2, r2, r2)
        fmt.Printf("Griechisch: %c (Unicode: %U, Dezimal: %d)\n", r3, r3, r3)
        fmt.Printf("Chinesisch: %c (Unicode: %U, Dezimal: %d)\n", r4, r4, r4)
        fmt.Printf("Emoji: %c (Unicode: %U, Dezimal: %d)\n", r5, r5, r5)
    
    }
    ASCII: A (Unicode: U+0041, Dezimal: 65)
    Umlaut: ä (Unicode: U+00E4, Dezimal: 228)
    Griechisch: Ω (Unicode: U+03A9, Dezimal: 937)
    Chinesisch: 中 (Unicode: U+4E2D, Dezimal: 20013)
    Emoji: 🚀 (Unicode: U+1F680, Dezimal: 128640)

    Unicode und UTF-8 Grundlagen

    Um Runes vollständig zu verstehen, muss man die Grundlagen von Unicode und UTF-8 verstehen.

    Unicode: Das universelle Zeichensystem

    Unicode ist ein internationaler Standard, der jedem Zeichen, Symbol und Emoji eine eindeutige Nummer zuweist - den sogenannten Code Point. Diese Nummern werden normalerweise in hexadezimaler Form dargestellt (z.B. U+0041 für ‘A’).

    Beispiel
    package main
    
    import (
        "fmt"
        "unicode"
    )
    
    func main() {
        text := "Hello, 世界! 🌍"
    
        for i, r := range text {
            // Eigenschaften sammeln
            properties := []string{}
    
            if unicode.IsLetter(r) {
                properties = append(properties, "Buchstabe")
            }
            if unicode.IsDigit(r) {
                properties = append(properties, "Ziffer")
            }
            if unicode.IsSpace(r) {
                properties = append(properties, "Leerzeichen")
            }
            if unicode.IsPunct(r) {
                properties = append(properties, "Satzzeichen")
            }
            if unicode.IsSymbol(r) {
                properties = append(properties, "Symbol")
            }
            if len(properties) == 0 {
                properties = append(properties, "Sonstige")
            }
    
            fmt.Printf("\nZeichen: %c\n", r)
            fmt.Printf("Unicode: U+%04X\n", r)
            fmt.Printf("Kategorie: %s\n", getUnicodeCategory(r))
            fmt.Printf("Eigenschaften: %v\n", properties)
            fmt.Printf("Position: %d\n", i)
    
            fmt.Println("--- --- ---")
        }
    }
    
    func getUnicodeCategory(r rune) string {
        switch {
        case unicode.IsLetter(r):
            return "Buchstabe"
        case unicode.IsDigit(r):
            return "Ziffer"
        case unicode.IsSpace(r):
            return "Leerraum"
        case unicode.IsPunct(r):
            return "Zeichen"
        case unicode.IsSymbol(r):
            return "Symbol"
        default:
            return "Sonstige"
        }
    }
    Zeichen: H
    Unicode: U+0048
    Kategorie: Buchstabe
    Eigenschaften: [Buchstabe]
    Position: 0
    --- --- ---
    
    Zeichen: e
    Unicode: U+0065
    Kategorie: Buchstabe
    Eigenschaften: [Buchstabe]
    Position: 1
    --- --- ---
    
    Zeichen: l
    Unicode: U+006C
    Kategorie: Buchstabe
    Eigenschaften: [Buchstabe]
    Position: 2
    --- --- ---
    
    Zeichen: l
    Unicode: U+006C
    Kategorie: Buchstabe
    Eigenschaften: [Buchstabe]
    Position: 3
    --- --- ---
    
    Zeichen: o
    Unicode: U+006F
    Kategorie: Buchstabe
    Eigenschaften: [Buchstabe]
    Position: 4
    --- --- ---
    
    Zeichen: ,
    Unicode: U+002C
    Kategorie: Zeichen
    Eigenschaften: [Satzzeichen]
    Position: 5
    --- --- ---
    
    Zeichen:  
    Unicode: U+0020
    Kategorie: Leerraum
    Eigenschaften: [Leerzeichen]
    Position: 6
    --- --- ---
    
    Zeichen: 世
    Unicode: U+4E16
    Kategorie: Buchstabe
    Eigenschaften: [Buchstabe]
    Position: 7
    --- --- ---
    
    Zeichen: 界
    Unicode: U+754C
    Kategorie: Buchstabe
    Eigenschaften: [Buchstabe]
    Position: 10
    --- --- ---
    
    Zeichen: !
    Unicode: U+0021
    Kategorie: Zeichen
    Eigenschaften: [Satzzeichen]
    Position: 13
    --- --- ---
    
    Zeichen:  
    Unicode: U+0020
    Kategorie: Leerraum
    Eigenschaften: [Leerzeichen]
    Position: 14
    --- --- ---
    
    Zeichen: 🌍
    Unicode: U+1F30D
    Kategorie: Symbol
    Eigenschaften: [Symbol]
    Position: 15
    --- --- ---

    UTF-8 Kodierung

    UTF-8 ist eine variable Längen-Kodierung für Unicode. Das bedeutet:

    • ASCII-Zeichen (0-127) werden mit 1 Byte kodiert
    • Andere Zeichen verwenden 2-4 Bytes
    Beispiel
    package main
    
    import (
        "fmt"
        "unicode/utf8"
    )
    
    func main() {
        // Verschiedene Zeichen mit unterschiedlicher Byte-Länge
        examples := []string{"A", "ä", "中", "🚀"}
    
        for _, char := range examples {
            r, size := utf8.DecodeRuneInString(char)
    
            fmt.Printf("\nZeichen: %s\n", char)
            fmt.Printf("Unicode: U+%04X (%d)\n", r, r)
            fmt.Printf("UTF-8 Bytes: %d\n", size)
            fmt.Printf("Binär UTF-8: \n")
            for _, b := range []byte(char) {
                fmt.Printf("\t%08b \n", b)
            }
    
            fmt.Printf("Hex UTF-8: ")
            for _, b := range []byte(char) {
                fmt.Printf("%02X ", b)
            }
    
            fmt.Printf("\n" + "--- --- ---\n")
        }
    }
    
    Zeichen: A
    Unicode: U+0041 (65)
    UTF-8 Bytes: 1
    Binär UTF-8: 
        01000001 
    Hex UTF-8: 41 
    --- --- ---
    
    Zeichen: ä
    Unicode: U+00E4 (228)
    UTF-8 Bytes: 2
    Binär UTF-8: 
        11000011 
        10100100 
    Hex UTF-8: C3 A4 
    --- --- ---
    
    Zeichen: 中
    Unicode: U+4E2D (20013)
    UTF-8 Bytes: 3
    Binär UTF-8: 
        11100100 
        10111000 
        10101101 
    Hex UTF-8: E4 B8 AD 
    --- --- ---
    
    Zeichen: 🚀
    Unicode: U+1F680 (128640)
    UTF-8 Bytes: 4
    Binär UTF-8: 
        11110000 
        10011111 
        10011010 
        10000000 
    Hex UTF-8: F0 9F 9A 80 
    --- --- ---

    Rune vs. Byte - Unterschied

    Dies ist einer der wichtigsten Unterschiede, den man verstehen muss. Verwechslungen hier führen zu den häufigsten Bugs bei der Textverarbeitung.

    Byte-basierte Verarbeitung (FALSCH)

    Beispiel - Bytes
    package main
    
    import "fmt"
    
    func main() {
        text := "Hëllø, 世界!"
    
        fmt.Printf("Text: %s\n", text)
        fmt.Printf("len(text): %d (Anzahl Bytes)\n", len(text))
    
        // Falsche Iteration - über Bytes
        for i := 0; i < len(text); i++ {
            fmt.Printf("text[%d] = %c (Byte: %d, 0x%02X)\n", i, text[i], text[i], text[i])
        }
    }
    Text: Hëllø, 世界!
    len(text): 16 (Anzahl Bytes)
    text[0] = H (Byte: 72, 0x48)
    text[1] = Ã (Byte: 195, 0xC3)
    text[2] = « (Byte: 171, 0xAB)
    text[3] = l (Byte: 108, 0x6C)
    text[4] = l (Byte: 108, 0x6C)
    text[5] = Ã (Byte: 195, 0xC3)
    text[6] = ¸ (Byte: 184, 0xB8)
    text[7] = , (Byte: 44, 0x2C)
    text[8] =   (Byte: 32, 0x20)
    text[9] = ä (Byte: 228, 0xE4)
    text[10] = ¸ (Byte: 184, 0xB8)
    text[11] = – (Byte: 150, 0x96)
    text[12] = ç (Byte: 231, 0xE7)
    text[13] = • (Byte: 149, 0x95)
    text[14] = Œ (Byte: 140, 0x8C)
    text[15] = ! (Byte: 33, 0x21)

    Hier sieht man deutlich, dass der Buchstabe ‘ë’ nicht korrekt verarbeitet wird. Wir iterieren hier nicht über das eigentliche Zeichen, sondern über Teile von diesem Zeichen, da dieses Zeichen aus mehreren Bytes besteht.

    text[1] = Ã (Byte: 195, 0xC3)
    text[2] = « (Byte: 171, 0xAB)

    Rune-basierte Verarbeitung (RICHTIG)

    Beispiel - Rune
    package main
    
    import (
        "fmt"
        "unicode/utf8"
    )
    
    func main() {
        text := "Hëllø, 世界!"
    
        fmt.Printf("Text: %s\n", text)
        fmt.Printf("len(text): %d Bytes\n", len(text))
        fmt.Printf("utf8.RuneCountInString(text): %d Runes\n", utf8.RuneCountInString(text))
    
        // Richtige Iteration - über Runes
        runeIndex := 0
        for byteIndex, r := range text {
            fmt.Printf("Rune %d (Byte-Position: %d): %c (U+%04X, %d)\n", runeIndex, byteIndex, r, r, r)
            runeIndex++
        }
    
        // Alternative: Manuelle Dekodierung
        byteIndex := 0
        runeIndex = 0
        for byteIndex < len(text) {
            r, size := utf8.DecodeRuneInString(text[byteIndex:])
            fmt.Printf("Rune %d: %c (Bytes: %d-%d, Größe: %d)\n", runeIndex, r, byteIndex, byteIndex+size-1, size)
            byteIndex += size
            runeIndex++
        }
    }
    Text: Hëllø, 世界!
    len(text): 16 Bytes
    utf8.RuneCountInString(text): 10 Runes
    Rune 0 (Byte-Position: 0): H (U+0048, 72)
    Rune 1 (Byte-Position: 1): ë (U+00EB, 235)
    Rune 2 (Byte-Position: 3): l (U+006C, 108)
    Rune 3 (Byte-Position: 4): l (U+006C, 108)
    Rune 4 (Byte-Position: 5): ø (U+00F8, 248)
    Rune 5 (Byte-Position: 7): , (U+002C, 44)
    Rune 6 (Byte-Position: 8):   (U+0020, 32)
    Rune 7 (Byte-Position: 9): 世 (U+4E16, 19990)
    Rune 8 (Byte-Position: 12): 界 (U+754C, 30028)
    Rune 9 (Byte-Position: 15): ! (U+0021, 33)
    Rune 0: H (Bytes: 0-0, Größe: 1)
    Rune 1: ë (Bytes: 1-2, Größe: 2)
    Rune 2: l (Bytes: 3-3, Größe: 1)
    Rune 3: l (Bytes: 4-4, Größe: 1)
    Rune 4: ø (Bytes: 5-6, Größe: 2)
    Rune 5: , (Bytes: 7-7, Größe: 1)
    Rune 6:   (Bytes: 8-8, Größe: 1)
    Rune 7: 世 (Bytes: 9-11, Größe: 3)
    Rune 8: 界 (Bytes: 12-14, Größe: 3)
    Rune 9: ! (Bytes: 15-15, Größe: 1)

    Rune-Literale und Syntax

    Rune-Literale sind die Art, wir man Unicode-Zeichen in Go-Code definiert. Ein Rune-Literal wird immer in einfache Anführungszeichen eingeschlossen und repräsentiert genau ein Unicode-Zeichen (einen Unicode Code Point).

    Direkte Zeichen-Literale

    Die einfachste Form sind direkte Zeichen, die einfach in einfache Anführungszeichen gesetzt werden.

    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        // Direkte Zeichen-Literale
        var r1 rune = 'A'  // ASCII-Buchstabe
        var r2 rune = 'ä'  // Latin-1 Zeichen mit Umlaut
        var r3 rune = ''  // Chinesisches Zeichen (CJK)
        var r4 rune = '🚀'  // Emoji (4-Byte UTF-8)
        var r5 rune = 'Ω'  // Grichisches Omega
        var r6 rune = '5'  // Ziffer als Zeichen
        var r7 rune = ' '  // Leerzeichen
        var r8 rune = '+'  // Sonderzeichen
    
        fmt.Printf("'A' (ASCII) ==> %c = %d (0x%X)\n", r1, r1, r1)
        fmt.Printf("'ä' (Latin-1) ==> %c = %d (0x%X)\n", r2, r2, r2)
        fmt.Printf("'中' (CJK) ==> %c = %d (0x%X)\n", r3, r3, r3)
        fmt.Printf("'🚀' (Emoji) ==> %c = %d (0x%X)\n", r4, r4, r4)
        fmt.Printf("'Ω' (Greek) ==> %c = %d (0x%X)\n", r5, r5, r5)
        fmt.Printf("'5' (Ziffer) ==> %c = %d (0x%X)\n", r6, r6, r6)
        fmt.Printf("' ' (Leerzeichen) ==> %c = %d (0x%X)\n", r7, r7, r7)
        fmt.Printf("'+' (Symbol) ==> %c = %d (0x%X)\n", r8, r8, r8)
    }
    'A' (ASCII) ==> A = 65 (0x41)
    'ä' (Latin-1) ==> ä = 228 (0xE4)
    '中' (CJK) ==> 中 = 20013 (0x4E2D)
    '🚀' (Emoji) ==> 🚀 = 128640 (0x1F680)
    'Ω' (Greek) ==> Ω = 937 (0x3A9)
    '5' (Ziffer) ==> 5 = 53 (0x35)
    ' ' (Leerzeichen) ==>   = 32 (0x20)
    '+' (Symbol) ==> + = 43 (0x2B)

    Wichtiger Hinweis: '5' ist das Zeichen ‘5’ (Unicode 53), nicht die Zahl 5.


    Escape-Sequenzen für spezielle Zeichen

    Manche Zeichen können nicht direkt dargestellt werden oder haben spezielle Bedeutungen. Dafür gibt es Escape-Sequenzen mit Backslash (\).

    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        // Standard Escape-Sequenzen
        var newline rune = '\n'  // Zeilenumbruch
        var tab rune = '\t'  // Tabulator
        var carriageReturn rune = '\r'  // Wagenrücklauf
        var backslash rune = '\\'  // Backslash selbst
        var singleQuote rune = '\''  // Einfaches Anführungszeichen
        var doubleQoute rune = '"'  // Doppeltes Anführungszeichen
        var bell rune = '\a'  // Klingelzeichen
        var backspace rune = '\b'   // Rückschritt
        var formfeed rune = '\f'  // Seitenvorschub
        var vtab rune = '\v'  // Vertikaler Tabulator
    
        fmt.Printf("Neue Zeile (\\n) ==> ASCII %d | Hex 0x%02X\n", newline, newline)
        fmt.Printf("Tabulator (\\t) ==> ASCII %d | Hex 0x%02X\n", tab, tab)
        fmt.Printf("Wagenrücklauf (\\r) ==> ASCII %d | Hex 0x%02X\n", carriageReturn, carriageReturn)
        fmt.Printf("Backslash (\\\\) ==> ASCII %d | Hex 0x%02X\n", backslash, backslash)
        fmt.Printf("Einfaches ' (\\') ==> ASCII %d | Hex 0x%02X\n", singleQuote, singleQuote)
        fmt.Printf("Doppeltes \" ('\"') ==> ASCII %d | Hex 0x%02X\n", doubleQoute, doubleQoute)
        fmt.Printf("Klingelzeichen (\\a) ==> ASCII %d | Hex 0x%02X\n", bell, bell)
        fmt.Printf("Rückschritt (\\b) ==> ASCII %d | Hex 0x%02X\n", backspace, backspace)
        fmt.Printf("Seitenvorschub (\\f) ==> ASCII %d | Hex 0x%02X\n", formfeed, formfeed)
        fmt.Printf("Vert. Tabulator (\\v) ==> ASCII %d | Hex 0x%02X\n", vtab, vtab)
    }
    Neue Zeile (\n) ==> ASCII 10 | Hex 0x0A
    Tabulator (\t) ==> ASCII 9 | Hex 0x09
    Wagenrücklauf (\r) ==> ASCII 13 | Hex 0x0D
    Backslash (\\) ==> ASCII 92 | Hex 0x5C
    Einfaches ' (\') ==> ASCII 39 | Hex 0x27
    Doppeltes " ('"') ==> ASCII 34 | Hex 0x22
    Klingelzeichen (\a) ==> ASCII 7 | Hex 0x07
    Rückschritt (\b) ==> ASCII 8 | Hex 0x08
    Seitenvorschub (\f) ==> ASCII 12 | Hex 0x0C
    Vert. Tabulator (\v) ==> ASCII 11 | Hex 0x0B

    Wichtiger Unterschied

    • In Rune-Literalen: '"' (doppeltes Anführungszeichen braucht kein Escape)
    • In String-Literalen: "\"" (doppeltes Anführungszeichen braucht Escape)

    Numerische Escape-Sequenzen

    Für Zeichen, die sich nicht direkt tippen lassen, gibt es numeische Escape-Sequenzen.

    Oktal-Escapes

    Oktal-Escapes verwenden die Form \nnn wobei nnn 1-3 oktale Ziffern (0-7) sind.

    Beispiel - Oktal-Escapes
    package main
    
    import "fmt"
    
    func main() {
        // Oktal-Escapes (Basis 8)
        // Wichtig: Nur gültig für Werte 0-255 (0-\377 oktal)
        var r1 rune = '\141'  // 141 oktal = 97 dezimal = 'a'
        var r2 rune = '\101'  // 101 oktal = 65 dezimal = 'A'
        var r3 rune = '\040'  // 040 oktal = 32 dezimal = ' ' (Leerzeichen)
        var r4 rune = '\012'  // 012 oktal = 10 dezimal = '\n'
        var r5 rune = '\000'  // 000 oktal = 0 dezimal = NULL-Zeichen
        var r6 rune = '\377'  // 377 oktal = 255 dezimal = höchstes Byte-Zeichen
    
        // Verkürzte Oktal-Schreibweise (1-2 Ziffern)
        var r7 rune = '\007'  // 007 oktal = 7 dezimal = Bell
        var r8 rune = '\033'  // 033 oktal = 27 dezimal = ESC
    
        fmt.Printf("\\141 (oktal) = %d (dezimal) = '%c'\n", r1, r1)
        fmt.Printf("\\101 (oktal) = %d (dezimal) = '%c'\n", r2, r2)
        fmt.Printf("\\040 (oktal) = %d (dezimal) = '%c' (Leerzeichen)\n", r3, r3)
        fmt.Printf("\\012 (oktal) = %d (dezimal) = Zeilenumbruch\n", r4)
        fmt.Printf("\\000 (oktal) = %d (dezimal) = NULL-Zeichen\n", r5)
        fmt.Printf("\\377 (oktal) = %d (dezimal) = höchstes Byte\n", r6)
        fmt.Printf("\\007 (oktal) = %d (dezimal) = Bell\n", r7)
        fmt.Printf("\\033 (oktal) = %d (dezimal) = ESC\n", r8)
    }
    \141 (oktal) = 97 (dezimal) = 'a'
    \101 (oktal) = 65 (dezimal) = 'A'
    \040 (oktal) = 32 (dezimal) = ' ' (Leerzeichen)
    \012 (oktal) = 10 (dezimal) = Zeilenumbruch
    \000 (oktal) = 0 (dezimal) = NULL-Zeichen
    \377 (oktal) = 255 (dezimal) = höchstes Byte
    \007 (oktal) = 7 (dezimal) = Bell
    \033 (oktal) = 27 (dezimal) = ESC

    Hier ein Beispiel zur Umrechnung von oktal nach dezimal. Bei Berechnung mit oktalen Zahlen ist die Basis = 8.

    Beispiel - Umrechnung
    package main
    
    import "fmt"
    
    func main() {
        octals := []string{
            "007",
            "033",
            "040",
            "101",
            "141",
            "377",
        }
    
        for _, oct := range octals {
            fmt.Printf("\\%s oktal = ", oct)
            switch oct {
            case "007":
                fmt.Printf("%d dezimal\n", 7)
            case "033":
                fmt.Printf("%d dezimal (0*64 + 3*8 + 3 = %d)\n", 0*64+3*8+3, 0*64+3*8+3)
            case "040":
                fmt.Printf("%d dezimal (0*64 + 4*8 + 0 = %d)\n", 0*64+4*8+0, 0*64+4*8+0)
            case "101":
                fmt.Printf("%d dezimal (1*64 + 0*8 + 1 = %d)\n", 1*64+0*8+1, 1*64+0*8+1)
            case "141":
                fmt.Printf("%d dezimal (1*64 + 4*8 + 1 = %d)\n", 1*64+4*8+1, 1*64+4*8+1)
            case "377":
                fmt.Printf("%d dezimal (3*64 + 7*8 + 7 = %d)\n", 3*64+7*8+7, 3*64+7*8+7)
            }
        }
    }
    \007 oktal = 7 dezimal
    \033 oktal = 27 dezimal (0*64 + 3*8 + 3 = 27)
    \040 oktal = 32 dezimal (0*64 + 4*8 + 0 = 32)
    \101 oktal = 65 dezimal (1*64 + 0*8 + 1 = 65)
    \141 oktal = 97 dezimal (1*64 + 4*8 + 1 = 97)
    \377 oktal = 255 dezimal (3*64 + 7*8 + 7 = 255)