Строки в Go: пока ты считал буквы, Go считал байты 🧠
Если ты думаешь, что строки в Go это просто «текст» - добро пожаловать в нижний уровень ада))
str := "Hi Привет 👩💻"
fmt. Println("len:", len(str))
for i := 0; i < len(str); i++ {
fmt. Printf("%2d: %q\n", i, str[i])
}
fmt. Println("----")
for i, r := range str {
fmt. Printf("%2d: %q\n", i, r)
}
len: 21
0: 'H'
1: 'i'
2: ' '
3: 'Ð'
4: '¥'
5: 'Ð'
...
15: '\u0080'
16: '\u008d'
17: 'ð'
18: '\u009f'
19: '\u0092'
20: '»'
----
0: 'H'
1: 'i'
2: ' '
3: 'Х'
5: 'а'
7: 'й'
9: ' '
10: '👩'
14: '\u200d'
17: '💻'
Да что тут вообще происходит?!
🤬 * неразборчиво матерится
1. Строка под капотом
string в Go - это не массив символов, а неизменяемая последовательность байт.
Вот как выглядит string в рантайме:
type StringHeader struct {
Data uintptr // *byte - первый байт строки в памяти
Len int // длина в байтах (не рунах или символах!)
}
Ни тебе кодировки, даже никакого терминатора \0, как в C.
Проходяcь по строке мы получим следующее:
str := "Привет"
for i := 0; i < len(str)-1; i++ {
// i = byte (uint8), а не символу
}
for i, v := range str {
// i = байтовой позиции начала UTF-8 последовательности
// v = rune (int32)
}
rune - это просто псевдоним для типа int32, который Go использует для представления одного Unicode-кодпоинта (символа в таблице Юникода).
Если конвертировать rune в string (string(v) - мы получим человеческий символ, например «П».
2. Строки иммутабельны
Go жёстко запрещает модификацию строк. Попытка сделать str[0]="A" - не скомпилится. А любая модификация = создание новой строки.
Но если очень хочется мутировать - преобразуй в []byte -> измени -> верни обратно в string(str). Это «грязный» способ, да еще и не бесплатный, тк данные копируются.
3. Строки шарят память
В Go есть 2 принципиально разных слоя где строки могут делить память.
Compile-time string interning (литералы) - компилятор автоматически интернит строковые литералы, т.е. одинаковые строковые константы в коде указывают на один и тот же участок памяти.
a := "hello"
b := "hello"
fmt. Println(a == b) // true
fmt. Println((*reflect. StringHeader)(unsafe. Pointer(&a)). Data ==
(*reflect. StringHeader)(unsafe. Pointer(&b)). Data) // true
Runtime allocation - строка создаётся в процессе выполнения и она будет новым объектом в памяти, даже при одинаковом содержимом и кладет в один общий пул.
s1 := fmt. Sprint("hi")
s2 := fmt. Sprint("hi")
fmt. Println(s1 == s2) // true
fmt. Println((*reflect. StringHeader)(unsafe. Pointer(&s1)). Data ==
(*reflect. StringHeader)(unsafe. Pointer(&s2)). Data) // false
Подстрока, например subStr := str[n:m], делит память с исходной строкой и удерживает весь буфер в памяти. Чтобы «разделить» память - используй strings. Clone(str).
Со строками легко словить «тихую утечку», особенно если не знать, что строка - это просто указатель на срез байтов.
4. Unsafe-трюк
Если прям совсем хочется обойтись без копий - можно использовать unsafe-магию: []byte -> string без копии.
```go
func bytesToStringNoCopy(b []byte) string {
return *(*string)(unsafe. Pointer(&b))
}
```
Но не советую! можно выстрелить в ногу, тк нарушается гарантия безопасности - b может измениться.
5. Конкатенация строк
Каждый += это новая строка, новая аллокация, новая боль. В циклах так еще больнее. А GC тебе спасибо тоже не скажет.
Используй strings. Builder, если хочешь жить)) Как альтернатива еще можно bytes. Buffer или заранее выделенный []byte.
6. Grapheme cluster - ещё один уровень ада
Grapheme cluster - как один видимый символ, даже если под капотом он состоит из нескольких Юникодных рун (кодовых точек).
Например, символ 👩💻 - это две руны (кластер): 👩 + 💻.
Go не умеет работать с такими штуками из коробки. Если хочешь реально "человеческие" символы - используй пакеты вроде http://github.com/rivo/uniseg.
Итого 🤌
Go сам прибирает за тобой, но не читает мысли.
Строки выглядят лёгкими, пока одна «ОК» не держит 10МБ 😄
#learn@goadvocate