Strings, Runes и Bytes в Go — под капотом
Тема, на которой “плавает” 80% джунов: разница между байтом, руной и строкой; почему
len("Привет")это 12, а не 6; чем отличаетсяfor i := range sотfor i := 0; i < len(s); i++; когда[]byte(s)копирует, а когда нет. Если вы понимаете UTF-8 иstringStruct— собес по строкам пройдёте легко.
Содержание (TOC)
Заголовок раздела «Содержание (TOC)»- Базовое определение и API
- Внутреннее устройство (под капотом)
- Тонкие моменты / Gotchas
- Производительность
- Типичные вопросы на собеседовании Junior
- Practice — задачки на проверку
- Источники и дополнительно
1. Базовое определение и API
Заголовок раздела «1. Базовое определение и API»1.1 Три фундаментальных типа
Заголовок раздела «1.1 Три фундаментальных типа»| Тип | Размер | Что это | Алиас для |
|---|---|---|---|
byte | 1 байт | сырой байт | uint8 |
rune | 4 байта | Unicode code point | int32 |
string | 16 байт header | immutable UTF-8 закодированная последовательность байт | — |
var b byte = 65 // ASCII 'A'var r rune = '日' // Unicode code point 0x65E5 (26085)var s string = "Привет" // 6 рун, но 12 байт (UTF-8)1.2 string в Go
Заголовок раздела «1.2 string в Go»string — это последовательность байт (не символов!), immutable, обычно содержит UTF-8 текст. Но в принципе может содержать любые байты.
s := "hello"fmt.Println(len(s)) // 5 — байтfmt.Println(s[0]) // 104 — byte (uint8), не "h" как символfmt.Println(string(s[0])) // "h"1.3 Базовые операции
Заголовок раздела «1.3 Базовые операции»s := "Hello, World!"
// Длина (в байтах!)len(s) // 13
// Индексация — возвращает byte (uint8), не rune!s[0] // 72 (byte, ASCII 'H')
// Срез — это новая string (но без копирования, см. ниже)s[7:12] // "World"
// Конкатенацияs2 := s + " hi"
// Сравнение — по байтамs == "Hello, World!" // trues < "Z" // лексикографическое
// Преобразованияb := []byte(s)r := []rune(s)back := string(b)back2 := string(r)1.4 Итерация
Заголовок раздела «1.4 Итерация»s := "Привет"
// 1. По БАЙТАМ — for i; i++for i := 0; i < len(s); i++ { fmt.Printf("%d: %d (%c)\n", i, s[i], s[i]) // выведет 12 итераций, каждый — байт}
// 2. По РУНАМ — for rangefor i, r := range s { fmt.Printf("%d: %d (%c)\n", i, r, r) // выведет 6 итераций (по числу рун) // i — байтовое смещение (0, 2, 4, ...) // r — rune (int32)}⚠️ В for i, r := range s индекс i — это байтовое смещение, а не индекс руны!
s := "Привет"for i, r := range s { fmt.Println(i, string(r))}// 0 П// 2 р// 4 и// 6 в// 8 е// 10 т1.5 Основные пакеты
Заголовок раздела «1.5 Основные пакеты»import ( "strings" // утилиты: Split, Join, Replace, Contains, Builder "strconv" // Atoi, Itoa, Quote, ParseFloat "unicode" // IsLetter, IsDigit, ToLower "unicode/utf8" // RuneCountInString, DecodeRuneInString "bytes" // те же утилиты для []byte)2. Внутреннее устройство (ПОД КАПОТОМ)
Заголовок раздела «2. Внутреннее устройство (ПОД КАПОТОМ)»2.1 StringHeader
Заголовок раздела «2.1 StringHeader»// runtime/string.go (упрощённо)type stringStruct struct { str unsafe.Pointer // указатель на байты len int // длина в БАЙТАХ}
// reflect/value.go (deprecated с Go 1.20)type StringHeader struct { Data uintptr Len int}Размер на 64-bit: 16 байт (2 × 8).
⚠️ В отличие от SliceHeader, у строки нет cap — строка immutable, ёмкость не нужна.
2.2 ASCII-схема памяти string
Заголовок раздела «2.2 ASCII-схема памяти string»s := "Привет"string header (16 байт)┌──────────────┐│ ptr ────────┼─────┐│ len = 12 │ │└──────────────┘ │ ▼Read-only data segment (например, .rodata):┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐│ D0 │ 9F │ D1 │ 80 │ D0 │ B8 │ D0 │ B2 │ D0 │ B5 │ D1 │ 82 │└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘ П р и в е т⚠️ Строки-литералы обычно живут в read-only сегменте. Попытка модифицировать (через unsafe) приведёт к segfault.
2.3 UTF-8 — что это и как кодирует
Заголовок раздела «2.3 UTF-8 — что это и как кодирует»UTF-8 — variable-width кодировка Unicode:
| Code point | Bytes | Pattern |
|---|---|---|
U+0000 - U+007F | 1 | 0xxxxxxx |
U+0080 - U+07FF | 2 | 110xxxxx 10xxxxxx |
U+0800 - U+FFFF | 3 | 1110xxxx 10xxxxxx 10xxxxxx |
U+10000 - U+10FFFF | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
Примеры:
'A'(U+0041): 1 байт —0x41'Я'(U+042F): 2 байта —0xD0 0xAF'日'(U+65E5): 3 байта —0xE6 0x97 0xA5'🎉'(U+1F389): 4 байта —0xF0 0x9F 0x8E 0x89
s := "A日🎉"fmt.Println(len(s)) // 8 (1+3+4)fmt.Println(utf8.RuneCountInString(s)) // 3 (количество РУН)2.4 rune
Заголовок раздела «2.4 rune»Rune — это Unicode code point, тип int32. Один rune может занимать 1-4 байта в UTF-8.
r := '日'fmt.Printf("%d %x %c\n", r, r, r) // 26085 65e5 日2.5 Conversion: string ↔ []byte ↔ []rune
Заголовок раздела «2.5 Conversion: string ↔ []byte ↔ []rune»s := "Hello"
// string → []byte (копирует!)b := []byte(s)
// []byte → string (копирует!)s2 := string(b)
// string → []rune (декодирует UTF-8, копирует!)r := []rune("日本語") // [26085 26412 35486]
// []rune → string (кодирует в UTF-8)s3 := string([]rune{72, 101, 108, 108, 111}) // "Hello"2.6 Compiler optimization для conversion
Заголовок раздела «2.6 Compiler optimization для conversion»Компилятор иногда избегает копирования:
-
string(b)в map lookup:m := map[string]int{"a": 1}b := []byte{'a'}v := m[string(b)] // НЕ копирует, оптимизация! -
[]byte(s)вfor range:s := "hello"for _, c := range []byte(s) { // компилятор может не копировать_ = c} -
s += "x"в hot loop — компилятор иногда оптимизирует, но не всегда. -
С Go 1.20+ есть
unsafe.Stringиunsafe.StringData/unsafe.SliceData— явный способ без копирования (см. ниже).
3. Тонкие моменты / Gotchas
Заголовок раздела «3. Тонкие моменты / Gotchas»Gotcha 1: len() считает байты, не символы
Заголовок раздела «Gotcha 1: len() считает байты, не символы»s := "Привет"fmt.Println(len(s)) // 12 (байт)fmt.Println(utf8.RuneCountInString(s)) // 6 (рун)⚠️ Самая популярная ошибка джунов: ожидают, что len("Привет") — 6.
Gotcha 2: Индексация даёт байт, не символ
Заголовок раздела «Gotcha 2: Индексация даёт байт, не символ»s := "Привет"fmt.Println(s[0]) // 208 (первый байт UTF-8)fmt.Println(string(s[0])) // "Ð" — мусор!
// Правильно для первого символа:r, _ := utf8.DecodeRuneInString(s)fmt.Println(string(r)) // "П"
// Или через []rune (если нужны все):rs := []rune(s)fmt.Println(string(rs[0])) // "П"Gotcha 3: range vs for-i — разная итерация
Заголовок раздела «Gotcha 3: range vs for-i — разная итерация»s := "日"
for i := 0; i < len(s); i++ { fmt.Printf("%d: %d\n", i, s[i])}// 0: 230// 1: 151// 2: 165
for i, r := range s { fmt.Printf("%d: %d\n", i, r)}// 0: 26085 — одна итерацияGotcha 4: string(int) — это rune, не цифры!
Заголовок раздела «Gotcha 4: string(int) — это rune, не цифры!»Самая каверзная ловушка:
n := 65s := string(n)fmt.Println(s) // "A" — это rune 65, не "65"!
s2 := string(72) // "H"✅ Чтобы получить число как строку:
s := strconv.Itoa(65) // "65"s2 := fmt.Sprintf("%d", 65)⚠️ С Go 1.15 go vet ругается на string(int) без явного rune-конверта:
string(rune(65)) // явноGotcha 5: Substring — это срез без копирования (memory leak!)
Заголовок раздела «Gotcha 5: Substring — это срез без копирования (memory leak!)»s := loadBigString() // допустим, 1 GBsmall := s[:10] // small ссылается на тот же backing arrays = "" // не помогает! backing array жив через small✅ Решение — явная копия:
small := string([]byte(s[:10]))// или strings.Clone (Go 1.18+):small := strings.Clone(s[:10])Gotcha 6: Конкатенация в цикле — O(n²)
Заголовок раздела «Gotcha 6: Конкатенация в цикле — O(n²)»// ПЛОХО: каждая итерация — новая аллокацияvar s stringfor i := 0; i < 1_000_000; i++ { s += "x"}
// ХОРОШО: strings.Buildervar b strings.Builderb.Grow(1_000_000) // preallocationfor i := 0; i < 1_000_000; i++ { b.WriteByte('x')}s := b.String()strings.Builder использует []byte под капотом и избегает копирования при финальном String() (через unsafe).
Gotcha 7: Strings immutable
Заголовок раздела «Gotcha 7: Strings immutable»s := "hello"// s[0] = 'H' // ОШИБКА компиляции: cannot assign to s[0]Нужно: b := []byte(s); b[0] = 'H'; s = string(b).
Gotcha 8: []byte(s) и string(b) — почти всегда копируют
Заголовок раздела «Gotcha 8: []byte(s) и string(b) — почти всегда копируют»s := "hello"b := []byte(s) // КОПИЯb[0] = 'H' // не влияет на sfmt.Println(s) // "hello"Это сделано специально: чтобы string был immutable (если бы byte slice ссылался на ту же память, можно было бы мутировать строку через unsafe).
Исключения — compiler optimizations (см. выше) и unsafe.String/Slice (Go 1.20+).
Gotcha 9: rune vs int сравнение
Заголовок раздела «Gotcha 9: rune vs int сравнение»r := 'A'fmt.Printf("%T\n", r) // int32 (rune)// Можно: r == 65 (untyped constant → конвертируется)// Нельзя: var i int = 65; r == i // ОШИБКА: разные типы
r == int32(i) // OKr == rune(i) // OKint(r) == i // OKGotcha 10: Невалидный UTF-8 → RuneError
Заголовок раздела «Gotcha 10: Невалидный UTF-8 → RuneError»При итерации по невалидному UTF-8 for range подставит utf8.RuneError ('�') — символ замены.
s := string([]byte{0xFF, 0xFE}) // невалидноfor _, r := range s { fmt.Println(r) // 65533 (RuneError)}Gotcha 11: ToUpper/ToLower с локалью
Заголовок раздела «Gotcha 11: ToUpper/ToLower с локалью»Стандартный strings.ToUpper использует Unicode default case mapping, не локаль.
strings.ToUpper("straße") // "STRASSE" (особый случай ß → SS)strings.ToUpper("istanbul") // "ISTANBUL" (но в турецком: İSTANBUL)Для локализации — golang.org/x/text/cases + language.Turkish.
Gotcha 12: EqualFold для case-insensitive
Заголовок раздела «Gotcha 12: EqualFold для case-insensitive»strings.EqualFold("Hello", "HELLO") // true"Hello" == "HELLO" // false
// Не используйте ToLower/ToUpper для сравнения:strings.ToLower("Hello") == strings.ToLower("HELLO") // работает, но 2 аллокацииGotcha 13: Сравнение строк с unicode-нормализацией
Заголовок раздела «Gotcha 13: Сравнение строк с unicode-нормализацией»// Эти строки выглядят одинаково, но разные code point:a := "café" // c-a-f-é (4 руны)b := "café" // c-a-f-e-(combining acute) (5 рун)fmt.Println(a == b) // false!Решение — нормализация через golang.org/x/text/unicode/norm:
import "golang.org/x/text/unicode/norm"norm.NFC.String(a) == norm.NFC.String(b) // trueGotcha 14: strings.Builder нельзя копировать
Заголовок раздела «Gotcha 14: strings.Builder нельзя копировать»var b strings.Builderb.WriteString("hello")b2 := b // ⚠️ копирование запрещено (warning от go vet)b2.WriteString(" world") // может вызвать panicBuilder содержит указатель и проверяет, не был ли он скопирован. Используйте *strings.Builder для передачи.
Gotcha 15: unsafe.String / unsafe.Slice (Go 1.20+)
Заголовок раздела «Gotcha 15: unsafe.String / unsafe.Slice (Go 1.20+)»import "unsafe"
s := "hello"// string → []byte без копирования (опасно: нельзя модифицировать!)b := unsafe.Slice(unsafe.StringData(s), len(s))
// []byte → string без копированияb2 := []byte{'h', 'i'}s2 := unsafe.String(&b2[0], len(b2))// ⚠️ Если потом изменить b2, изменится и s2 — что нарушает immutability!✅ Используйте только в hot path, после бенчмарков, понимая риски.
Gotcha 16: Хеш строк рандомизирован при старте
Заголовок раздела «Gotcha 16: Хеш строк рандомизирован при старте»// Hash для одной и той же строки в разных запусках программы — разный// Это защита от hash flood DoS (см. maps)Поэтому не сериализуйте hash строки между запусками.
Gotcha 17: Сравнение строк через ==
Заголовок раздела «Gotcha 17: Сравнение строк через ==»s1 == s2 — это сравнение по байтам, O(n) в худшем случае. Сначала Go проверяет длину (O(1)), потом байты.
// "abc" == "abc" → сначала len: 3==3, потом memcmp4. Производительность
Заголовок раздела «4. Производительность»4.1 strings.Builder vs concatenation
Заголовок раздела «4.1 strings.Builder vs concatenation»Бенчмарк показывает 100-1000x разницу для длинных строк:
// O(n²)var s stringfor _, w := range words { s += w}
// O(n)var b strings.Builderfor _, w := range words { b.WriteString(w)}return b.String()Builder.String() использует unsafe для избежания копии при возврате.
4.2 strings.Join — самый быстрый для известного списка
Заголовок раздела «4.2 strings.Join — самый быстрый для известного списка»s := strings.Join(words, "")Под капотом — preallocate точного размера + memcpy. Лучший вариант, когда все слова известны заранее.
4.3 Preallocation в Builder
Заголовок раздела «4.3 Preallocation в Builder»var b strings.Builderb.Grow(1024) // если знаете примерный размерfor ... { b.WriteString(...) }4.4 []byte vs string в hot path
Заголовок раздела «4.4 []byte vs string в hot path»[]byte мутабельный → меньше аллокаций.
// Если работаете с большими буферами:buf := make([]byte, 0, 4096)for ... { buf = append(buf, data...)}return string(buf) // ОДНА копия в конце4.5 strconv vs fmt
Заголовок раздела «4.5 strconv vs fmt»strconv.Itoa(123) // быстро, no reflectionfmt.Sprintf("%d", 123) // медленнее, reflectionРазница 5-10x. В hot path используйте strconv.
4.6 Iterate by byte if ASCII-only
Заголовок раздела «4.6 Iterate by byte if ASCII-only»Если данные гарантированно ASCII (например, JSON keys), итерация по байтам быстрее range:
// Быстрее (если ASCII):for i := 0; i < len(s); i++ { if s[i] == '"' { ... }}
// Медленнее (декодирует UTF-8):for _, r := range s { if r == '"' { ... }}4.7 strings.Cut (Go 1.18+)
Заголовок раздела «4.7 strings.Cut (Go 1.18+)»// Эффективная замена для SplitN(s, sep, 2):before, after, found := strings.Cut("key=value", "=")Без аллокации slice результата.
4.8 string interning
Заголовок раздела «4.8 string interning»Если у вас миллионы повторяющихся строк (например, имена полей JSON), интернирование экономит память:
var interner = map[string]string{}func Intern(s string) string { if v, ok := interner[s]; ok { return v } interner[s] = s return s}С Go 1.23 появилось unique.Handle[T] для типобезопасного interning.
5. Типичные вопросы на собеседовании Junior
Заголовок раздела «5. Типичные вопросы на собеседовании Junior»Q1: Что такое string в Go?
Заголовок раздела «Q1: Что такое string в Go?»A: Immutable последовательность байт. Под капотом — header {ptr, len} (16 байт на 64-bit). Обычно содержит UTF-8 текст, но может — любые байты.
Q2: Что выведет len("Привет")?
Заголовок раздела «Q2: Что выведет len("Привет")?»A: 12. Это длина в байтах. В русском (Cyrillic) каждая буква — 2 байта в UTF-8. Для количества символов: utf8.RuneCountInString(s) или len([]rune(s)).
Q3: Чем отличается byte от rune?
Заголовок раздела «Q3: Чем отличается byte от rune?»A:
byte— алиасuint8, 1 байт, обычно для сырых данных или ASCII.rune— алиасint32, 4 байта, Unicode code point.
Q4: Что выведет?
Заголовок раздела «Q4: Что выведет?»s := "日"fmt.Println(s[0])A: 230. Это первый байт UTF-8 представления символа 日 (0xE6 = 230).
Q5: Что выведет?
Заголовок раздела «Q5: Что выведет?»s := "Hello"fmt.Println(string(s[0]))A: "H". Здесь s[0] — byte (72), string(72) интерпретирует как rune 72, что есть 'H'.
Q6: В чём разница итерации?
Заголовок раздела «Q6: В чём разница итерации?»for i := 0; i < len(s); i++ {} // ?for i, r := range s {} // ?A:
for i; i++— по байтам,s[i]— byte.for range— по рунам,r— rune,i— байтовое смещение начала руны.
Q7: Почему строки в Go immutable?
Заголовок раздела «Q7: Почему строки в Go immutable?»A:
- Безопасность: можно безопасно делиться без блокировок.
- Оптимизация: substring — без копирования (просто header указывает в тот же memory).
- Можно хранить в read-only сегменте бинарника.
- Хеш можно кэшировать.
Q8: Когда []byte(s) копирует?
Заголовок раздела «Q8: Когда []byte(s) копирует?»A: Почти всегда. Это сделано специально, чтобы immutable string не мутировался через mutable []byte. Compiler optimization избегает копирования в специфичных случаях (map lookup, for-range).
Q9: Что такое UTF-8?
Заголовок раздела «Q9: Что такое UTF-8?»A: Variable-width кодировка Unicode. ASCII (0-127) — 1 байт. Расширенные — 2-4 байта. Самосинхронизируется (можно начать читать с любого байта и понять, где границы рун).
Q10: Что выведет?
Заголовок раздела «Q10: Что выведет?»fmt.Println(string(65))A: "A". Это rune conversion: 65 интерпретируется как Unicode code point. С Go 1.15 go vet ругается без явного string(rune(65)).
Q11: Как правильно преобразовать int в string?
Заголовок раздела «Q11: Как правильно преобразовать int в string?»A:
strconv.Itoa(123) // "123"fmt.Sprintf("%d", 123) // "123"// НЕ: string(123) // "{" — code point 123Q12: Чем strings.Builder лучше +=?
Заголовок раздела «Q12: Чем strings.Builder лучше +=?»A:
+=создаёт новую строку каждый раз → O(n²).Builderиспользует[]byteпод капотом, амортизированный O(1) на запись.Builder.String()возвращает результат без копирования (unsafe).
Q13: Можно ли получить адрес символа в строке?
Заголовок раздела «Q13: Можно ли получить адрес символа в строке?»A: Нет, &s[0] — ошибка. Строки immutable, доступ только для чтения. Через unsafe.StringData(s) (Go 1.20+) — можно, но мутировать нельзя.
Q14: Что такое RuneError?
Заголовок раздела «Q14: Что такое RuneError?»A: Константа utf8.RuneError = '�' (символ замены). Возвращается при декодировании невалидного UTF-8.
Q15: Как сравнить две строки без учёта регистра?
Заголовок раздела «Q15: Как сравнить две строки без учёта регистра?»A: strings.EqualFold(a, b). Лучше, чем ToLower(a) == ToLower(b) (нет аллокаций, обрабатывает Unicode-edge cases).
Q16: Можно ли сравнить строки через </>?
Заголовок раздела «Q16: Можно ли сравнить строки через </>?»A: Да, лексикографическое сравнение по байтам. UTF-8 устроен так, что byte-order сравнение совпадает с code point order.
Q17: Что выведет?
Заголовок раздела «Q17: Что выведет?»s := "café"fmt.Println(len(s))A: 5. c, a, f — по 1 байту, é — 2 байта в UTF-8 (U+00E9 = 0xC3 0xA9).
Q18: Что произойдёт с substring при memory?
Заголовок раздела «Q18: Что произойдёт с substring при memory?»A: Substring — это header, указывающий на ту же память. Если оригинальная строка большая, держа substring, мы держим всю память. Решение: strings.Clone(substr).
Q19: Что выведет?
Заголовок раздела «Q19: Что выведет?»b := []byte("hello")b[0] = 'H's := string(b)fmt.Println(s)A: "Hello". []byte мутабельный.
Q20: Что такое strings.Cut?
Заголовок раздела «Q20: Что такое strings.Cut?»A: Go 1.18+: разделяет строку по первому вхождению separator. Возвращает before, after, found. Эффективная замена для SplitN(s, sep, 2).
before, after, ok := strings.Cut("key=value", "=")// before="key", after="value", ok=trueQ21: Когда использовать []byte вместо string?
Заголовок раздела «Q21: Когда использовать []byte вместо string?»A:
- Когда нужна мутабельность.
- В I/O —
io.Reader/io.Writerработают с[]byte. - В hot path, чтобы избежать копирования при
[]byte(s). - Для бинарных данных (картинки, networking).
Q22: Размер заголовка string?
Заголовок раздела «Q22: Размер заголовка string?»A: 16 байт на 64-bit (ptr 8 байт + len 8 байт). У строки нет capacity (в отличие от slice).
Q23: Что такое NFC/NFD нормализация?
Заголовок раздела «Q23: Что такое NFC/NFD нормализация?»A: Unicode нормализация. NFC — composed (одна руна на символ когда возможно). NFD — decomposed (комбинируемые символы). Без нормализации “café” может быть представлено двумя способами, и они != по байтам.
Q24: Как корректно итерировать с обоими индексом руны и значением?
Заголовок раздела «Q24: Как корректно итерировать с обоими индексом руны и значением?»A:
for i, r := range s { // i — байтовое смещение! _ = i; _ = r}
// Если нужен ИНДЕКС руны:ri := 0for _, r := range s { _ = ri; _ = r ri++}
// Или сначала []rune:runes := []rune(s)for i, r := range runes { ... }Q25: Что выведет?
Заголовок раздела «Q25: Что выведет?»var b strings.Builderb.WriteString("Hello")b.WriteRune(' ')b.WriteString("World")fmt.Println(b.String())A: "Hello World".
6. Practice — задачки на проверку
Заголовок раздела «6. Practice — задачки на проверку»Задача 1: Реверс строки
Заголовок раздела «Задача 1: Реверс строки»Перевернуть строку с учётом Unicode (НЕ просто байты).
func Reverse(s string) string { // ???}Решение:
func Reverse(s string) string { runes := []rune(s) for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { runes[i], runes[j] = runes[j], runes[i] } return string(runes)}
// Тест:Reverse("Привет") // "тевирП" — правильно// Если бы делали через []byte — получили бы мусорЗадача 2: Подсчёт рун
Заголовок раздела «Задача 2: Подсчёт рун»Посчитайте количество рун в строке тремя способами.
Решение:
s := "Привет"
// 1.n1 := utf8.RuneCountInString(s) // 6
// 2.n2 := len([]rune(s)) // 6 (но аллокация!)
// 3.n3 := 0for range s { n3++ } // 6, без аллокацийСамый быстрый — №1 или №3 (без аллокаций).
Задача 3: Конкатенация эффективно
Заголовок раздела «Задача 3: Конкатенация эффективно»Соедините 1М строк в одну, время покажет разницу.
words := makeWords(1_000_000)
// БЫСТРО:var b strings.Builderb.Grow(estimatedSize(words))for _, w := range words { b.WriteString(w)}result := b.String()
// ИЛИ:result := strings.Join(words, "")
// МЕДЛЕННО:var s stringfor _, w := range words { s += w}Задача 4: Случай палиндрома
Заголовок раздела «Задача 4: Случай палиндрома»Проверить, является ли строка палиндромом (с поддержкой Unicode, case-insensitive).
Решение:
import "unicode"
func IsPalindrome(s string) bool { runes := []rune(s) i, j := 0, len(runes)-1 for i < j { // Пропускаем не-буквы for i < j && !unicode.IsLetter(runes[i]) { i++ } for i < j && !unicode.IsLetter(runes[j]) { j-- } if unicode.ToLower(runes[i]) != unicode.ToLower(runes[j]) { return false } i++; j-- } return true}
IsPalindrome("А роза упала на лапу Азора") // trueIsPalindrome("racecar") // trueIsPalindrome("hello") // falseЗадача 5: Парсинг CSV-строки (без пакета csv)
Заголовок раздела «Задача 5: Парсинг CSV-строки (без пакета csv)»func ParseCSVRow(row string) []string { var fields []string var b strings.Builder for i := 0; i < len(row); i++ { c := row[i] if c == ',' { fields = append(fields, b.String()) b.Reset() } else { b.WriteByte(c) } } fields = append(fields, b.String()) return fields}(Упрощённая версия без поддержки кавычек и escape.)
Задача 6: Угадай вывод
Заголовок раздела «Задача 6: Угадай вывод»s := "Hello"b := []byte(s)b[0] = 'h'fmt.Println(s, string(b))Решение: Hello hello. []byte(s) — это копия, мутация b не влияет на s.
7. Источники и дополнительно
Заголовок раздела «7. Источники и дополнительно»- Go Blog — “Strings, bytes, runes and characters in Go” (Rob Pike) — https://go.dev/blog/strings — must-read.
- Russ Cox — “Strings, bytes, runes and characters” — детали реализации.
- The Go Programming Language Specification — Strings — https://go.dev/ref/spec#String_types
- unicode/utf8 package — https://pkg.go.dev/unicode/utf8 — RuneCountInString, DecodeRune.
- strings package — https://pkg.go.dev/strings — Builder, Cut, EqualFold.
- unsafe.String/Slice (Go 1.20) — https://pkg.go.dev/unsafe#String — zero-copy conversion.
- Habr — “Строки в Go: байты, руны и UTF-8” — поиск свежих обзоров 2024-2025.
- runtime/string.go — https://github.com/golang/go/blob/master/src/runtime/string.go — исходники.
- “100 Go Mistakes and How to Avoid Them” (Teiva Harsanyi) — разделы про strings.
- golang.org/x/text/unicode/norm — для NFC/NFD нормализации.
Строка в Go — это байтовый view. Помнить про UTF-8, immutability и compiler magic при конвертациях — и собес по строкам становится тривиальным.