Avatar ·

Как объединить два среза (slice) в Go? Конкатенация срезов

Привет! Я только начал изучать Go и столкнулся с проблемой. Мне нужно объединить два среза (slice) в один. В Python я бы просто написал `list1 + list2`, но в Go, как я понял, такой операции нет. У меня есть два среза целых чисел: ```go slice1 := []int{1, 2, 3} slice2 := []int{4, 5, 6} ``` Я хочу получить результат: `[1 2 3 4 5 6]`. Я пробовал использовать оператор `+`, но компилятор выдаёт ошибку: `invalid operation: slice1 + slice2 (operator + not defined on slice)`. Потом я наткнулся на функцию `append`, но она добавляет элементы по одному. Писать цикл для каждого элемента — это же неэффективно? Может, есть какой-то встроенный способ или идиоматичное решение в Go? И ещё вопрос: если я использую `append`, не повлияет ли это на исходные срезы? Будет ли новый срез независимой копией? Подскажите, как правильно объединять срезы в Go, и что при этом происходит с памятью? Заранее спасибо! <<>>

Использование append с распаковкой (variadic)

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

package main

import "fmt"

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

    // Ключевой момент: slice2... распаковывает slice2 в отдельные аргументы
    result := append(slice1, slice2...)

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

Как это работает

Функция append принимает срез и произвольное количество элементов. Синтаксис slice2... (variadic argument) распаковывает второй срез в отдельные элементы, как если бы мы написали append(slice1, 4, 5, 6).

Важные нюансы про память и исходные срезы

  • Новый срез или модификация? Если в slice1 достаточно ёмкости (cap), append может перезаписать данные за его пределами и вернуть тот же базовый массив. Если ёмкости не хватает — выделяется новый массив, и данные копируются.
  • Когда это опасно? Пример:
a := []int{1, 2}
b := []int{3, 4, 5}
// cap(a) = 2, append не хватит места
c := append(a, b...)
a[0] = 999
fmt.Println(c) // [1 3 4 5] — a и c не связаны, всё ок

// А теперь опасный случай:
d := make([]int, 2, 5)
d[0] = 1
d[1] = 2
e := []int{3, 4}
f := append(d, e...)
d[0] = 999
fmt.Println(f) // [1 3 4] — f всё ещё ссылается на старый массив? НЕТ!
// На самом деле append создаст новый массив, потому что d и e — разные срезы

Главное правило: результат append всегда нужно присваивать переменной. Исходный срез slice1 не изменяется (но его базовый массив может быть изменён, если ёмкости хватало).

Безопасная конкатенация с копированием

Если вы хотите гарантированно получить независимую копию и не трогать исходные срезы, используйте make + copy:

func concatSlices[T any](slices ...[]T) []T {
    totalLen := 0
    for _, s := range slices {
        totalLen += len(s)
    }
    result := make([]T, totalLen)
    i := 0
    for _, s := range slices {
        i += copy(result[i:], s)
    }
    return result
}

// Использование:
r := concatSlices(slice1, slice2)
// r — полностью независимая копия

Производительность

append с распаковкой компилируется в эффективный код. Go runtime оптимизирует копирование через memmove. Для большинства случаев это лучший выбор. Если вы конкатенируете много срезов (десятки и больше), то версия с copy может быть чуть быстрее, но разница обычно незначительна.

Итог: используйте append(slice1, slice2...) — это стандартный идиоматичный способ. Помните про ёмкость, если важна независимость данных.

<<>>

Альтернативный подход: функция copy для явного контроля

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

func mergeSlices(s1, s2 []int) []int {
    // Выделяем новый массив с суммарной длиной
    result := make([]int, len(s1)+len(s2))
    // Копируем первый срез
    copy(result, s1)
    // Копируем второй срез начиная с позиции len(s1)
    copy(result[len(s1):], s2)
    return result
}

// Пример:
a := []int{1, 2}
b := []int{3, 4}
c := mergeSlices(a, b)
a[0] = 100 // не влияет на c
fmt.Println(c) // [1 2 3 4]

Когда это полезно

  • Когда вы работаете с большими срезами и хотите избежать неожиданного переиспользования памяти.
  • Когда нужно объединить более двух срезов — можно легко расширить функцию.
  • Когда важна читаемость кода для новичков: явное копирование понятнее, чем магия ....

Недостаток: немного больше кода и одно дополнительное выделение памяти (но в Go это нормально). Для простых случаев append с распаковкой остаётся предпочтительным вариантом.