Слайсы и массивы в Go: полный разбор отличий с примерами

массивы в go

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

Что такое массивы в Go?

Массив в Go — это фиксированная последовательность элементов одного типа с заданной длиной. Длина массива является частью его типа, что означает, что [3]int и [5]int — это разные типы данных.

// объявление массива
var arr1 [3]int                 // [0 0 0]
arr2 := [3]int{1, 2, 3}         // [1 2 3]
arr3 := [...]int{1, 2, 3, 4, 5} // компилятор сам посчитает длину: [1 2 3 4 5]

// массивы копируются полностью при присваивании
original := [3]string{"A", "B", "C"}
copy := original // создается полная копия массива
copy[0] = "X"

fmt.Println(original) // [A B C] - оригинал не изменился
fmt.Println(copy)     // [X B C] - изменилась только копия

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

Что такое слайсы в Go?

Slice (срез) — это гибкая, динамическая «обертка» над массивами. Слайс не хранит данные самостоятельно, а содержит указатель на базовый массив, длину и емкость.

// создание слайсов
slice1 := []int{1, 2, 3, 4, 5} // слайс с заданными значениями
slice2 := make([]int, 3, 5}    // длина 3, емкость 5: [0 0 0]
slice3 := arr2[1:3]            // слайс на основе массива: [2 3]

// слайсы работают по ссылке на общие данные
originalSlice := []int{1, 2, 3, 4, 5}
reference := originalSlice[1:4] // [2 3 4]
reference[0] = 99

fmt.Println(originalSlice) // [1 99 3 4 5] - оригинал изменился
fmt.Println(reference)     // [99 3 4]

Основные отличия: массив vs слайс

1. Фиксированный размер vs динамический

// массив - фиксированный размер
var arr [5]int
// arr = append(arr, 6) // ошибка компиляции

// слайс - динамический размер
slice := []int{1, 2, 3}
slice = append(slice, 4, 5) // теперь слайс содержит [1 2 3 4 5]

2. Передача по значению vs по ссылке

package main

import "fmt"

func modifyArray(arr [3]int) {
    arr[0] = 100 // изменяется только локальная копия
}

func modifySlice(slice []int) {
    slice[0] = 100 // изменяется только оригинальный слайс
}

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

    modifyArray(arr)
    modifySlice(slice)

    fmt.Println(arr)   // [1 2 3] - не изменился
    fmt.Println(slice) // [100 2 3] - изменился
}

3. Сравнение типов

arr1 := [3]int{1, 2, 3}
arr2 := [3]int{1, 2, 3}
fmt.Println(arr1 == arr2) // true - массивы можно сравнивать

slice1 := []int{1, 2, 3}
slice2 := []int{1, 2, 3}
// fmt.Println(slice1 == slice2) // Ошибка! Слайсы нельзя сравнивать напрямую

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

Когда использовать массивы:

// 1. Фиксированные наборы данных (дни недели, цвета радуги)
var daysOfWeek = [7]string{"Понедельник", "Вторник", "Среда", 
              "Четверг", "Пятница", "Суббота", "Воскресенье",
}

// 2. Матрица фиксированного размера
var identityMatrix [3][3]int = [3][3]int {
    {1, 0, 0},
    {0, 1, 0},
    {0, 0, 1}
}

// 3. Когда нужна статическая память (системы реального времени)
type SensorData struct {
    Timestamp int64
    Values    [10]float64 // гарантированный размер
}

Когда использовать слайсы:

// 1. Динамические коллекции (результаты запросов к внешним источникам)
func fetchUsers() []User {
    users := []User{}
    // имитируем получение данных неизвестного размера
    for i := 0; i < 5; i++ {
        users = append(users, User{ID: i})
    }

    return users
}

// 2. Построение данных постепенно
func collectMetrics() []float64 {
    metrics := make([]float64, 0, 100) // оптимизация: задаем начальную емкость
    for i := 0; i < 100; i++ {
        metrics = append(metrics, rand.Float64())
    }

    return metrics
}

// 3. Срезы (подмножества) данных
func getRecentLogs(logs []LogEntry, n int) []LogEntry {
    if n > len(logs) {
        n = len(logs)
    }
   
    return logs[:n] // первые n записей
}

Важные нюансы работы со слайсами

Проблема неожиданных изменений:

original := []int{1, 2, 3, 4, 5}
slice := original[1:4] // [2 3 4]

// меняем слайс
slice[0] = 99
fmt.Println(original) // [1 99 3 4 5] - оригинал изменился!

// Решение: копирование, если нужно изолировать данные
isolated := make([]int, len(original))
copy(isolated, original)
isolated[0] = 100
fmt.Println(original) // [1 99 3 4 5] - оригинал не изменился

Проблема утечка памяти:

func getFirstElement(data []int) []int {
    return data[:1] // слайс продолжает ссылаться на весь исходный массив
}

// Решение: явное копирование
func getFirstElementSafe(data []int) []int {
    result := make([]int, 1)
    result[0] = data[0]
    return result
}

Лучшие практики

  1. Используйте массивы для статических данных фиксированного размера.
  2. Используйте слайсы в 95% случаев для работы с коллекциями.
  3. Задавайте емкость при создании слайсов, если знаете примерный размер.
  4. Помните о ссылочной природе слайсов при передаче в функции.
  5. Используйте copy() когда нужно создать независимую копию данных.

Ключевой вывод

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

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

А вы часто используете массивы в своих проектах на Go? Поделитесь в комментариях, в каких сценариях вы предпочитаете массивы слайсам и почему.

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

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