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
| Nazwa | Ten sam typ co | Literał | Znaczenie |
|---|---|---|---|
byte | uint8 | byte('A'), 0x41 | jeden bajt danych |
rune | int32 | '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 kodowe | Bajty | Przykłady |
|---|---|---|
| od U+0000 do U+007F | 1 | ASCII: a, 7, { |
| od U+0080 do U+07FF | 2 | é, ß, ж, ع |
| od U+0800 do U+FFFF | 3 | 語, €, 한 |
| U+10000 i wyżej | 4 | emoji, 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
| Konwersja | Wynik | Koszt |
|---|---|---|
[]byte(s) | bajty UTF-8 | kopiuje |
[]rune(s) | zdekodowane punkty kodowe | dekoduje i kopiuje (4 bajty na runę) |
string(b) | string z bajtów | kopiuje |
string(r), gdy r to []rune | koduje do UTF-8 | kopiuje |
string(r), gdy r to rune | string z jednym znakiem | niewielki |
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
stringi używajrange, 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
[]bytei pakietubytes. - 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.