Slices в Go — под капотом
Слайсы — одна из самых частых тем на собесе Go-джуна. И одна из самых “ловушечных”. Если вы понимаете SliceHeader, growth pattern
append, sharing backing array — большинство caverzных вопросов снимается. Этот файл — фундамент, без которого не пройти собес.
Содержание (TOC)
Заголовок раздела «Содержание (TOC)»- Базовое определение и API
- Внутреннее устройство (под капотом)
- Тонкие моменты / Gotchas
- Производительность
- Типичные вопросы на собеседовании Junior
- Practice — задачки на проверку
- Источники и дополнительно
1. Базовое определение и API
Заголовок раздела «1. Базовое определение и API»1.1 Что такое slice
Заголовок раздела «1.1 Что такое slice»Slice — это дескриптор (header) над фрагментом массива. Не сам массив. Slice состоит из 3 полей:
- ptr — указатель на начало фрагмента в underlying array
- len — текущая длина (количество доступных элементов)
- cap — capacity (сколько ещё можно “вырасти” без перевыделения памяти)
var s []int // nil slice: ptr=nil, len=0, cap=0s1 := []int{1,2,3} // literal, len=3, cap=3s2 := make([]int, 5) // len=5, cap=5, нулиs3 := make([]int, 3, 10) // len=3, cap=101.2 Базовые операции
Заголовок раздела «1.2 Базовые операции»s := []int{10, 20, 30, 40, 50}
len(s) // 5cap(s) // 5s[0] // 10s[1:3] // [20 30] (новый slice, общий backing array)s[:2] // [10 20]s[2:] // [30 40 50]s[:] // [10 20 30 40 50]
s = append(s, 60) // [10 20 30 40 50 60]s = append(s, 70, 80) // вариативныйother := []int{100, 200}s = append(s, other...) // распаковка1.3 Создание
Заголовок раздела «1.3 Создание»// 1. Literala := []int{1, 2, 3}
// 2. makeb := make([]int, 5) // len=5, cap=5c := make([]int, 0, 10) // len=0, cap=10 — preallocation
// 3. Из массиваarr := [5]int{1,2,3,4,5}d := arr[1:4] // [2 3 4]
// 4. Nilvar e []int // nilfmt.Println(e == nil) // truefmt.Println(len(e)) // 0e = append(e, 1) // работает! Создаст новый backing array2. Внутреннее устройство (ПОД КАПОТОМ)
Заголовок раздела «2. Внутреннее устройство (ПОД КАПОТОМ)»2.1 Структура SliceHeader
Заголовок раздела «2.1 Структура SliceHeader»// runtime/slice.go (упрощённо)type slice struct { array unsafe.Pointer // ptr к началу элементов len int cap int}
// reflect/value.go — публичная версия (deprecated с Go 1.20):type SliceHeader struct { Data uintptr Len int Cap int}⚠️ reflect.SliceHeader помечен deprecated с Go 1.20. Современный способ — unsafe.Slice/unsafe.SliceData.
Размер header на 64-bit: 24 байта (3 × 8).
2.2 ASCII-схема памяти слайса
Заголовок раздела «2.2 ASCII-схема памяти слайса»arr := [10]int{10,20,30,40,50,60,70,80,90,100}s := arr[2:5] // len=3, cap=8 Slice header (24 байта) ┌────────────────────┐ │ ptr ──────────────┼──────┐ │ len = 3 │ │ │ cap = 8 │ │ └────────────────────┘ │ ▼Underlying array (массив на 10 элементов):┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐│ 10 │ 20 │ 30 │ 40 │ 50 │ 60 │ 70 │ 80 │ 90 │100 │└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘ ▲ ▲ ▲ ▲ ▲ ▲ ▲ ▲ └ ptr указывает сюда (на 30) └ len = 3 ─────┘ └ cap = 8 ──────────────────────────┘2.3 Growth — как растёт append
Заголовок раздела «2.3 Growth — как растёт append»Когда len == cap и нужно добавить элемент, аллоцируется новый массив большего размера, и старые элементы копируются.
Алгоритм (Go 1.18+):
если новый требуемый размер < threshold (256): new_cap = 2 * old_capиначе: new_cap = old_cap + (old_cap + 3*256) / 4(плавно сходится к 1.25x для больших слайсов)До Go 1.18 порог был 1024 и удвоение использовалось дольше. С 1.18 алгоритм сделали более плавным, чтобы экономить память на больших слайсах.
s := make([]int, 0)for i := 0; i < 10; i++ { s = append(s, i) fmt.Printf("len=%d cap=%d\n", len(s), cap(s))}
// Примерный вывод (зависит от версии Go):// len=1 cap=1// len=2 cap=2// len=3 cap=4// len=4 cap=4// len=5 cap=8// len=6 cap=8// ...⚠️ Точные значения cap не гарантированы спецификацией. Не закладывайтесь на них в коде.
2.4 Что возвращает append
Заголовок раздела «2.4 Что возвращает append»append может вернуть тот же или новый слайс. Поэтому всегда присваивайте:
s = append(s, x) // правильноappend(s, x) // бессмысленно: результат теряетсяКогда append НЕ копирует:
- если
len+n <= cap— просто пишет в существующий backing array, обновляетlen.
Когда append копирует:
- если
len+n > cap— новая аллокация, копирование, новый backing array.
2.5 Slicing s[i:j:k] — full slice expression
Заголовок раздела «2.5 Slicing s[i:j:k] — full slice expression»s[low : high : max]low— start index (включительно)high— end index (исключительно),len = high - lowmax— capacity limit,cap = max - low
arr := [10]int{0,1,2,3,4,5,6,7,8,9}s := arr[2:5:6]fmt.Println(s) // [2 3 4]fmt.Println(len(s)) // 3fmt.Println(cap(s)) // 4 (= 6-2)Это критически важно для предотвращения memory leaks (см. ниже).
3. Тонкие моменты / Gotchas
Заголовок раздела «3. Тонкие моменты / Gotchas»Gotcha 1: Изменение слайса в функции — backing array shared
Заголовок раздела «Gotcha 1: Изменение слайса в функции — backing array shared»func modify(s []int) { s[0] = 999 // ВИДНО снаружи!}
s := []int{1,2,3}modify(s)fmt.Println(s) // [999 2 3]Слайс — это header, копируется по значению, но указатель внутри — тот же. Поэтому изменение элементов видно.
Но:
func appendInside(s []int) { s = append(s, 999) // s — локальная копия header}
s := []int{1,2,3}appendInside(s)fmt.Println(s) // [1 2 3] — не изменился!Здесь append поменял локальную копию header (новый len). Снаружи slice остался прежним.
✅ Если функция меняет slice → возвращайте новый:
func appendInside(s []int) []int { return append(s, 999)}s = appendInside(s)Gotcha 2: append к sub-slice мутирует “соседа”
Заголовок раздела «Gotcha 2: append к sub-slice мутирует “соседа”»a := []int{1, 2, 3, 4, 5}b := a[:3] // b = [1 2 3], len=3, cap=5b = append(b, 999) // НЕ выделил новый массив, cap=5 хватает
fmt.Println(a) // [1 2 3 999 5] — !!!fmt.Println(b) // [1 2 3 999]⚠️ append записал 999 в a[3], потому что cap позволял.
Решение — full slice expression:
b := a[:3:3] // cap=3 — append вынужден будет аллоцироватьb = append(b, 999)fmt.Println(a) // [1 2 3 4 5] — не тронутGotcha 3: Memory leak при slicing большого слайса
Заголовок раздела «Gotcha 3: Memory leak при slicing большого слайса»func firstTen(big []byte) []byte { return big[:10] // ⚠️ Backing array всего big остаётся жив!}Несмотря на то, что мы вернули только 10 байт, оригинальный массив (например, 1 ГБ) не будет освобождён GC, пока есть ссылка на наш slice.
✅ Решение: скопировать:
func firstTen(big []byte) []byte { res := make([]byte, 10) copy(res, big[:10]) return res}
// или Go 1.21+:res := slices.Clone(big[:10])Gotcha 4: range копирует значения
Заголовок раздела «Gotcha 4: range копирует значения»type Pt struct{ X, Y int }pts := []Pt{{1,2}, {3,4}, {5,6}}
for _, p := range pts { p.X = 0 // ИЗМЕНЯЕТ КОПИЮ!}fmt.Println(pts) // [{1 2} {3 4} {5 6}]
// Правильно:for i := range pts { pts[i].X = 0}⚠️ В Go 1.22 семантика loop variable изменилась (теперь каждая итерация — новая переменная), но значение всё равно копия.
Gotcha 5: append к nil — работает
Заголовок раздела «Gotcha 5: append к nil — работает»var s []int // nils = append(s, 1, 2)fmt.Println(s) // [1 2]fmt.Println(s == nil) // false (теперь — обычный slice)Это удобно: можно не инициализировать пустые слайсы.
Gotcha 6: make([]T, 0, cap) vs make([]T, len)
Заголовок раздела «Gotcha 6: make([]T, 0, cap) vs make([]T, len)»a := make([]int, 5) // [0 0 0 0 0], len=5, cap=5b := make([]int, 0, 5) // [], len=0, cap=5
a[0] = 1 // OK// b[0] = 1 // PANIC: out of rangeb = append(b, 1) // OK✅ Для preallocation под append — всегда make([]T, 0, N).
Gotcha 7: GC и cap
Заголовок раздела «Gotcha 7: GC и cap»Если slice имеет cap=1000, но len=1, GC всё равно держит весь backing array.
big := make([]byte, 1_000_000)small := big[:1] // cap = 1_000_000, GC держит всёbig = nil // ничего не освободилось, small держит ref
// Освободить:small = slices.Clone(small) // теперь small — независимая копияGotcha 8: Nil slice vs empty slice
Заголовок раздела «Gotcha 8: Nil slice vs empty slice»var a []int // nil, len=0, cap=0b := []int{} // не nil, len=0, cap=0c := make([]int, 0) // не nil, len=0, cap=0
a == nil // trueb == nil // falsec == nil // false
// Но: len, range, append — ведут себя одинаковоfor range a {} // 0 итерацийfor range b {} // 0 итераций⚠️ В JSON-сериализации различаются:
json.Marshal(a) // "null"json.Marshal(b) // "[]"Gotcha 9: Сравнение слайсов через == запрещено
Заголовок раздела «Gotcha 9: Сравнение слайсов через == запрещено»a := []int{1,2,3}b := []int{1,2,3}// a == b // COMPILE ERRORa == nil // OK
// Правильно:slices.Equal(a, b) // Go 1.21+reflect.DeepEqual(a, b)Gotcha 10: copy(dst, src) — что копируется
Заголовок раздела «Gotcha 10: copy(dst, src) — что копируется»copy возвращает min(len(dst), len(src)) элементов.
src := []int{1,2,3,4,5}dst := make([]int, 3)n := copy(dst, src)fmt.Println(n, dst) // 3 [1 2 3]
// Распространённая ошибка:var dst2 []int // len=0n2 := copy(dst2, src)fmt.Println(n2, dst2) // 0 [] — ничего не скопировалось!⚠️ copy НЕ растягивает dst. Нужно make([]T, len(src)).
Gotcha 11: range index переменная (Go 1.22+)
Заголовок раздела «Gotcha 11: range index переменная (Go 1.22+)»// До Go 1.22:var fns []func()for _, v := range []int{1, 2, 3} { fns = append(fns, func() { fmt.Println(v) })}for _, f := range fns { f() } // 3 3 3 (одна переменная)
// Go 1.22+:// Тот же код выведет: 1 2 3 (новая v на каждой итерации)Gotcha 12: append с переменным числом аргументов
Заголовок раздела «Gotcha 12: append с переменным числом аргументов»a := []int{1, 2, 3}b := []int{4, 5, 6}c := append(a, b...) // распаковка b
// Внимание: первый аргумент append не распаковывается// append(a..., b) // syntax errorGotcha 13: 2D slice — это не матрица
Заголовок раздела «Gotcha 13: 2D slice — это не матрица»// Самый простой способ — slice of slices:m := make([][]int, 3)for i := range m { m[i] = make([]int, 4)}// Память: 3 отдельные аллокации для рядовЭто не плоский массив. Если важна locality (CPU cache), используйте плоский:
const rows, cols = 3, 4data := make([]int, rows*cols)// доступ: data[i*cols + j]Gotcha 14: Возврат nil vs пустого slice из функций
Заголовок раздела «Gotcha 14: Возврат nil vs пустого slice из функций»// Стиль "Go way":func find() []int { return nil // OK — клиенты могут range и len}
// Не делайте:func find2() []int { return []int{} // лишняя аллокация}✅ Возвращайте nil для “ничего не найдено”. Range/len работают одинаково.
Gotcha 15: append может (но необязан) изменять оригинал
Заголовок раздела «Gotcha 15: append может (но необязан) изменять оригинал»a := make([]int, 3, 10)a = []int{1,2,3}b := append(a, 4)fmt.Println(a) // [1 2 3] (len=3)fmt.Println(b) // [1 2 3 4] (len=4)fmt.Println(a[:4]) // [1 2 3 4] — backing array общий!b и a делят backing array, но имеют разные len. a[:4] “оживляет” четвёртый элемент.
4. Производительность
Заголовок раздела «4. Производительность»4.1 Preallocation — главная оптимизация
Заголовок раздела «4.1 Preallocation — главная оптимизация»// ПЛОХО:var s []intfor i := 0; i < 1_000_000; i++ { s = append(s, i) // ~20 копирований за время роста}
// ХОРОШО:s := make([]int, 0, 1_000_000)for i := 0; i < 1_000_000; i++ { s = append(s, i) // 0 копирований}Выигрыш: 5-10x скорости, меньше нагрузка на GC.
4.2 Когда range копирует — это дорого
Заголовок раздела «4.2 Когда range копирует — это дорого»Если элементы — большие структуры, копирование в range v дорогое.
type Big struct { data [1024]byte }items := make([]Big, 1000)
// Дорого:for _, item := range items { _ = item}
// Дешевле:for i := range items { _ = items[i]}4.3 Copy быстрее, чем цикл
Заголовок раздела «4.3 Copy быстрее, чем цикл»copy оптимизирован через memmove. Никогда не пишите свой цикл копирования.
copy(dst, src) // быстро (memmove)// vsfor i, v := range src { dst[i] = v } // медленнее4.4 Pool для повторно используемых слайсов
Заголовок раздела «4.4 Pool для повторно используемых слайсов»var bufPool = sync.Pool{ New: func() any { return make([]byte, 0, 4096) },}
buf := bufPool.Get().([]byte)defer func() { buf = buf[:0] // сбросить len, сохранить cap bufPool.Put(buf)}()4.5 slices пакет (Go 1.21+)
Заголовок раздела «4.5 slices пакет (Go 1.21+)»Появился пакет slices со стандартными операциями:
import "slices"
slices.Contains(s, 5)slices.Index(s, 5)slices.Sort(s)slices.Reverse(s)slices.Clone(s)slices.Equal(a, b)slices.Concat(a, b, c)slices.Min(s)slices.Max(s)slices.BinarySearch(sortedS, 42)slices.DeleteFunc(s, func(x int) bool { return x < 0 })Все они оптимизированы через generics.
4.6 Growth pattern: считайте, сколько раз будет realloc
Заголовок раздела «4.6 Growth pattern: считайте, сколько раз будет realloc»// 1М append без preallocation:// аллокации на cap = 1, 2, 4, 8, ..., 524288, 1048576 → ~21 realloc4.7 Escape analysis для слайсов
Заголовок раздела «4.7 Escape analysis для слайсов»// Stack (если не escape):func sum() int { a := []int{1,2,3} // обычно escape на heap (slice — указатель) s := 0 for _, v := range a { s += v } return s}⚠️ Большинство слайсов escape на heap. Используйте -gcflags="-m" для проверки.
5. Типичные вопросы на собеседовании Junior
Заголовок раздела «5. Типичные вопросы на собеседовании Junior»Q1: Что такое slice в Go?
Заголовок раздела «Q1: Что такое slice в Go?»A: Slice — это header, состоящий из трёх полей: указатель на начало underlying array, длина (len) и capacity (cap). Slice не владеет данными — он ссылается на массив.
Q2: Что выведет?
Заголовок раздела «Q2: Что выведет?»s := []int{1, 2, 3}fmt.Println(len(s), cap(s))A: 3 3.
Q3: Что выведет?
Заголовок раздела «Q3: Что выведет?»s := make([]int, 3, 10)fmt.Println(len(s), cap(s))A: 3 10.
Q4: Разница len и cap?
Заголовок раздела «Q4: Разница len и cap?»A:
len— количество доступных черезs[i]элементов.cap— сколько можно добавить черезappendбез перевыделения.- Всегда
len <= cap.
Q5: Можно ли append к nil-slice?
Заголовок раздела «Q5: Можно ли append к nil-slice?»A: Да. var s []int; s = append(s, 1) — работает. Будет создан новый backing array.
Q6: Что выведет?
Заголовок раздела «Q6: Что выведет?»a := []int{1, 2, 3}b := a[:2]b = append(b, 99)fmt.Println(a)A: [1 2 99]. b имеет cap=3, append записал в существующий backing array, а a[2] показывает на тот же элемент.
Q7: Что выведет?
Заголовок раздела «Q7: Что выведет?»func modify(s []int) { s[0] = 100 s = append(s, 999)}
s := []int{1, 2, 3}modify(s)fmt.Println(s)A: [100 2 3]. s[0] = 100 поменял backing array (видно снаружи). append создал новый или нет — неважно, локальная s обновилась, внешняя — нет.
Q8: Как append растёт?
Заголовок раздела «Q8: Как append растёт?»A: Если len + n <= cap, просто пишет на месте. Иначе аллоцирует новый backing array размером growslice-формулы (примерно 2x для маленьких, 1.25x для больших >256), копирует старые элементы, добавляет новые. Возвращает новый slice header.
Q9: Что такое full slice expression?
Заголовок раздела «Q9: Что такое full slice expression?»A: s[low:high:max] — задаёт slice с len = high-low и cap = max-low. Используется для ограничения cap, чтобы append не мутировал соседние элементы оригинала.
Q10: Можно ли сравнить два slice через ==?
Заголовок раздела «Q10: Можно ли сравнить два slice через ==?»A: Нет, ошибка компиляции. Только с nil. Для поэлементного сравнения: slices.Equal() или reflect.DeepEqual().
Q11: Чему равен len и cap у nil-slice?
Заголовок раздела «Q11: Чему равен len и cap у nil-slice?»A: Оба 0. Можно range (0 итераций), len, append. Нельзя индексировать s[0] — panic.
Q12: Что такое memory leak в контексте slicing?
Заголовок раздела «Q12: Что такое memory leak в контексте slicing?»A: Когда big := make([]int, 1_000_000); small := big[:1] — backing array на 1М остаётся жив, пока есть ссылка на small. Решение: slices.Clone(big[:1]) или copy.
Q13: Почему for _, v := range s бывает медленным?
Заголовок раздела «Q13: Почему for _, v := range s бывает медленным?»A: Если v — большая структура, она копируется на каждой итерации. Решение: for i := range s { _ = s[i] }.
Q14: Что выведет?
Заголовок раздела «Q14: Что выведет?»s := make([]int, 0)s = append(s, 1, 2, 3)s2 := s[1:]s2[0] = 999fmt.Println(s)A: [1 999 3]. s2 указывает на тот же backing array.
Q15: Как корректно удалить элемент из slice?
Заголовок раздела «Q15: Как корректно удалить элемент из slice?»A:
// Удалить s[i]:s = append(s[:i], s[i+1:]...)// или с Go 1.21:s = slices.Delete(s, i, i+1)⚠️ Это сдвигает элементы. Для slices указателей — обнулите последний, иначе memory leak.
Q16: Что вернёт copy(dst, src)?
Заголовок раздела «Q16: Что вернёт copy(dst, src)?»A: Минимум len(dst) и len(src) — количество скопированных элементов. Если dst короче — копируется не весь src. Если dst пустой — 0.
Q17: nil-slice — это то же, что и []int{}?
Заголовок раздела «Q17: nil-slice — это то же, что и []int{}?»A: Семантически почти да: оба имеют len=0, оба работают с range и append. Но nil == []int{} — это true для nil-сравнения справа, и false для []int{}. В JSON: nil → null, []int{} → [].
Q18: Сколько памяти занимает slice header?
Заголовок раздела «Q18: Сколько памяти занимает slice header?»A: 24 байта на 64-битной системе (3 поля по 8 байт: ptr, len, cap).
Q19: Что произойдёт?
Заголовок раздела «Q19: Что произойдёт?»s := make([]int, 5)s[10] = 1A: Panic: runtime error: index out of range [10] with length 5.
Q20: Как создать 2D-slice?
Заголовок раздела «Q20: Как создать 2D-slice?»A:
m := make([][]int, rows)for i := range m { m[i] = make([]int, cols)}Или плоский для лучшего cache locality:
m := make([]int, rows*cols)Q21: Можно ли изменять slice во время range?
Заголовок раздела «Q21: Можно ли изменять slice во время range?»A: Можно (Go копирует header в начале range), но это опасно. Если append вызовет realloc, изменения “потеряются” после реаллокации.
Q22: Что такое slices пакет?
Заголовок раздела «Q22: Что такое slices пакет?»A: Стандартный пакет с Go 1.21 для работы со slices: Contains, Index, Sort, Equal, Clone, Delete, BinarySearch и др. Использует generics.
Q23: Какой growth factor у append?
Заголовок раздела «Q23: Какой growth factor у append?»A: До Go 1.18 — 2x для малых, потом 1.25x. С Go 1.18 — плавный переход, threshold ~256. Точные значения не гарантируются спецификацией.
Q24: Что выведет?
Заголовок раздела «Q24: Что выведет?»a := []int{1,2,3,4,5}b := a[1:4]fmt.Println(len(b), cap(b))A: 3 4. len = 4-1 = 3. cap = len(a) - 1 = 4 (от индекса 1 до конца underlying array).
Q25: Что выведет?
Заголовок раздела «Q25: Что выведет?»s := []int{1,2,3}s = append(s[:1], s[2:]...)fmt.Println(s)A: [1 3]. Удалили s[1]=2.
6. Practice — задачки на проверку
Заголовок раздела «6. Practice — задачки на проверку»Задача 1: Угадайте вывод
Заголовок раздела «Задача 1: Угадайте вывод»s := []int{1, 2, 3, 4, 5}slice1 := s[1:3] // [2 3]slice2 := s[2:5] // [3 4 5]
slice1 = append(slice1, 99)fmt.Println(s)fmt.Println(slice1)fmt.Println(slice2)Решение:
slice1имеетlen=2, cap=4(от индекса 1 до конца, 5-1=4).append(slice1, 99)— есть запас, пишет в backing array, в s[3].sстановится[1 2 3 99 5].slice1=[2 3 99].slice2— указывает на s[2:5] =[3 99 5]— изменилось!
Задача 2: Предотвращение memory leak
Заголовок раздела «Задача 2: Предотвращение memory leak»Реализуйте функцию, возвращающую первые N байт большого файла, не удерживая весь файл в памяти.
func FirstN(data []byte, n int) []byte { // ???}Решение:
func FirstN(data []byte, n int) []byte { if n > len(data) { n = len(data) } out := make([]byte, n) copy(out, data) return out}
// Или Go 1.21+:return slices.Clone(data[:n])Задача 3: Угадайте без запуска
Заголовок раздела «Задача 3: Угадайте без запуска»func main() { s := []int{1, 2, 3} s2 := append(s, 4) s3 := append(s, 5) fmt.Println(s2) fmt.Println(s3)}Решение:
sимеет cap=3.append(s, 4)— нужна расширение, аллоцируется новый array, cap станет ~6.s2 = [1 2 3 4].append(s, 5)— снова смотрит наs(len=3, cap=3 — старый), нужна расширение, аллоцируется ЕЩЁ ОДИН новый array.s3 = [1 2 3 5].
Подвох: если бы cap(s) > 3, то s2 и s3 использовали бы один и тот же backing array, и s2[3] стал бы 5 (последняя запись).
Задача 4: Удаление элемента
Заголовок раздела «Задача 4: Удаление элемента»Удалите элемент с индексом 2 из slice [10 20 30 40 50]. Покажите 2 способа.
Решение:
s := []int{10, 20, 30, 40, 50}
// Способ 1: с сохранением порядкаs = append(s[:2], s[3:]...)// [10 20 40 50]
// Способ 2: O(1), без сохранения порядкаs2 := []int{10, 20, 30, 40, 50}s2[2] = s2[len(s2)-1]s2 = s2[:len(s2)-1]// [10 20 50 40]
// Способ 3 (Go 1.21+):s3 := []int{10, 20, 30, 40, 50}s3 = slices.Delete(s3, 2, 3)Задача 5: Реализация stack
Заголовок раздела «Задача 5: Реализация stack»type Stack[T any] struct { data []T}
func (s *Stack[T]) Push(v T) { s.data = append(s.data, v) }func (s *Stack[T]) Pop() (T, bool) { var zero T if len(s.data) == 0 { return zero, false } n := len(s.data) - 1 v := s.data[n] s.data[n] = zero // !!! очищаем ref для GC s.data = s.data[:n] return v, true}⚠️ Обратите внимание на s.data[n] = zero. Если T — это указатель/slice/map, без зануления старая ссылка останется в backing array и GC не освободит её.
7. Источники и дополнительно
Заголовок раздела «7. Источники и дополнительно»- Go Blog — “Go Slices: usage and internals” — https://go.dev/blog/slices-intro — официальный гайд.
- Go Blog — “Arrays, slices: the mechanics of ‘append’” — https://go.dev/blog/slices — глубокое объяснение append.
- Dave Cheney — “How the Go runtime implements maps efficiently (without generics)” — есть и про slices.
- Russ Cox — “Go Data Structures” — https://research.swtch.com/godata
- runtime/slice.go — исходники: https://github.com/golang/go/blob/master/src/runtime/slice.go — функция
growslice. - Habr — “Слайсы в Go: подробно с примерами” (2024-2025) — поиск свежих обзоров.
- “100 Go Mistakes and How to Avoid Them” (Teiva Harsanyi) — раздел про слайсы — обязательное чтение.
- slices пакет — https://pkg.go.dev/slices — стандартная библиотека Go 1.21+.
Слайс — это вид на массив. Понимайте header, понимайте sharing — и большинство багов исчезает.