Слайс API в Go - структура среза операции и подводные камни

05 января 2026
Автор

Олег Марков

Введение

Срезы в Go — это один из ключевых инструментов работы с коллекциями данных. На практике почти любой не тривиальный код опирается на них. Смотрите, здесь важно понимать не только базовый синтаксис, но и то, как устроен Слайс API (часто говорят api-slice) — то есть совокупность операций и функций, с помощью которых вы управляете срезами.

Когда вы вызываете append, берете поддиапазон a[2:5], копируете данные copy(dst, src) или передаете срез в функцию, вы фактически используете этот набор возможностей. Если не понимать, как они работают "под капотом", легко получить неожиданные эффекты: лишние аллокации, утечки памяти, влияние одного среза на другой, гонки данных.

В этой статье вы увидите:

  • как устроен срез внутри;
  • какие операции входят в условный Слайс API;
  • как работает append и почему иногда он меняет capacity;
  • как правильно использовать copy и срезы-поддиапазоны;
  • как избегать типичных ошибок при работе со срезами.

Давайте разберемся на последовательных примерах и постепенно соберем целостную картину.

Внутреннее устройство среза

Логическая модель среза

Срез в Go — это не просто динамический массив, а описатель (дескриптор) последовательности элементов. В памяти срез представлен как структура из трех полей:

  • указатель на первый элемент;
  • длина (len);
  • емкость (cap).

В языке эта структура не видна напрямую, но компилятор и рантайм работают именно с такой моделью.

Условно это выглядит так:

// Псевдоструктура - так срез устроен в рантайме
type SliceHeader struct {
    Data uintptr // Указатель на первый элемент в массиве
    Len  int     // Текущее количество элементов
    Cap  int     // Максимальное возможное количество элементов без реаллокации
}

Вы не должны создавать SliceHeader вручную для обычного кода. Но понимание его структуры сильно помогает при анализе поведения append, copy и поддиапазонов.

Базовые свойства len и cap

Давайте посмотрим простой пример, чтобы вы почувствовали разницу между длиной и емкостью:

package main

import "fmt"

func main() {
    // Здесь мы создаем срез из массива литералом
    nums := []int{1, 2, 3}

    fmt.Println(len(nums)) // 3 - длина среза
    fmt.Println(cap(nums)) // 3 - емкость совпадает с длиной
}
  • len — сколько элементов действительно доступно для чтения и записи по индексу.
  • cap — сколько элементов можно добавить (через append), прежде чем рантайм выделит новый массив.

Теперь давайте создадим срез другой длины и емкости:

package main

import "fmt"

func main() {
    // Здесь мы выделяем срез через make с длиной 2 и емкостью 5
    nums := make([]int, 2, 5)

    fmt.Println(len(nums)) // 2 - доступно 2 элемента
    fmt.Println(cap(nums)) // 5 - в массиве есть место для 5 элементов

    // Здесь мы добавляем новые элементы через append
    nums = append(nums, 10, 20)

    fmt.Println(len(nums)) // 4 - длина выросла
    fmt.Println(cap(nums)) // 5 - емкость пока прежняя
}

Обратите внимание: в примере выше append не выделяет новый массив, потому что емкость позволяет разместить дополнительные элементы.

Срез и базовый массив

Срез всегда "сидит" на каком-то базовом массиве. Этот массив может быть:

  • создан как часть литерала []int{1,2,3};
  • выделен через make([]T, len, cap);
  • получен из массива arr[:].

Вы не управляете этим массивом напрямую — все делается через срез, но важно помнить, что:

  • несколько срезов могут указывать на один и тот же базовый массив;
  • изменение элемента через один срез отражается в других, если они разделяют память.

Пример:

package main

import "fmt"

func main() {
    // Здесь мы создаем базовый массив фиксированной длины
    arr := [5]int{1, 2, 3, 4, 5}

    // Здесь мы берем срез массива - все элементы
    s1 := arr[:]

    // Здесь мы берем поддиапазон - элементы с индексами 1,2,3
    s2 := arr[1:4]

    // Изменяем элемент через второй срез
    s2[0] = 20 // Это меняет arr[1]

    fmt.Println(arr) // [1 20 3 4 5] - массив изменен
    fmt.Println(s1)  // [1 20 3 4 5] - срез s1 тоже "видит" изменения
}

Как видите, изменение через один срез затрагивает общий базовый массив.

Создание и инициализация срезов

Литералы срезов

Самый простой способ создать срез — использовать литерал:

// Здесь мы создаем срез целых чисел с тремя элементами
nums := []int{1, 2, 3}

// Здесь мы создаем срез строк
names := []string{"Alice", "Bob", "Charlie"}

Литерал среза одновременно создает базовый массив и срез, указывающий на него.

Функция make

Функция make — ключевой элемент Слайс API. Через нее вы создаете "пустой" срез определенной длины и емкости.

Синтаксис:

// Здесь мы создаем срез типа T с длиной len и емкостью cap
s := make([]T, len, cap)

Если емкость не указана, она равна длине:

// Здесь емкость будет равна 3
s := make([]int, 3)

Пример с комментариями:

package main

import "fmt"

func main() {
    // Здесь мы создаем срез длиной 0 и емкостью 5
    buf := make([]byte, 0, 5)

    fmt.Println(len(buf)) // 0 - в срезе нет логических элементов
    fmt.Println(cap(buf)) // 5 - но место в массиве уже выделено
}

Такой подход часто используют для снижения количества аллокаций, если вы заранее знаете примерный объем данных.

Срезы из массивов

Можно создать срез на основе массива:

package main

import "fmt"

func main() {
    // Здесь мы создаем массив из 4 элементов
    arr := [4]int{10, 20, 30, 40}

    // Здесь мы берем срез со всеми элементами массива
    s1 := arr[:]

    // Здесь мы берем срез с элементами с индексами 1 и 2
    s2 := arr[1:3]

    fmt.Println(s1) // [10 20 30 40]
    fmt.Println(s2) // [20 30]
}

Смотрите, теперь любые изменения через s1 или s2 будут отражаться в arr, пока новые append не приведут к выделению нового массива.

Поддиапазоны и операции над диапазонами

Базовый срез диапазона

Вы уже видели выражения вида a[i:j]. Это ключевая часть api-slice — возможность быстро создавать представление подмножества данных.

Правило индексов:

  • i — начальный индекс (включительно);
  • j — конечный индекс (исключительно);
  • длина результата = j - i;
  • выражение валидно, если 0 <= i <= j <= cap(a) и j <= len(a) для обычного среза.

Пример:

package main

import "fmt"

func main() {
    // Здесь мы создаем срез из 5 элементов
    s := []int{0, 1, 2, 3, 4}

    // Здесь мы берем поддиапазон с элементами с индексами 1,2,3
    sub := s[1:4]

    fmt.Println(sub)      // [1 2 3]
    fmt.Println(len(sub)) // 3
    fmt.Println(cap(sub)) // 4 - от s[1] до конца базового массива
}

Трехиндексный срез

Go позволяет задать емкость явно, используя трехиндексную форму: a[i:j:k], где:

  • i — начало;
  • j — конец (длина);
  • k — ограничение емкости;
  • длина = j - i;
  • емкость = k - i.

Это тоже часть Слайс API, хотя используется не так часто. Она полезна, если вы хотите "обрезать" доступную емкость и избежать влияния append на соседние данные.

Пример:

package main

import "fmt"

func main() {
    // Здесь мы создаем исходный срез
    s := []int{0, 1, 2, 3, 4}

    // Здесь мы берем поддиапазон с ограниченной емкостью
    sub := s[1:3:3]

    fmt.Println(sub)      // [1 2]
    fmt.Println(len(sub)) // 2
    fmt.Println(cap(sub)) // 2 - емкость ограничена

    // Здесь мы добавляем элемент в sub
    sub = append(sub, 99)

    fmt.Println(sub) // [1 2 99]

    // Обратите внимание - базовый срез s не изменился в части элементов
    fmt.Println(s) // [0 1 2 3 4]
}

Здесь я использую трехиндексную форму, чтобы append провоцировал выделение нового массива и не "портил" исходный срез s.

Если бы мы взяли просто sub := s[1:3], емкость бы была больше, и append модифицировал бы элементы s.

Функция append и расширение среза

Общий принцип работы append

Функция append — центральный элемент Слайс API. Она позволяет добавлять элементы в срез:

// Базовый синтаксис
s = append(s, x)        // добавить один элемент
s = append(s, x, y, z)  // добавить несколько элементов
s = append(s, other...) // добавить все элементы другого среза

Важно: append всегда возвращает новый срез. Иногда этот новый срез использует тот же базовый массив, иногда — новый. Поэтому вы почти всегда должны присваивать результат обратно.

Пример:

package main

import "fmt"

func main() {
    // Здесь мы создаем срез с емкостью 3
    s := make([]int, 0, 3)

    // Здесь мы добавляем элементы
    s = append(s, 1)
    s = append(s, 2, 3)

    fmt.Println(s)      // [1 2 3]
    fmt.Println(len(s)) // 3
    fmt.Println(cap(s)) // 3

    // Здесь мы добавляем еще элементы, превышая емкость
    s = append(s, 4)

    fmt.Println(s)      // [1 2 3 4]
    fmt.Println(len(s)) // 4
    fmt.Println(cap(s)) // емкость увеличена - значение зависит от стратегии рантайма
}

Когда длина среза при добавлении не превышает емкость, новый массив не выделяется. Когда превышает — создается новый массив, обычно большего размера (стратегия роста зависит от версии рантайма, но общая идея — рост примерно в 2 раза для больших срезов).

Когда append изменяет базовый массив, а когда — нет

Давайте сравним два сценария, чтобы вы увидели разницу.

Сценарий 1 - хватает емкости

package main

import "fmt"

func main() {
    // Здесь мы создаем срез с длиной 3 и емкостью 5
    base := make([]int, 3, 5)
    copy(base, []int{1, 2, 3}) // Здесь мы заполняем элементы

    // Здесь мы берем другой срез, указывающий на тот же массив
    alias := base[:]

    // Здесь мы добавляем один элемент - емкости достаточно
    base = append(base, 4)

    fmt.Println(base)  // [1 2 3 4]
    fmt.Println(alias) // [1 2 3 4 0] - alias видит измененный массив
}

Оба среза используют один базовый массив. append просто записывает новый элемент.

Сценарий 2 - емкости не хватает

package main

import "fmt"

func main() {
    // Здесь мы создаем срез длиной и емкостью 3
    base := []int{1, 2, 3}

    // Здесь мы берем другой срез, указывающий на тот же массив
    alias := base[:]

    // Здесь мы добавляем несколько элементов - емкости уже не хватает
    base = append(base, 4, 5, 6)

    fmt.Println(base)         // [1 2 3 4 5 6]
    fmt.Println(alias)        // [1 2 3] - alias по-прежнему ссылается на старый массив
    fmt.Println(len(alias))   // 3
    fmt.Println(cap(alias))   // 3
}

В этом случае рантайм создал новый массив, скопировал туда старые элементы и добавил новые. base теперь указывает на новый массив, а alias остался на старом.

Добавление среза к срезу

Очень часто вам нужно "слить" два среза. Для этого используют "распаковку" с помощью ...:

package main

import "fmt"

func main() {
    // Здесь мы создаем первый срез
    a := []int{1, 2, 3}

    // Здесь мы создаем второй срез
    b := []int{4, 5}

    // Здесь мы добавляем все элементы из b в a
    a = append(a, b...)

    fmt.Println(a) // [1 2 3 4 5]
}

Три точки означают, что элементы b будут переданы в append как отдельные аргументы.

Функция copy и копирование срезов

Зачем нужен copy

Функция copy — еще один ключевой элемент Слайс API. Она позволяет скопировать элементы из одного среза в другой:

n := copy(dst, src)

Где:

  • dst — целевой срез;
  • src — источник;
  • n — количество реально скопированных элементов (минимум из len(dst) и len(src)).

Пример:

package main

import "fmt"

func main() {
    // Здесь мы создаем исходный срез
    src := []int{1, 2, 3, 4}

    // Здесь мы создаем целевой срез длиной 2
    dst := make([]int, 2)

    // Здесь мы копируем данные
    n := copy(dst, src)

    fmt.Println(dst) // [1 2] - скопировалось только два элемента
    fmt.Println(n)   // 2 - количество скопированных элементов
}

Глубокое и "поверхностное" копирование

copy копирует значения элементов. Если элементы — простые типы (int, string, struct без указателей), можно считать, что это полноценное копирование.

Но если элементы — указатели, срезы, карты или структуры с указателями, то после copy оба среза будут ссылаться на одни и те же объекты.

Пример с указателями:

package main

import "fmt"

func main() {
    // Здесь мы создаем два числа
    a, b := 10, 20

    // Здесь мы создаем срез указателей
    src := []*int{&a, &b}

    // Здесь мы создаем целевой срез той же длины
    dst := make([]*int, len(src))

    // Здесь мы копируем указатели
    copy(dst, src)

    // Меняем значение через dst
    *dst[0] = 100

    fmt.Println(*src[0]) // 100 - исходный тоже изменился
}

Как видите, copy не создает новые объекты, он копирует только ссылки.

Копирование перекрывающихся срезов

Иногда нужно "сдвинуть" часть среза внутри себя. copy корректно работает и для перекрывающихся областей.

Пример:

package main

import "fmt"

func main() {
    // Здесь мы создаем срез
    s := []int{1, 2, 3, 4, 5}

    // Здесь мы "сдвигаем" элементы влево на один
    copy(s, s[1:])

    fmt.Println(s) // [2 3 4 5 5] - последний элемент дублируется
}

Этот прием часто используют для "удаления" элемента по индексу с сохранением порядка.

Работа со срезами в функциях

Передача среза как аргумента

Срез в Go — это структура из трех полей, и при передаче в функцию он передается по значению. Но внутри есть указатель на данные, поэтому изменение элементов внутри функции влияет на исходный массив.

Пример:

package main

import "fmt"

// Здесь мы определяем функцию, которая изменяет первый элемент среза
func modifyFirst(s []int) {
    // Изменяем элемент по индексу 0
    s[0] = 99
}

func main() {
    // Здесь мы создаем срез
    nums := []int{1, 2, 3}

    // Передаем срез в функцию
    modifyFirst(nums)

    fmt.Println(nums) // [99 2 3] - первый элемент изменился
}

То есть:

  • изменение элементов внутри функции видно снаружи;
  • изменение самого дескриптора (например, s = append(s, ...)) не влияет на исходную переменную, если вы не возвращаете новый срез наружу.

Пример:

package main

import "fmt"

// Здесь мы пытаемся добавить элементы к срезу внутри функции
func addNumbers(s []int) {
    // append возвращает новый срез, но мы его не возвращаем и не сохраняем
    s = append(s, 4, 5)
}

func main() {
    // Здесь мы создаем исходный срез
    nums := []int{1, 2, 3}

    // Вызываем функцию
    addNumbers(nums)

    fmt.Println(nums) // [1 2 3] - длина не изменилась
}

Чтобы изменение длины было видно снаружи, вы либо должны вернуть новый срез, либо работать с указателем на срез:

package main

import "fmt"

// Вариант с возвратом нового среза
func addReturn(s []int) []int {
    s = append(s, 4, 5)
    return s
}

// Вариант с указателем на срез
func addPtr(ps *[]int) {
    // Разыменовываем указатель и вызываем append
    *ps = append(*ps, 4, 5)
}

func main() {
    nums := []int{1, 2, 3}

    // Первый вариант
    nums = addReturn(nums)
    fmt.Println(nums) // [1 2 3 4 5]

    // Второй вариант
    addPtr(&nums)
    fmt.Println(nums) // [1 2 3 4 5 4 5]
}

Типичные операции над срезами

Добавление в начало и середину

У append есть важное ограничение — он добавляет только в конец. Но вы можете комбинировать append и поддиапазоны.

Добавление в начало

package main

import "fmt"

func prepend(s []int, v int) []int {
    // Здесь мы создаем новый срез длиной 0, но с емкостью на один больше
    s = append([]int{v}, s...) // Мы объединяем новый элемент и все старые

    return s
}

func main() {
    nums := []int{2, 3}

    nums = prepend(nums, 1)

    fmt.Println(nums) // [1 2 3]
}

Такой подход достаточно прост, но обычно требует выделения нового массива (особенно если срез большой).

Вставка в середину

package main

import "fmt"

func insertAt(s []int, idx int, v int) []int {
    // Проверяем границы индекса
    if idx < 0 || idx > len(s) {
        return s
    }

    // Добавляем "пустое" место в конец
    s = append(s, 0)

    // Сдвигаем элементы вправо, начиная с позиции idx
    copy(s[idx+1:], s[idx:])

    // Записываем новый элемент в позицию idx
    s[idx] = v

    return s
}

func main() {
    nums := []int{1, 2, 4, 5}

    nums = insertAt(nums, 2, 3)

    fmt.Println(nums) // [1 2 3 4 5]
}

Здесь я использую комбинацию append и copy, чтобы реализовать вставку с сохранением порядка.

Удаление элементов

Удаление по индексу — частая задача. Смотрите, я покажу вам один из стандартных паттернов.

Удаление по индексу с сохранением порядка

package main

import "fmt"

func removeAt(s []int, idx int) []int {
    if idx < 0 || idx >= len(s) {
        return s
    }

    // Сдвигаем элементы после idx влево
    copy(s[idx:], s[idx+1:])

    // Укорачиваем срез на один
    return s[:len(s)-1]
}

func main() {
    nums := []int{1, 2, 3, 4, 5}

    nums = removeAt(nums, 2)

    fmt.Println(nums) // [1 2 4 5]
}

Удаление без сохранения порядка

Иногда порядок не важен, и можно удалить элемент более эффективно:

package main

import "fmt"

func removeUnordered(s []int, idx int) []int {
    if idx < 0 || idx >= len(s) {
        return s
    }

    // Переносим последний элемент на место удаляемого
    s[idx] = s[len(s)-1]

    // Укорачиваем срез
    return s[:len(s)-1]
}

func main() {
    nums := []int{10, 20, 30, 40}

    nums = removeUnordered(nums, 1)

    fmt.Println(nums) // Например [10 40 30] - порядок изменен
}

Этот прием особенно полезен для больших срезов, когда вы часто удаляете элементы.

Память, аллокации и утечки при работе со срезами

Удержание "лишней" памяти поддиапазонами

Опасный момент Слайс API — когда вы создаете поддиапазон большого среза, он удерживает в памяти весь базовый массив. Даже если новый срез содержит пару элементов.

Пример:

package main

import "fmt"

func main() {
    // Здесь мы создаем большой срез
    big := make([]byte, 1_000_000)

    // Здесь мы берем маленький поддиапазон - первые 10 байт
    small := big[:10]

    // Если здесь забыть big, сборщик мусора все равно не освободит память,
    // потому что small ссылается на тот же массив
    fmt.Println(len(small), cap(small)) // 10 и 1_000_000
}

Чтобы "отцепиться" от большого массива, удобно скопировать нужные данные в новый срез:

package main

import "fmt"

func main() {
    big := make([]byte, 1_000_000)

    // Здесь мы берем нужный диапазон
    part := big[100:200]

    // Здесь мы создаем новый срез точной длины
    data := make([]byte, len(part))

    // Копируем только нужные байты
    copy(data, part)

    // Теперь data не держит большой массив, и тот может быть освобожден
    fmt.Println(len(data), cap(data)) // 100 и 100
}

Срез и escape analysis

Когда вы храните срезы в структурах, передаете их между горутинами, рантайму иногда приходится размещать базовые массивы в куче. Тогда важно следить за лишними аллокациями.

Несколько практических советов:

  • если вы заранее знаете размер данных, используйте make с нужной емкостью;
  • избегайте лишних копирований срезов, когда они не нужны;
  • старайтесь не создавать много временных срезов-поддиапазонов в горячем коде, если это приводит к удержанию больших массивов.

Срезы и горутины

Совместное использование срезов из разных горутин

Срезы сами по себе не потокобезопасны. Если две горутины одновременно:

  • читают и записывают элементы;
  • или записывают элементы;

то вы можете получить гонку данных.

Общий подход:

  • если только чтение — безопасно использовать один и тот же срез;
  • если есть запись — либо защищайте доступ мьютексами, либо используйте каналы, либо не делитесь срезами.

Пример потенциальной гонки:

package main

import (
    "fmt"
    "sync"
)

func main() {
    // Здесь мы создаем срез
    nums := []int{1, 2, 3}

    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        defer wg.Done()
        // Изменяем элемент
        nums[0] = 100 // Запись
    }()

    go func() {
        defer wg.Done()
        // Читаем элемент
        fmt.Println(nums[0]) // Чтение
    }()

    wg.Wait()
}

Без синхронизации этот код может вызвать гонку. Чтобы избежать этого, нужно либо защитить доступ, либо работать с копиями:

// Здесь мы передаем в горутину копию среза
go func(local []int) {
    defer wg.Done()
    local[0] = 100 // Меняем только локальную копию
}(append([]int(nil), nums...)) // Копируем базовый массив

Практические паттерны использования Слайс API

Буферизация данных

Часто срезы используют как буфер для чтения/записи.

Пример буфера для чтения из io.Reader:

package main

import (
    "io"
    "os"
)

func main() {
    // Здесь мы создаем буфер фиксированного размера
    buf := make([]byte, 4096)

    for {
        // Читаем данные в буфер
        n, err := os.Stdin.Read(buf)
        if n > 0 {
            // Обрабатываем только n байт
            process(buf[:n]) // Передаем поддиапазон реальных данных
        }
        if err == io.EOF {
            break
        }
        if err != nil {
            // Здесь надо обработать ошибку
            break
        }
    }
}

func process(data []byte) {
    // Здесь мы обрабатываем данные среза
    // ...
}

Здесь вы видите типичный прием: один и тот же базовый массив используется многократно, а "видимая" часть меняется через поддиапазоны.

Пулл срезов

Чтобы снизить количество аллокаций, иногда используют пулы срезов (через sync.Pool или кастомные структуры). Внутри таких решений активно используется Слайс API: make, append, диапазоны и copy.

Здесь важно аккуратно обнулять срезы перед возвратом в пул, чтобы не удерживать лишние объекты.

package main

import (
    "sync"
)

var bufPool = sync.Pool{
    New: func() any {
        // Здесь мы создаем новый срез фиксированной емкости
        b := make([]byte, 0, 4096)
        return &b
    },
}

func getBuf() []byte {
    // Берем указатель на срез из пула
    b := bufPool.Get().(*[]byte)

    // Обрезаем длину до нуля, но сохраняем емкость
    *b = (*b)[:0]

    return *b
}

func putBuf(b []byte) {
    // Обнуляем данные, если нужно, и кладем обратно
    bufPool.Put(&b)
}

Так вы повторно используете один и тот же базовый массив для разных операций.

Заключение

Срезы в Go и сопутствующий им Слайс API (операции диапазона, функции make, append, copy, передача в функции, работа с емкостью и длиной) образуют мощный и гибкий механизм работы с последовательностями данных. Важно помнить несколько ключевых идей:

  • срез — это дескриптор поверх базового массива, а не сам массив;
  • len и cap — разные вещи, и append опирается именно на емкость;
  • append может как модифицировать существующий массив, так и создавать новый — из-за этого копии срезов иногда ведут себя по-разному;
  • поддиапазоны удерживают в памяти весь базовый массив, что может приводить к скрытым утечкам;
  • copy копирует значения элементов, но не обязательно объекты, на которые те ссылаются;
  • при работе с горутинами срезы нужно использовать аккуратно, синхронизируя доступ при записи.

Если вы будете осознанно использовать эти свойства, то сможете писать более предсказуемый, эффективный и простой для сопровождения код на Go.

Частозадаваемые технические вопросы

Как безопасно "обрезать" емкость среза, чтобы append не затронул соседние данные?

Используйте трехиндексный срез:

sub := s[i:j:j] // len = j-i, cap = j-i

Так вы ограничите емкость поддиапазона, и любой append к sub приведет к выделению нового массива, не изменяя s.

Как скопировать срез "полностью", включая базовый массив, чтобы изменения не влияли на оригинал?

Создайте новый срез нужной длины и используйте copy:

dst := make([]T, len(src)) // Новый массив
copy(dst, src)             // Копируем элементы

Теперь dst и src независимы по памяти (кроме случаев, когда элементы — указатели или сложные объекты).

Как эффективно очистить срез, чтобы он не держал в памяти большие объекты?

Если вам нужно, чтобы сборщик мусора мог освободить объекты, на которые ссылаются элементы, обнулите их и укоротите срез:

for i := range s {
    s[i] = nil // или zero-value вашего типа
}
s = s[:0] // длина 0, емкость прежняя

Если нужно уменьшить и емкость, создайте новый срез и скопируйте нужные данные.

Почему append к срезу, переданному в функцию, иногда "не работает"?

Потому что append возвращает новый срез, а вы изменяете только локальную копию аргумента. Возвращайте срез из функции или передавайте указатель:

func f(s []int) []int {
    s = append(s, 1)
    return s
}

или

func f(ps *[]int) {
    *ps = append(*ps, 1)
}

Как избежать лишних аллокаций при постепенном росте среза?

Предварительно задайте емкость с помощью make, если примерно знаете ожидаемый размер:

s := make([]T, 0, expected)

Если размер неизвестен, можно увеличивать емкость порциями (например, удваивать свою "целевую" емкость и перераспределять массив вручную), но чаще достаточно встроенной стратегии роста рантайма.

Стрелочка влевоГоризонтальные слайсы horizontal-slices - практическое руководство по организации кода

Все гайды по Fsd

Открыть базу знаний

Отправить комментарий