В оперативной памяти строковые литералы хранятся как массивы байтов в кодировке utf-8
. Нотация s[i]
возвращает i-й байт в памяти. Этот байт совпадает с символом только в случае однобайтовой кодировки, то есть базовой латинницы. Достаточно поменять в вашем примере строку "hello"
на строку "Привет"
, как ваш цикл s[i]
начнёт выводить всякие странные значения.
Выбор utf-8
разработчиками Go обусловлен желанием сэкономить память на строковых литералах. Мол, весь мир пишет на английском, поэтому ни к чему хранить кучу нулей в юникодной кодировке с фиксированным размером (UTF-32
)
Обратная сторона этой оптимизации - невозможно получить символ по индексу, так как utf-8
тратит разное число байтов на разные символы. Например, базовая латинница занимает один байт, кириллица - два байта, а китайские иероглифы три байта. Не обработав предварительно строку невозможно предсказать, где в памяти находится i
-й символ. Поэтому для получения символов в строке без предварительно обработки Go предоставляет только потоковый интерфейс range
, который перебирает символы в строке один за другим.
package main
import (
"fmt"
)
func main() {
var s string = "привет"
for i := 0; i < len(s); i++ {
fmt.Printf("%q\n", s[i])
}
for _, symbol := range s {
fmt.Printf("%q\n", symbol)
}
}
Результат
'Ð'
'¿'
'Ñ'
'\u0080'
'Ð'
'¸'
'Ð'
'²'
'Ð'
'µ'
'Ñ'
'\u0082'
'п'
'р'
'и'
'в'
'е'
'т'
Альтернативой range
служит приведение к массиву рун (rune
) []rune(s)
. Этот тип используется в Go для представления юникодных символов в 4-х байтовой кодировке. При выполнении этого приведения рантайм пробежит по строке как range
, сложит все найденные юникодные символы в массив 32-х битных целых и вернёт получившийся массив как результат.
Если вам нужно один перебрать символ за символом, пользуйтесь range
. Если нужно несколько раз выбирать символы из промежуточных позиций, приводите строку к массиву []rune