
Что такое slice в Go простыми словами

slice в Go, или слайс, это гибкая обёртка над массивом. Массив в Go имеет фиксированный размер, а слайс позволяет работать с последовательностью элементов удобнее: добавлять данные, передавать части массива, сортировать, копировать, фильтровать и собирать новые наборы.
Через slices в Go обычно:
- хранят данные
- собирают результаты запросов
- работают с JSON
- обрабатывают файлы
- делают очереди
- фильтруют элементы
- сортируют данные
- передают наборы значений между функциями
Тип слайса выглядит так:
[]T
Где T — тип элементов.
Например:
[]int
[]string
[]User
[]byte
[]map[string]any
Обычно в Go используют именно слайсы, а не массивы. Массивы нужны реже: когда размер строго фиксирован и является частью типа. В повседневной разработке почти всегда нужен список переменной длины, поэтому ты будешь видеть []int, []string, []User, а не [10]int.
Создание простого slice:
nums := []int{1, 2, 3}
Добавление элементов:
nums = append(nums, 4)
Результат:
[1 2 3 4]
На первый взгляд всё выглядит как обычный динамический массив, но внутри slices работают сильно интереснее. В Go slice — это не самостоятельный контейнер с данными. Он хранит:
- указатель на массив
- длину
- ёмкость
И именно из-за этого появляются почти все странные эффекты, которые получают новички:
- append неожиданно меняет другой slice
- данные внезапно общие между функциями
- маленький subslice удерживает огромный кусок памяти
- nil slice и пустой slice ведут себя по-разному в JSON
- конкурентный append приводит к race condition
Поэтому срезы в Go бесполезно просто учить. Если нормально не понять, как они устроены, и не попрактиковаться, потом начинаются странные баги в API, worker pool, очередях, SQL batch insert, protobuf и конкурентном коде.
- чем slice отличается от массива
- как работает append
- что такое len и cap
- как устроен slice внутри runtime
- как правильно копировать slices
- как удалять элементы
- как избежать лишних аллокаций
- почему бывают утечки памяти
- как slices ведут себя в goroutine
Плюс по пути посмотрим реальные production-кейсы и популярные вопросы с Go-собеседований.
Slice в Go — это не динамический массив
Главная ошибка почти всех новичков — воспринимать slice как самостоятельную структуру данных. На самом деле slice — это просто оболочка над массивом. Он не хранит элементы внутри себя, он хранит:
- указатель на массив
- длину
- ёмкость
И именно из-за этого появляются почти все неожиданные эффекты, которые разработчики ловят в Go.
s1 := []int{1, 2, 3}
s2 := s1[:2]
s2[0] = 100
fmt.Println(s1) // [100 2 3]
Многие ожидают, что s2 будет независимой копией, но копии нет. Оба slices смотрят на один и тот же массив.
Именно это делает slices одновременно:
- очень быстрыми;
- очень удобными;
- и иногда очень опасными.
Чем slice отличается от array в Go

Чтобы нормально понять слайсы, нужно быстро разобрать массивы. Массив в Go имеет фиксированную длину, и эта длина входит в тип:
var a [3]int
var b [4]int
[3]int и [4]int это разные типы. Их нельзя просто так присвоить друг другу.
var a [3]int
var b [4]int
// a = b // compile error
Слайс длину в типе не хранит:
var s []int
Сегодня в нём может быть 3 элемента, завтра 300, потом снова 0. Поэтому слайсы удобнее для большинства задач.
Коротко:
- массив [N]T хранит ровно N элементов, длина массива является частью типа и при передаче в функцию копируется массив целиком
- слайс []T хранит ссылку на участок массива и при передаче слайс копируется как маленький дескриптор, но смотрит на те же данные
Пример с массивом:
func changeArray(a [3]int) {
a[0] = 100
}
func main() {
arr := [3]int{1, 2, 3}
changeArray(arr)
fmt.Println(arr) // [1 2 3]
}
Массив скопировался, исходные данные не изменились.
Пример со слайсом:
func changeSlice(s []int) {
s[0] = 100
}
func main() {
nums := []int{1, 2, 3}
changeSlice(nums)
fmt.Println(nums) // [100 2 3]
}
Слайс передался по значению, но внутри у копии и оригинала общий базовый массив. Это один из главных моментов темы.
Как устроен slice под капотом

Слайс внутри можно представить как небольшую структуру из трёх частей:
type sliceHeader struct {
ptr *T
len int
cap int
}
В реальном runtime всё устроено сложнее, но для понимания хватает этой модели.
У слайса есть:
- указатель на первый доступный элемент базового массива
- длина len, сколько элементов сейчас видно через слайс
- ёмкость cap, сколько элементов можно использовать от текущей позиции до конца базового массива
Пример:
s := make([]int, 3, 6)
fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 6
fmt.Println(s) // [0 0 0]
Мы создали слайс длиной 3 и ёмкостью 6. Это значит, что доступны только первые 3 элемента, но в базовом массиве зарезервировано место под 6 элементов.
len = 3
cap = 6
[0 0 0 _ _ _]
^ доступная часть
К элементам внутри длины можно обращаться:
s[1] = 10
fmt.Println(s) // [0 10 0]
А вот обращение за пределы len, даже если внутри cap место есть, вызовет panic:
s[4] = 100 // panic: index out of range
Ёмкость не даёт права индексировать элементы. Она нужна для роста через append.
words := []string{"go", "map", "go", "slice", "map", "go"}
counts := make(map[string]int)
for _, word := range words {
counts[word]++
}
fmt.Println(counts) // map[go:3 map:2 slice:1]
Здесь работает важная особенность Go map: если ключа ещё нет, чтение вернёт zero value для типа значения. Для int это 0, поэтому counts[word]++ работает даже для нового слова.
Важно
len отвечает за доступные элементы. cap показывает запас базового массива. Индексировать можно только в пределах len, а не cap.
Как создать слайс в Go
Есть несколько нормальных способов создать слайс в Golang. Под разные задачи подходят разные варианты.
Через литерал
Когда данные известны сразу:
nums := []int{1, 2, 3}
words := []string{"go", "slice", "append"}
Это простой и читаемый вариант.
Через make slice в Go с длиной
Когда нужна конкретная длина:
s := make([]int, 5)
fmt.Println(s) // [0 0 0 0 0]
fmt.Println(len(s)) // 5
fmt.Println(cap(s)) // 5
Все элементы получают zero value своего типа. Для int это 0, для string пустая строка, для указателей nil.
Через make slice с длиной и ёмкостью
Когда ты хочешь сразу задать запас:
s := make([]int, 0, 100)
fmt.Println(len(s)) // 0
fmt.Println(cap(s)) // 100
Так часто делают, когда заранее известен примерный размер результата, но добавлять элементы удобнее через append.
Через var
Когда конечная длина неизвестна:
var s []int
s = append(s, 1)
Это nil slice. Он нормально работает с append, len, range.
var s []int
fmt.Println(s == nil) // true
fmt.Println(len(s)) // 0
s = append(s, 1)
fmt.Println(s) // [1]
len и cap в Go slice

Длина и ёмкость слайса постоянно всплывают в задачах и собеседованиях.
s := make([]int, 3, 5)
fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 5
len показывает, сколько элементов сейчас доступно. cap показывает, сколько элементов помещается в базовый массив от начала слайса до конца массива.
Посмотрим на нарезку:
s := []int{0, 1, 2, 3, 4, 5}
part := s[2:4]
fmt.Println(part) // [2 3]
fmt.Println(len(part)) // 2
fmt.Println(cap(part)) // 4
Почему cap(part) равен 4? Потому что part начинается с элемента s[2], а до конца базового массива остаётся 4 элемента: 2, 3, 4, 5.
Это влияет на append. Если у слайса есть запас ёмкости, append может записать новые элементы в тот же базовый массив. И иногда это меняет данные, которые ты не ожидал менять.
Как работает go slice append

append добавляет элементы в конец слайса и возвращает новый слайс.
s := []int{1, 2, 3}
s = append(s, 4)
fmt.Println(s) // [1 2 3 4]
Важно всегда сохранять результат:
s = append(s, 10)
Плохо:
append(s, 10) // compile error: value of append is not used
append может вернуть слайс, который ссылается на тот же массив, а может вернуть слайс уже с новым массивом. Это зависит от ёмкости.
Пример, когда хватает места:
s := make([]int, 0, 3)
s = append(s, 1)
s = append(s, 2)
fmt.Println(len(s)) // 2
fmt.Println(cap(s)) // 3
Новый массив не нужен.
Пример, когда места не хватает:
s := make([]int, 0, 2)
s = append(s, 1, 2)
s = append(s, 3)
fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // обычно больше 2
Когда ёмкость исчерпана, Go выделяет новый базовый массив, копирует туда старые элементы и добавляет новые.
Факт
Исторически часто говорили, что ёмкость слайса удваивается до 1024 элементов, а потом растёт примерно на 25%. В современных версиях Go формула роста сложнее и является деталью runtime. В прикладном коде лучше не завязываться на точный коэффициент роста, а заранее задавать capacity там, где размер результата понятен.
Почему append может изменить другой slice

Вот место, где многие впервые начинают уважать слайсы.
s1 := []int{1, 2, 3}
s2 := s1[1:2]
s3 := append(s2, 10)
fmt.Println(s1) // [1 2 10]
fmt.Println(s2) // [2]
fmt.Println(s3) // [2 10]
На первый взгляд странно. Мы же добавляли в s2, почему изменился s1?
Потому что s1, s2 и s3 смотрят на один базовый массив. У s2 длина 1, но ёмкость больше. append увидел свободное место и записал 10 в общий массив.
Такая же проблема может появиться при передаче слайса в функцию:
func addSomething(s []int) {
_ = append(s, 10)
}
func main() {
nums := []int{1, 2, 3}
addSomething(nums[:2])
fmt.Println(nums) // [1 2 10]
}
Функция вроде получила только первые два элемента, но смогла изменить третий через общий базовый массив.
Как защититься полным выражением слайса
В Go есть полное выражение слайса:
s[low:high:max]
Оно позволяет ограничить capacity нового слайса.
func addSomething(s []int) {
_ = append(s, 10)
}
func main() {
nums := []int{1, 2, 3}
addSomething(nums[:2:2])
fmt.Println(nums) // [1 2 3]
}
nums[:2] даёт длину 2 и ёмкость 3.
nums[:2:2] даёт длину 2 и ёмкость 2.
Когда функция вызывает append, свободной ёмкости уже нет, поэтому Go создаёт новый массив, а исходный nums не меняется.
Важно
Полное выражение слайса защищает от побочного изменения через append, но не решает проблему удержания большого базового массива в памяти. Для этого нужна копия.
go slice copy: как правильно копировать слайсы
Для копирования слайсов есть встроенная функция copy. Она нужна, когда тебе нужна именно независимая копия данных, а не ещё один slice, который смотрит на тот же базовый массив.
Сигнатура по смыслу такая:
copy(dst, src)
Первый аргумент — куда копируем. Второй — откуда.
Частая ошибка:
src := []int{1, 2, 3}
var dst []int
copy(dst, src)
fmt.Println(dst) // []
Почему ничего не скопировалось? Потому что dst имеет длину 0. copy копирует минимум из len(dst) и len(src).
Правильно:
src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src)
fmt.Println(dst) // [1 2 3]
Можно скопировать часть:
src := []int{1, 2, 3, 4}
dst := make([]int, 2)
n := copy(dst, src)
fmt.Println(dst) // [1 2]
fmt.Println(n) // 2
copy возвращает количество скопированных элементов.
Альтернатива через append:
dst := append([]int(nil), src...)
Так тоже можно. Но в обучающем и командном коде make + copy обычно читается понятнее.

nil slice и empty slice: в чём разница

В Go есть две похожие, но не одинаковые ситуации.
Nil slice:
var s []string
Пустой, но не nil slice:
s := []string{}
Или:
s := make([]string, 0)
Проверим:
var a []string
b := []string{}
c := make([]string, 0)
fmt.Println(a == nil) // true
fmt.Println(b == nil) // false
fmt.Println(c == nil) // false
fmt.Println(len(a)) // 0
fmt.Println(len(b)) // 0
fmt.Println(len(c)) // 0
Для большинства операций они ведут себя одинаково:
var s []int
fmt.Println(len(s)) // 0
for _, v := range s {
fmt.Println(v) // не выполнится
}
s = append(s, 1)
fmt.Println(s) // [1]
Разница становится заметна на границах системы. Например, в JSONДля API это может быть важно. Одни клиенты ждут [], другие нормально обрабатывают null, а третьи ломаются от неожиданного формата.
type Response struct {
Items []string `json:"items"`
}
var nilSlice []string
emptySlice := []string{}
b1, _ := json.Marshal(Response{Items: nilSlice})
b2, _ := json.Marshal(Response{Items: emptySlice})
fmt.Println(string(b1)) // {"items":null}
fmt.Println(string(b2)) // {"items":[]}
Для API это может быть важно. Одни клиенты ждут [], другие нормально обрабатывают null, а третьи ломаются от неожиданного формата.
Практика: если ты просто собираешь результат в коде, var s []T нормален. Если отдаёшь JSON и хочешь именно пустой массив, инициализируй []T{} или make([]T, 0).
Как проверять, что slice пустой
Лучший способ проверить, есть ли элементы:
if len(s) == 0 {
// пусто
}
Не надо проверять только s == nil, если тебе важно именно отсутствие элементов.
Плохой пример:
func handle(items []Item) {
if items != nil {
process(items)
}
}
Если функция получит пустой, но не nil slice, условие выполнится:
items := []Item{}
handle(items)
Правильнее:
if len(items) > 0 {
process(items)
}
Это одинаково работает и для nil slice, и для empty slice.
Та же логика полезна и для map: если нужно понять, есть ли элементы, обычно проверяй len(m), а не m != nil. Подробнее про map можно прочитать в статье «Map в Go: как работает мапа Golang».
if len(s) == 0 {
// пусто
}
Нарезка slice: s[a:b] и s[a:b:c]
![Нарезка slice s[a:b:c]](/img/blog/golang-slice/9.webp)
Нарезка создаёт новый слайс из существующего массива или слайса.
s := []int{0, 1, 2, 3, 4}
part := s[1:4]
fmt.Println(part) // [1 2 3]
Границы работают так:
s[low:high]
low включается, high не включается.
s := []string{"a", "b", "c", "d"}
fmt.Println(s[:2]) // [a b]
fmt.Println(s[2:]) // [c d]
fmt.Println(s[:]) // [a b c d]
Нарезка не копирует данные. Новый слайс смотрит на тот же базовый массив.
s := []int{1, 2, 3}
part := s[1:]
part[0] = 100
fmt.Println(s) // [1 100 3]
fmt.Println(part) // [100 3]
Полное выражение слайса:
part := s[1:3:3]
Формула ёмкости:
cap = max - low
Это редко нужно в простом коде, но очень полезно, когда ты передаёшь часть слайса в функцию и не хочешь, чтобы она через append затронула исходный массив.
Удаление элемента из слайса Golang

В Go нет встроенной функции delete для slice, как для map. Поэтому новички постоянно ищут delete slice, slice remove и не знают, как удалить элемент из слайса golang.
Способ зависит от того, важен ли порядок.
Удалить с сохранением порядка
func removeAt(s []int, i int) []int {
return append(s[:i], s[i+1:]...)
}
Пример:
nums := []int{10, 20, 30, 40}
nums = removeAt(nums, 1)
fmt.Println(nums) // [10 30 40]
Порядок сохранился, но элементы после i были сдвинуты.
Удалить без сохранения порядка
Если порядок не важен, можно быстрее:
func removeAtUnordered(s []int, i int) []int {
s[i] = s[len(s)-1]
return s[:len(s)-1]
}
Пример:
nums := []int{10, 20, 30, 40}
nums = removeAtUnordered(nums, 1)
fmt.Println(nums) // [10 40 30]
Такой вариант часто используют в игровых системах, очередях задач, внутренних буферах, где порядок не важен.
Важный нюанс с указателями
Если слайс хранит указатели или большие структуры с указателями, при удалении лучше занулять удаляемый элемент, чтобы GC мог освободить память.
func removePtr(s []*User, i int) []*User {
copy(s[i:], s[i+1:])
s[len(s)-1] = nil
return s[:len(s)-1]
}
Без s[len(s)-1] = nil последний элемент может продолжать удерживать объект в памяти через базовый массив.

Важно
Для слайсов с указателями удаление через append(s[:i], s[i+1:]...) может оставить ссылку в хвосте базового массива. В долгоживущих структурах это превращается в неприятные утечки.
golang slice contains: как проверить наличие элемента
В Go долго не было универсальной встроенной функции contains для слайсов, поэтому часто писали цикл.
func containsInt(s []int, target int) bool {
for _, v := range s {
if v == target {
return true
}
}
return false
}
Для строк:
func containsString(s []string, target string) bool {
for _, v := range s {
if v == target {
return true
}
}
return false
}
В современных версиях Go можно использовать пакет slices из стандартной библиотеки:
import "slices"
ok := slices.Contains([]int{1, 2, 3}, 2)
fmt.Println(ok) // true
Если проверок много, иногда лучше построить set через map:
ids := []int{10, 20, 30}
set := make(map[int]struct{}, len(ids))
for _, id := range ids {
set[id] = struct{}{}
}
_, ok := set[20]
Один поиск по slice — нормально. Тысячи поисков по большому slice — уже повод подумать о map.
golang sort slice: как сортировать слайсы
Для сортировки в Go есть пакет sort, а в новых версиях ещё и пакет slices.
Сортировка []int через sort:
nums := []int{3, 1, 2}
sort.Ints(nums)
fmt.Println(nums) // [1 2 3]
Сортировка []string:
words := []string{"go", "append", "slice"}
sort.Strings(words)
fmt.Println(words) // [append go slice]
Сортировка структур:
type User struct {
Name string
Age int
}
users := []User{
{Name: "Mark", Age: 24},
{Name: "Niyaz", Age: 30},
}
sort.Slice(users, func(i, j int) bool {
return users[i].Age < users[j].Age
})
Через пакет slices:
slices.Sort(nums)
Для структур:
slices.SortFunc(users, func(a, b User) int {
return cmp.Compare(a.Age, b.Age)
})
Запросы go sort slice, golang sort slice обычно закрываются этими вариантами.
go slice string и golang slice string
Здесь есть два разных смысла.
Первый: slice строк.
words := []string{"go", "slice", "map"}
Второй: преобразование slice в string, чаще всего []byte в string.
b := []byte{'g', 'o'}
s := string(b)
fmt.Println(s) // go
Обратное преобразование:
s := "golang"
b := []byte(s)
fmt.Println(b)
Важно понимать, что строка в Go неизменяемая. Когда ты превращаешь string в []byte, получаешь изменяемую копию данных.
s := "go"
b := []byte(s)
b[0] = 'G'
fmt.Println(string(b)) // Go
fmt.Println(s) // go
Для Unicode-строк часто нужен []rune, а не []byte.
s := "привет"
runes := []rune(s)
fmt.Println(len(s)) // байты
fmt.Println(len(runes)) // символы
Это важный момент для задач на строки, особенно если в данных есть кириллица.
golang map to slice: как собрать slice из map

Map и slice часто используют вместе. Map даёт быстрый доступ по ключу, slice удобен для порядка, сортировки и вывода.
Например, собрать ключи map в slice:
m := map[string]int{
"go": 3,
"slice": 2,
"map": 1,
}
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
fmt.Println(keys)
Собрать значения:
values := make([]int, 0, len(m))
for _, v := range m {
values = append(values, v)
}
Собрать пары:
type Pair struct {
Key string
Value int
}
pairs := make([]Pair, 0, len(m))
for k, v := range m {
pairs = append(pairs, Pair{Key: k, Value: v})
}
Если ты не читал разбор map, лучше вернуться к нему отдельно как работает мапа Golang. Там подробно разобраны ключи, значения, range, delete, sync.Map и внутреннее устройство.
Как скопировать map, slice и interface в Go
Запрос как скопировать map slice interface часто появляется из-за одной проблемы: в Go много ссылочных структур, и простое присваивание не всегда делает независимую копию данных.
Со slice:
src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src)
С map:
src := map[string]int{"a": 1}
dst := make(map[string]int, len(src))
for k, v := range src {
dst[k] = v
}
Если внутри map лежат slice, нужно копировать и их:
src := map[string][]int{
"a": {1, 2, 3},
}
dst := make(map[string][]int, len(src))
for k, v := range src {
copied := make([]int, len(v))
copy(copied, v)
dst[k] = copied
}
С interface{} или any универсального простого ответа нет. Надо знать реальный тип внутри. Если там слайс, копируешь как слайс. Если map, как map. Если структура с указателями, нужно решать, нужна поверхностная или глубокая копия.
Практика: в Go лучше не пытаться сделать deep copy на все случаи жизни. Обычно безопаснее явно копировать конкретные структуры данных, которые используются в твоём коде.
Производительность: как правильно инициализировать slice

Представим, у нас есть слайс []Foo, и мы хотим получить []Bar такой же длины.
Плохой вариант для большого количества данных:
func convert(foos []Foo) []Bar {
bars := make([]Bar, 0)
for _, foo := range foos {
bars = append(bars, fooToBar(foo))
}
return bars
}
Код рабочий, но для большого входа Go будет несколько раз расширять базовый массив, выделять новую память и копировать элементы.
Лучше задать ёмкость:
func convert(foos []Foo) []Bar {
bars := make([]Bar, 0, len(foos))
for _, foo := range foos {
bars = append(bars, fooToBar(foo))
}
return bars
}
Ещё быстрее, если длина результата точно равна длине входа:
func convert(foos []Foo) []Bar {
bars := make([]Bar, len(foos))
for i, foo := range foos {
bars[i] = fooToBar(foo)
}
return bars
}
Почему второй вариант иногда всё равно выбирают? Потому что append часто проще читать, особенно когда на один входной элемент приходится несколько выходных.
Пример:
func collectKeys(items []Item) []string {
keys := make([]string, 0, len(items)*2)
for _, item := range items {
keys = append(keys, item.StartKey, item.EndKey)
}
return keys
}
Через индексы это будет быстрее на микробенчмарке, но хуже для чтения:
func collectKeys(items []Item) []string {
keys := make([]string, len(items)*2)
for i, item := range items {
keys[i*2] = item.StartKey
keys[i*2+1] = item.EndKey
}
return keys
}
В реальном коде важен баланс. Если участок не горячий, читаемость часто важнее экономии нескольких процентов.
Правило
Если длина результата известна и ты заполняешь каждый элемент один к одному, используй make([]T, len(src)) и запись по индексу. Если количество элементов зависит от условий или на один вход приходится переменное число выходов, используй make([]T, 0, expected) и append.
Утечка памяти из-за нарезки большого slice

Одна из самых неприятных ловушек со слайсами: маленький слайс может удерживать в памяти огромный массив.
Представим, что мы читаем сообщение размером 1 МБ, а сохранить хотим только первые 5 байт.
func getMessageType(msg []byte) []byte {
return msg[:5]
}
Выглядит нормально. Возвращаем всего 5 байт.
Но msg[:5] не копирует данные. Новый слайс продолжает ссылаться на базовый массив размером 1 МБ. Если сохранить 1000 таких маленьких слайсов, можно случайно удерживать около 1 ГБ памяти, хотя полезных данных всего 5000 байт.
Правильно делать копию:
func getMessageType(msg []byte) []byte {
msgType := make([]byte, 5)
copy(msgType, msg[:5])
return msgType
}
Или компактно:
func getMessageType(msg []byte) []byte {
return append([]byte(nil), msg[:5]...)
}
Полное выражение слайса не решит эту проблему:
return msg[:5:5]
Оно ограничит capacity, но базовый массив всё равно останется в памяти, пока на него есть ссылка через слайс.
Это особенно важно в коде, который работает с файлами, сетевыми буферами, бинарными протоколами, логами и большими JSON.
Утечки памяти со слайсами указателей
Есть ещё один тип утечек: когда слайс содержит указатели или структуры с полями-указателями.
type Foo struct {
Data []byte
}
Допустим, у нас есть 1000 элементов, и каждый держит по 1 МБ.
func keepFirstTwo(foos []Foo) []Foo {
return foos[:2]
}
Мы оставили только два элемента, но базовый массив всё ещё содержит остальные структуры, а внутри них ссылки на большие []byte. GC видит эти ссылки и не освобождает память.
Если сохраняем маленькую часть, лучше копировать:
func keepFirstTwo(foos []Foo) []Foo {
res := make([]Foo, 2)
copy(res, foos[:2])
return res
}
Если удаляем небольшую часть, а большую оставляем, можно обнулять ненужные ссылки:
func keepFirstTwo(foos []Foo) []Foo {
for i := 2; i < len(foos); i++ {
foos[i].Data = nil
}
return foos[:2]
}
Как выбирать:
- оставляешь маленькую часть большого слайса — копируй
- оставляешь почти всё, но удаляешь несколько элементов — обнуляй ссылки
- работаешь с []byte из большого буфера — копируй нужный кусок
- держишь слайсы долго в памяти — особенно внимательно смотри на базовые массивы
Такие вещи редко всплывают в учебных задачах, зато легко появляются в проде, когда сервис долго живёт и обрабатывает много данных.
Slice в функциях: что меняется, а что нет
Слайс передаётся в функцию по значению. Копируется не весь массив, а только дескриптор: pointer, len, cap.
Изменение элемента видно снаружи:
func change(s []int) {
s[0] = 100
}
func main() {
nums := []int{1, 2, 3}
change(nums)
fmt.Println(nums) // [100 2 3]
}
А изменение самого дескриптора не видно, если ты не вернул новый слайс:
func add(s []int) {
s = append(s, 4)
}
func main() {
nums := []int{1, 2, 3}
add(nums)
fmt.Println(nums) // [1 2 3]
}
Правильно:
func add(s []int) []int {
return append(s, 4)
}
func main() {
nums := []int{1, 2, 3}
nums = add(nums)
fmt.Println(nums) // [1 2 3 4]
}
Это частая ошибка новичков: функция вызывает append, но результат не возвращает.
Фильтрация slice без лишних аллокаций
Фильтрация часто пишется так:
func filterPositive(nums []int) []int {
res := make([]int, 0, len(nums))
for _, n := range nums {
if n > 0 {
res = append(res, n)
}
}
return res
}
Это безопасно и понятно.
Если можно менять исходный слайс, можно фильтровать на месте:
func filterPositiveInPlace(nums []int) []int {
res := nums[:0]
for _, n := range nums {
if n > 0 {
res = append(res, n)
}
}
return res
}
nums[:0] создаёт слайс нулевой длины, но с той же ёмкостью. Мы переиспользуем старый массив и не выделяем новый.
Но такой подход меняет исходные данные. Его нельзя использовать, если вызывающий код ожидает, что старый slice останется нетронутым.
Практика: in-place фильтрация хороша для внутренних буферов и hot path. Для публичных API и кода, где важна предсказуемость, лучше вернуть новый слайс.
Слайсы и конкурентность в Go

Сам slice не является синхронизированной структурой. Если несколько goroutine одновременно пишут в один и тот же slice или делают append без защиты, будет гонка.
Опасный код:
var nums []int
for i := 0; i < 10; i++ {
go func(i int) {
nums = append(nums, i)
}(i)
}
Здесь гонка сразу в двух местах: меняется дескриптор слайса и общий базовый массив.
Правильно через mutex:
var (
mu sync.Mutex
nums []int
)
for i := 0; i < 10; i++ {
go func(i int) {
mu.Lock()
defer mu.Unlock()
nums = append(nums, i)
}(i)
}
Или через канал, если это часть pipeline:
ch := make(chan int)
for i := 0; i < 10; i++ {
go func(i int) {
ch <- i
}(i)
}
Паттерны параллельной обработки через каналы и goroutine лучше отдельно смотреть в статье Fan-in / Fan-out в Go. Там как раз разбирается, как безопасно раздавать задачи воркерам и собирать результаты.
Частые ошибки со слайсами в Go
Путать len и cap
s := make([]int, 3, 10)
s[5] = 1 // panic
Можно обращаться только к индексам меньше len(s).
Не сохранять результат append
append(s, 1) // нельзя
Нужно:
s = append(s, 1)
Думать, что нарезка копирует данные
part := s[:2]
part[0] = 100
Изменится общий базовый массив.
Копировать в nil destination
var dst []int
copy(dst, src) // скопирует 0 элементов
Нужно:
dst := make([]int, len(src))
copy(dst, src)
Проверять пустоту через nil
if s != nil {
// не значит, что есть элементы
}
Лучше:
if len(s) > 0 {
// есть элементы
}
Удерживать большой массив маленьким слайсом
return bigData[:5]
Если слайс живёт долго, лучше скопировать нужные данные.
Что спрашивают про slices на собеседовании по Go
На собеседовании по Golang слайсы спрашивают очень часто. И обычно не потому, что хотят услышать синтаксис. Проверяют понимание памяти.
Частые вопросы:
- Что такое slice в Go?
- Чем slice отличается от array?
- Что такое len и cap?
- Как устроен slice под капотом?
- Что делает append?
- Когда append создаёт новый массив?
- Почему append может изменить исходный slice?
- Как работает copy?
- Чем nil slice отличается от empty slice?
- Как удалить элемент из slice?
- Как проверить, содержит ли slice значение?
- Как отсортировать slice?
- Почему маленький slice может удерживать большой массив?
- Что такое full slice expression?
- Безопасно ли делать append в нескольких goroutine?
Если человек отвечает на эти вопросы уверенно и с примерами, обычно видно, что он не просто проходил тему по учебнику, а реально писал код.
Где слайсы встречаются в backend-разработке
В реальной Go-разработке slices появляются постоянно.
В HTTP API:
type Response struct {
Items []Item `json:"items"`
}
В работе с базой:
ids := make([]int64, 0, len(rows))
В батчевой обработке:
batch := make([]Event, 0, 1000)
В очередях, буферах и воркерах:
tasks := []Task{}
В алгоритмах:
stack := []Node{root}
В строковых задачах:
runes := []rune(text)
Поэтому слайсы нельзя выучить как одну маленькую тему и забыть. Они лежат в основе огромного количества Go-кода.
Если ты сейчас собираешь системный план изучения Go, посмотри ещё Golang Roadmap 2026: путь Go-разработчика за 18 недель. Там слайсы идут в ранней части роадмапа, потому что без них дальше тяжело понимать алгоритмы, структуры данных, JSON, SQL и конкурентность.
Мини-шпаргалка по slices в Golang

Создать slice:
s := []int{1, 2, 3}
Создать через make:
s := make([]int, 0, 100)
Добавить элемент:
s = append(s, 10)
Добавить другой slice:
s = append(s, other...)
Скопировать:
dst := make([]int, len(src))
copy(dst, src)
Удалить с сохранением порядка:
s = append(s[:i], s[i+1:]...)
Удалить без сохранения порядка:
s[i] = s[len(s)-1]
s = s[:len(s)-1]
Проверить пустоту:
len(s) == 0
Проверить наличие элемента:
slices.Contains(s, x)
Отсортировать:
slices.Sort(s)
Сделать строку из []byte:
str := string(bytes)
Сделать []byte из строки:
bytes := []byte(str)
Итог
Слайсы в Go — одна из базовых тем, на которой держится почти весь прикладной код. Но базовая не значит простая.
На уровне синтаксиса нужно знать make, append, copy, len, cap, range, сортировку и удаление элементов. На уровне реальной разработки важно понимать другое: слайс смотрит на базовый массив, нарезка не копирует данные, append может переиспользовать ёмкость, маленький слайс может удерживать большой массив, а nil и empty slice иногда дают разный результат на границах системы.
Главное, что стоит забрать из статьи:
- slice это дескриптор над массивом, а не сам массив
- len и cap отвечают за разные вещи
- append всегда возвращает новый slice header, его нужно сохранять
- если capacity хватает, append пишет в старый базовый массив
- нарезка не копирует данные
- для настоящей копии используй copy или append([]T(nil), src...)
- пустоту проверяй через len(s), а не через s != nil
- для больших буферов копируй нужный кусок, если он будет жить долго
- при удалении указателей не забывай про обнуление хвоста
- конкурентный append без синхронизации небезопасен
Если ты готовишься к собеседованию по Go, слайсы нужно уметь объяснить на примерах. Не просто сказать, что это динамический массив, а показать, как ведут себя len, cap, append, copy и базовый массив. Именно по этим деталям чаще всего отличают человека, который писал Go руками, от того, кто просто посмотрел пару роликов.
Для закрепления рядом стоит прочитать ещё три темы:
- Zero Values в Go, потому что nil slice напрямую связан с zero value
- Map в Go, потому что map и slice часто работают вместе
- Fan-in / Fan-out в Go, чтобы понимать, как безопасно использовать slices в конкурентной обработке результатов.


