Тонкости работы со срезами в Go

Тонкости работы со срезами в Go

Рассмотрим простой пример на Go, который демонстрирует ключевую особенность работы срезов (slices):

package main

import "fmt"

func main() {
    var foo []int
    var bar []int

    foo = append(foo, 1)
    foo = append(foo, 2)
    foo = append(foo, 3)
    bar = append(foo, 4)
    foo = append(foo, 5)

    fmt.Println(foo, bar)
}

Что выведет эта программа? [1 2 3 5] [1 2 3 4]? Или может что-то другое? Давайте разберемся.

Понимание срезов в Go

Что такое срез?

Срез (slice) в Go — это динамическая оболочка над массивом. Он состоит из 3-х основных компонентов:

  • Указатель (pointer) на базовый массив
  • Длина (length) — количество элементов в срезе
  • Емкость (capacity) — размер базового массива

Критически важная особенность

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

Пошаговый разбор примера

Шаг 1-2: Инициализация foo и bar

var foo []int // foo: nil, len=0, cap=0
var bar []int // bar: nil, len=0, cap=0

Шаг 3-5: Добавление элементов в foo

// создается новый массив, foo: [1], len=1, cap=1
foo = append(foo, 1)
// емкости недостаточно, создается новый массив cap=2
foo = append(foo, 2)
// емкости недостаточно, создается новый массив cap=4
foo = append(foo, 3)
// теперь foo: [1 2 3], len=3, cap=4

Важно: у foo теперь емкость 4, хотя длина всего 3.

Шаг 6: Важный момент

bar = append(foo, 4)

Что происходит:

  • append(foo, 4) получает срез foo (len=3, cap=4)
  • Емкость (4) больше текущей длины (3), поэтому новый массив НЕ создается
  • Элемент 4 добавляется в существующий базовый массив
  • bar теперь указывает на тот же массив, что и foo, но с длиной 4
  • Состояние: foo: [1 2 3], len=3, cap=4, bar: [1 2 3 4], len=4, cap=4

Шаг 7: Последнее добавление

foo = append(foo, 5)

Что происходит:

  • append(foo, 5) получает foo (len=3, cap=4)
  • Емкости достаточно, поэтому новый массив не создается
  • Элемент 5 записывается в 4-ю позицию базового массива
  • Но 4-ая позиция уже содержит значение 4 (которое было добавлено через bar)
  • Результат: foo: [1 2 3 5], len=4, cap=4

Результат выполнения

Программа выведет:

[1 2 3 5] [1 2 3 5]

Не [1 2 3 5] [1 2 3 4], как можно было бы ожидать.

Почему так происходит?

Оба среза foo и bar ссылаются на один и тот же базовый массив. Когда добавляем элементы, изменяется этот общий массив.

Как избежать подобных проблем?

1. Использовать копирование

bar = append([]int{}, foo...) // создаем новый срез
bar = append(bar, 4)

2. Использовать функцию copy

bar = make([]int, len(foo))
copy(bar, foo)
bar = append(bar, 4)

3. Использовать полное выражение среза

bar = append(foo[:len(foo):len(foo)], 4)

Выводы и лучшие практики

  1. Срезы — не массивы: они могут разделять память с другими срезами
  2. append может изменять общие данные: если емкости достаточно
  3. Для независимых копий используйте copy(): когда нужно гарантировать разделение данных
  4. Учитывайте емкость: понимание cap помогает предсказать поведение append

Поделитесь в комментариях, сталкивались ли вы с подобными «сюрпризами» в работе со срезами?

Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: