Пост из канала Thank Go!

Что есть строка?

Начнём разбираться со строками. В начале посмотрим, как же это все работает, а потом попробуем ответить на наш любимый вопрос — кому все-таки нужны такие сложности?

На первый взгляд всё очень просто: строка в Го — это слайс байт. Всё, на этом можно было бы и закончить, если бы не одна мелочь — в конечном итоге строки читают люди. А значит где-то тут должны появиться символы языка, понятные человеку. И тут начинается самое интересное.

Некоторые ребята считают, что строки в Го только про UTF-8. Это не так, строковая переменная необязательно содержит валидный набор UTF-8 символов, она может содержать все что угодно. Но пока переусложнять не будем и далее рассмотрим примеры только в UTF-8.

Так что же такое символы языка, понятные человеку? Вот например такой — ‘é’. Юникод-стандарт использует определение “code point”, дословно это тот самый минимальный элемент, который имеет код в юникоде. Авторы языка Го решили, что использовать два слова для такой простой вещи уж слишком многословно и называют ровно эту же штуку «руной». Так и запомним.

Поищем соответствие нашему символу ‘é’ среди юникод рун и с удивлением обнаружим, что оно неоднозначно.

‘é’ → \u00E9

‘é’ → ‘e’ + ‘◌́’ → \u0065\u0301

Получается, что определить однозначно именно человекочитаемый символ ‘é’ возможности нет. В более сложных символах, например, иероглифах, многозначность вообще вырастает в разы.

Вернёмся к началу, строка — это слайс байт. Посмотрим, что нам скажет Го про одинаковые на первый взгляд строчки:

Сравним строковое представление:

// символ 'é' из одной руны
oneRune := "\u00E9"
// символ 'é' из двух рун 'e' и '◌́'
twoRunes := "\u0065\u0301"

// напечатаем
fmt.Printf("%s %s", oneRune, twoRunes)
// >> é é

Сравним байтовое представление:

fmt.Printf("'é': ")
for i := 0; i < len(oneRune); i++ {
   fmt.Printf("%x ", oneRune[i])
}
fmt.Printf("'e' + '◌́: '")
// >> 'é': c3 a9

for i := 0; i < len(twoRunes); i++ {
   fmt.Printf("%x ", twoRunes[i])
}
// >> 'e' + '◌́: 65 cc 81

А равны ли строки?

fmt.Println(oneRune==twoRunes)
// >> false

Т.е. Го никаких подковёрных игр не ведёт, при сравнении используются именно те исходные байтовые строки, которые и были заданы.

Можно поиграться с этим примером в плейграунде.

А слайс ли байт?

Итак, мы узнали, что строка в Го — это слайс байт. Значит и итерируясь по нему, мы будем получать байты?

Проверим

str := "éé" 
for i := 0; i < len(str); i++ {
   fmt.Printf("%x ", str[i])
}
// >> 65 cc 81 65 cc 81 

Все сходится. Попробуем воспользоваться вторым способом итерации — конструкцией for ... range

for num, item := range str {
   fmt.Printf("%v-%q ", num, item)
}
// >> 0-'e' 1-'́' 3-'e' 4-'́' 

Воу-воу, это что же получается, мы итерировались уже не по слайсу байт, а по слайсу рун? Все верно, происходит неявная подмена, первая щепоточка магии от Го.

А как взять конкретную руну из строчки? Ведь наивный подход str[0] вернёт первый элемент слайса байт, а не рун. В этом случае требуется явное преобразование:

fmt.Printf("%q", []rune(str)[0])
// >> е

Закрепим

// просто строка 
fmt.Printf("%v\n", str)
// >> éé

// слайс рун
fmt.Printf("%q\n", []rune(str))
// >> ['e' '́' 'e' '́']

// слайс байт
fmt.Printf("%v\n", []byte(str))
// >> [101 204 129 101 204 129]

Итого:

  1. При итерации по элементам строка представляется как слайс байт []byte(string)
  2. При итерации через for … range строка представлёется как слайс рун []rune(string)

Я так и не нашел логичного объяснения такой хитрости. Рекомендуется это принять и запомнить.

Все примеры в плейграунде

Почитать:

[1] Роб Пайк о строках в Go

[2] Джоел Сполски о том, что должен знать каждый программист о строках в мультиязычных программах

Продолжение истории