Неправильное использование массивов вместо слайсов (или наоборот) — частая причина неоптимального кода на Go. Разберемся, когда выбирать каждый из вариантов и как избежать типичных ошибок.
- Что такое массивы в Go?
- Что такое слайсы в Go?
- Основные отличия: массив vs слайс
- 1. Фиксированный размер vs динамический
- 2. Передача по значению vs по ссылке
- 3. Сравнение типов
- Практические примеры использования
- Когда использовать массивы:
- Когда использовать слайсы:
- Важные нюансы работы со слайсами
- Проблема неожиданных изменений:
- Проблема утечка памяти:
- Лучшие практики
- Ключевой вывод
Что такое массивы в 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
}
Лучшие практики
- Используйте массивы для статических данных фиксированного размера.
- Используйте слайсы в 95% случаев для работы с коллекциями.
- Задавайте емкость при создании слайсов, если знаете примерный размер.
- Помните о ссылочной природе слайсов при передаче в функции.
- Используйте
copy()когда нужно создать независимую копию данных.
Ключевой вывод
Массивы и слайсы — фундаментальные структуры данных в Go с разными областями применения. Массивы предлагают предсказуемость и безопасность благодаря фиксированному размеру и передаче по значению. Слайсы обеспечивают гибкость и эффективность для работы с динамическими данными.
Главное правило: если сомневаетесь — используйте слайсы. Они покрывают большинство сценариев разработки, а массивы применяйте в специфических случаях, где важна гарантия размера или нужно избежать накладных расходов на сборку мусора.
А вы часто используете массивы в своих проектах на Go? Поделитесь в комментариях, в каких сценариях вы предпочитаете массивы слайсам и почему.