Menu

Rune i byte w Golang: stringi UTF-8 wyjaśnione

Co oznaczają rune i byte w Go, jak stringi przechowują UTF-8, dlaczego len liczy bajty, jak range dekoduje rune, konwersje między string, []byte i []rune oraz praca z pakietem bytes.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

String w Go to ciąg bajtów, a kod źródłowy Go i literały stringów są w UTF-8. byte to jeden z tych bajtów. rune to jeden znak Unicode (punkt kodowy), który UTF-8 zapisuje na 1 do 4 bajtach. Większość nieporozumień ze stringami w Go bierze się z mylenia tych dwóch pojęć:

語 zajmuje w UTF-8 trzy bajty, więc "語Go" ma 5 bajtów, ale 3 rune. Pętla po indeksach widzi pięć bajtów, a bajty 0, 1 i 2 same w sobie nic nie znaczą. Pętla range dekoduje UTF-8 i zwraca trzy rune pod indeksami 0, 3 i 4: i to zawsze przesunięcie w bajtach, a nie numer znaku.

byte i rune to aliasy

NazwaTen sam typ coLiterałZnaczenie
byteuint8byte('A'), 0x41jeden bajt danych
runeint32'A', 'é', '世'jeden punkt kodowy Unicode

Ponieważ to aliasy, byte jest typem uint8, a rune typem int32; między aliasem a jego typem bazowym nie trzeba żadnej konwersji. Pojedyncze cudzysłowy tworzą runę, podwójne tworzą string. 'ab' to błąd kompilacji, bo literał rune zawiera dokładnie jeden znak.

Rune to liczby, więc można na nich wykonywać działania arytmetyczne:

'7' - '0' to klasyczny sposób zamiany znaku cyfry na jej wartość. Pakiet unicode klasyfikuje rune (IsLetter, IsDigit, IsSpace, IsUpper) i poprawnie zmienia wielkość liter także poza ASCII. %c wypisuje runę jako znak, %U w postaci U+4E16, a %q jako literał rune w cudzysłowach.

Jak UTF-8 zapisuje znaki

Punkty kodoweBajtyPrzykłady
od U+0000 do U+007F1ASCII: a, 7, {
od U+0080 do U+07FF2é, ß, ж, ع
od U+0800 do U+FFFF3語, €, 한
U+10000 i wyżej4emoji, rzadkie znaki CJK

Tekst ASCII w UTF-8 wygląda identycznie, dlatego kod oparty na bajtach często działa w testach i psuje się przy pierwszym imieniu z polskim znakiem. Pakiet unicode/utf8 pracuje bezpośrednio z kodowaniem:

% x (ze spacją) wypisuje bajty szesnastkowo, oddzielone spacjami. utf8.ValidString mówi, czy string jest poprawnym UTF-8. Stringi w Go mogą zawierać dowolne bajty, nie tylko poprawny UTF-8; gdy range natrafi na niepoprawny bajt, zwraca utf8.RuneError (U+FFFD, znak zastępczy) i przesuwa się o jeden bajt.

Konwersje między string, []byte i []rune

KonwersjaWynikKoszt
[]byte(s)bajty UTF-8kopiuje
[]rune(s)zdekodowane punkty kodowedekoduje i kopiuje (4 bajty na runę)
string(b)string z bajtówkopiuje
string(r), gdy r to []runekoduje do UTF-8kopiuje
string(r), gdy r to runestring z jednym znakiemniewielki

Odwracanie po bajtach zamieniłoby 世界 w niepoprawny UTF-8, dlatego reverse działa na runach. Nawet rune nie mówią wszystkiego: znak taki jak é można też zapisać jako e plus łączony akcent, czyli dwie rune. Dla znaków widocznych dla użytkownika (klastrów grafemów) użyj biblioteki takiej jak github.com/rivo/uniseg.

Każda konwersja kopiuje dane, bo stringi są niezmienne, a slice'y nie. W gorącym kodzie unikaj konwertowania tam i z powrotem; kompilator optymalizuje niektóre przypadki (na przykład string(b) użyte jako klucz mapy lub w porównaniu) i pomija kopię.

Indeksowanie daje bajt

s[i] zwraca byte, a s[i:j] wycina fragment według przesunięć w bajtach:

s := "café"
fmt.Println(s[3])            // 195: the first byte of é
fmt.Println(string(s[3]))    // "Ã": that byte read as a code point
fmt.Println(s[:3])           // "caf"
fmt.Println(s[:4])           // "caf\xc3": half of é, invalid UTF-8

string(s[3]) to pułapka: konwersja pojedynczego bajtu na string traktuje go jako punkt kodowy 195, czyli Ã, a nie jako sam bajt. Aby znaleźć granice znaków, użyj range, funkcji z rodziny strings.Index (zwracają przesunięcia w bajtach, które zawsze leżą na granicach znaków) albo utf8.DecodeRuneInString.

Pakiet bytes

bytes odpowiada pakietowi strings, ale dla []byte: bytes.Contains, bytes.Split, bytes.Fields, bytes.TrimSpace, bytes.Equal, bytes.ToUpper i tak dalej. Używaj go, gdy dane przychodzą jako bajty (zawartość plików, treść żądań HTTP, bufory sieciowe), żeby nie konwertować ich na string i z powrotem:

bytes.Buffer to rosnący bufor bajtów, który implementuje io.Reader i io.Writer. To dobre narzędzie, gdy bajty zarówno zapisujesz, jak i odczytujesz; do budowania stringa strings.Builder jest nieco tańszy. Slice'y bajtów porównuj przez bytes.Equal, bo == nie działa na slice'ach.

Co wybrać

  • Tekst, który wyświetlasz, porównujesz lub przeszukujesz: zostaw go jako string i używaj range, gdy przechodzisz po nim znak po znaku.
  • Pozycje znaków, odwracanie, obcinanie do n znaków: zamień na []rune.
  • Wejście i wyjście, hashowanie, kodowanie, protokoły binarne: używaj []byte i pakietu bytes.
  • Pojedynczy znak: rune. Pojedynczy surowy bajt: byte.

Najczęściej zadawane pytania

Czym jest rune w Go?

rune to alias dla int32 i reprezentuje jeden punkt kodowy Unicode. Literał rune zapisuje się w pojedynczych cudzysłowach: 'a' to 97, 'é' to 233, '世' to 19990. Pętla for i, r := range s po stringu daje rune zdekodowane z bajtów UTF-8 tego stringa.

Czym różni się rune od byte w Go?

byte (alias dla uint8) to 8 bitów surowych danych. rune (alias dla int32) to cały znak Unicode. W stringu UTF-8 znaki ASCII zajmują po jednym bajcie, a pozostałe znaki od dwóch do czterech bajtów, ale nadal są jedną runą. len(s) liczy bajty; utf8.RuneCountInString(s) liczy rune.

Jak przejść w pętli po znakach stringa w Go?

Użyj for i, r := range s. Każda iteracja daje przesunięcie w bajtach i i runę r, więc znaki wielobajtowe są obsługiwane poprawnie. Zwykła pętla po indeksach for i := 0; i < len(s); i++ odwiedza bajty, przez co dzieli znaki spoza ASCII na kawałki.

Jak zamienić string na slice bajtów w Go?

b := []byte(s) kopiuje bajty stringa do nowego slice'a. string(b) zamienia z powrotem, znowu z kopią. Użyj zamiast tego []rune(s), gdy musisz pracować na znakach, na przykład żeby odwrócić string albo wziąć jego pierwsze n znaków.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ