מחרוזת ב-Go היא רצף של בתים, וקוד המקור והליטרלים של מחרוזות ב-Go הם UTF-8. byte הוא אחד מהבתים האלה. rune הוא תו Unicode אחד (code point), ש-UTF-8 שומר ב-1 עד 4 בתים. רוב הבלבול סביב מחרוזות ב-Go נובע מערבוב בין השניים:
語 תופס שלושה בתים ב-UTF-8, ולכן "語Go" הוא 5 בתים אבל 3 runes. לולאת האינדקס רואה חמישה בתים, ולבתים 0, 1 ו-2 אין משמעות בפני עצמם. לולאת ה-range מפענחת UTF-8 ומחזירה שלושה runes באינדקסים 0, 3 ו-4: i הוא תמיד היסט בבתים, לא מספר תווים.
byte ו-rune הם כינויים
| שם | אותו טיפוס כמו | ליטרל | משמעות |
|---|---|---|---|
byte | uint8 | byte('A'), 0x41 | בית אחד של נתונים |
rune | int32 | 'A', 'é', '世' | code point אחד של Unicode |
מכיוון שהם כינויים, byte הוא uint8 ו-rune הוא int32; אין צורך בהמרה בין הכינוי לבין הטיפוס שמתחתיו. גרשיים בודדים יוצרים rune, ומירכאות כפולות יוצרות מחרוזת. 'ab' היא שגיאת הידור, כי ליטרל rune מחזיק בדיוק תו אחד.
runes הם מספרים, ולכן אפשר לבצע עליהם חשבון:
'7' - '0' היא הדרך הקלאסית להפוך תו של ספרה לערך שלה. החבילה unicode מסווגת runes (IsLetter, IsDigit, IsSpace, IsUpper) ומשנה אותיות גדולות וקטנות נכון גם באותיות שאינן ASCII. %c מדפיס rune כתו, %U בצורה U+4E16, ו-%q כליטרל rune במירכאות.
איך UTF-8 שומר תווים
| Code points | בתים | דוגמאות |
|---|---|---|
| U+0000 עד U+007F | 1 | ASCII: a, 7, { |
| U+0080 עד U+07FF | 2 | é, ß, ж, ع |
| U+0800 עד U+FFFF | 3 | 語, €, 한 |
| U+10000 ומעלה | 4 | אימוג'י, CJK נדירים |
טקסט ASCII זהה ב-UTF-8, ולכן קוד שעובד על בתים עובד לעיתים קרובות בבדיקות ונשבר בשם הראשון עם ניקוד או אות מוטעמת. החבילה unicode/utf8 עובדת ישירות עם הקידוד:
% x (עם רווח) מדפיס בתים בהקסדצימלי מופרדים ברווחים. utf8.ValidString מדווחת אם מחרוזת היא UTF-8 תקין. מחרוזות ב-Go יכולות להחזיק כל בית, לא רק UTF-8 תקין; כש-range נתקלת בבית לא תקין היא מחזירה utf8.RuneError (U+FFFD, תו ההחלפה) ומתקדמת בבית אחד.
המרה בין string, []byte ו-[]rune
| המרה | תוצאה | עלות |
|---|---|---|
[]byte(s) | בתי ה-UTF-8 | מעתיקה |
[]rune(s) | code points מפוענחים | מפענחת ומעתיקה (4 בתים לכל rune) |
string(b) | מחרוזת מבתים | מעתיקה |
string(r) כש-r הוא []rune | מקודדת ל-UTF-8 | מעתיקה |
string(r) כש-r הוא rune | מחרוזת של תו אחד | קטנה |
היפוך לפי בתים היה מערבב את 世界 ל-UTF-8 לא תקין, ולכן reverse עובדת על runes. גם runes הם לא כל הסיפור: תו כמו é יכול להיכתב גם כ-e ועוד סימן הטעמה מצטרף, כלומר שני runes. לתווים שהמשתמש רואה (grapheme clusters), השתמשו בספרייה כמו github.com/rivo/uniseg.
כל המרה מעתיקה, כי מחרוזות הן immutable ו-slices לא. בקוד חם, הימנעו מהמרות הלוך ושוב; המהדר מבצע אופטימיזציה בחלק מהמקרים (כמו string(b) שמשמש כמפתח של map או בהשוואה) ומדלג על ההעתקה.
גישה לפי אינדקס מחזירה byte
s[i] מחזיר byte, ו-s[i:j] חותך לפי היסטים בבתים:
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]) הוא מלכודת: המרה של בית בודד ל-string מתייחסת אליו כאל ה-code point 195, שהוא Ã, ולא אל הבית עצמו. כדי למצוא גבולות של תווים, השתמשו ב-range, במשפחת strings.Index (שמחזירות היסטים בבתים שתמיד נופלים על גבולות), או ב-utf8.DecodeRuneInString.
החבילה bytes
bytes משקפת את strings עבור []byte: bytes.Contains, bytes.Split, bytes.Fields, bytes.TrimSpace, bytes.Equal, bytes.ToUpper וכן הלאה. השתמשו בה כשהנתונים מגיעים כבתים (תוכן קבצים, גופי HTTP, buffers של רשת), כדי לא להמיר למחרוזת ובחזרה:
bytes.Buffer הוא buffer של בתים שגדל לפי הצורך ומממש את io.Reader ואת io.Writer. הוא הכלי הנכון כשגם כותבים וגם קוראים בתים; לבניית מחרוזת, strings.Builder זול מעט יותר. השוו slices של בתים עם bytes.Equal, כי == לא עובד על slices.
איך לבחור ביניהם
- טקסט שמציגים, משווים או מחפשים בו: השאירו אותו
string, והשתמשו ב-rangeכשעוברים עליו תו אחרי תו. - מיקומי תווים, היפוך, קיצור ל-n תווים: המירו ל-
[]rune. - קלט ופלט, hashing, קידוד, פרוטוקולים בינאריים: השתמשו ב-
[]byteובחבילהbytes. - תו בודד:
rune. בית גולמי בודד:byte.
שאלות נפוצות
מה זה rune ב-Go?
rune הוא כינוי ל-int32 ומייצג code point אחד של Unicode. ליטרל rune נכתב בגרשיים בודדים: 'a' הוא 97, 'é' הוא 233, '世' הוא 19990. מעבר על מחרוזת עם for i, r := range s נותן runes, שמפוענחים מבתי ה-UTF-8 של המחרוזת.
מה ההבדל בין rune ל-byte ב-Go?
byte (כינוי ל-uint8) הוא 8 ביטים של נתונים גולמיים. rune (כינוי ל-int32) הוא תו Unicode שלם. במחרוזת UTF-8, כל תו ASCII תופס בית אחד, ותווים אחרים תופסים שניים עד ארבעה בתים אבל הם עדיין rune אחד. len(s) סופרת בתים; utf8.RuneCountInString(s) סופרת runes.
איך עוברים בלולאה על התווים של מחרוזת ב-Go?
השתמשו ב-for i, r := range s. כל איטרציה נותנת את ההיסט בבתים i ואת ה-rune r, כך שתווים מרובי בתים מטופלים נכון. לולאת אינדקס רגילה for i := 0; i < len(s); i++ עוברת על בתים, ומפרקת תווים שאינם ASCII לחתיכות.
איך ממירים מחרוזת ל-slice של בתים ב-Go?
b := []byte(s) מעתיקה את הבתים של המחרוזת ל-slice חדש. string(b) ממירה בחזרה, שוב עם העתקה. השתמשו במקום זה ב-[]rune(s) כשצריך לעבוד עם תווים, למשל כדי להפוך מחרוזת או לקחת את n התווים הראשונים שלה.