🐹 Go

Go (Golang) में ऐरे और कलेक्शन मेथड्स: एक विस्तृत गाइड

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты — учитель физкультуры, и у тебя есть список учеников в классе. Этот список — и есть массив или коллекция (в Go чаще используется срез — slice).

Без методов тебе пришлось бы делать всё вручную: переписывать список от руки, если кто-то пришёл новый, вычёркивать ушедших, искать каждого ученика глазами. Это долго и неудобно.

Методы массивов и коллекций — это как набор волшебных инструментов, которые делают всю рутинную работу за тебя. Хочешь добавить ученика? Есть метод append. Хочешь узнать, сколько всего учеников? Есть len. Хочешь скопировать список? Есть copy.

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

💡 Запомни: Массивы и коллекции — это контейнеры для хранения множества однотипных элементов. Методы — это действия, которые можно с ними совершать.

Теория: как это работает изнутри

В Go есть два основных типа для хранения наборов данных: массивы (arrays) и срезы (slices). Они похожи, но работают по-разному.

Массивы (Array)

Массив — это фиксированный набор элементов одного типа. Представь себе парковку на 10 мест. Сколько бы машин ни приехало, больше 10 ты не поставишь. Места пронумерованы от 0 до 9.

Когда ты создаёшь массив, компьютер сразу выделяет для него непрерывный блок памяти. Все элементы лежат рядом друг с другом. Это очень быстро для доступа, но негибко: нельзя добавить 11-й элемент.

Срезы (Slice)

Срез — это динамический массив. Он может расти и уменьшаться. Внутри срез — это «окошко» на часть массива. У него есть три важных параметра:

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

Когда ты добавляешь элемент в срез через append, Go проверяет: есть ли свободное место? Если да — просто кладёт элемент рядом. Если нет — создаёт новый массив большего размера (обычно в 2 раза), копирует туда все старые элементы и добавляет новый. Это называется перераспределение памяти.

⚠️ Важно: Срез — это ссылочный тип. Если ты передашь срез в функцию и изменишь его элементы, изменения увидят и там, откуда ты его передал. А вот длина и вместимость — нет, если не вернуть новый срез.

Как хранятся данные?

Представь шкаф с ящиками. Каждый ящик имеет номер (индекс). Массив — это шкаф с фиксированным числом ящиков. Срез — это поднос, на котором лежат ящики. Ты можешь взять поднос и положить на него больше ящиков, но если поднос маленький, придётся взять поднос побольше и переложить все ящики заново.

Синтаксис в Go (Golang): все формы и варианты

Давай разберём, как создавать массивы и срезы, и какие методы к ним применимы.

Создание массивов

// Способ 1: явно указываем размер и значения
var arr1 [5]int = [5]int{1, 2, 3, 4, 5}

// Способ 2: короткая запись (компилятор сам посчитает размер)
arr2 := [...]string{"яблоко", "банан", "апельсин"}

// Способ 3: создаём пустой массив (все элементы — нули)
var arr3 [3]float64  // будет [0.0, 0.0, 0.0]

// Доступ по индексу (нумерация с нуля)
arr1[0] = 10  // теперь arr1 = [10, 2, 3, 4, 5]
fmt.Println(arr1[2])  // выведет 3

Создание срезов

// Способ 1: из массива (срез — это окно в массив)
arr := [5]int{10, 20, 30, 40, 50}
slice1 := arr[1:4]  // [20, 30, 40] — элементы с 1 по 3 (4 не включается)

// Способ 2: литерал среза (без указания размера)
slice2 := []int{1, 2, 3, 4, 5}

// Способ 3: через make (указываем тип, длину, вместимость)
slice3 := make([]int, 3, 5)  // длина 3, вместимость 5, элементы [0, 0, 0]

// Способ 4: пустой срез
var slice4 []string  // nil-срез, не указывает ни на какой массив
slice5 := []int{}    // пустой срез, но не nil

Основные методы и функции

Метод/Функция Описание Пример
len(s) Возвращает длину (количество элементов) len([]int{1,2,3}) → 3
cap(s) Возвращает вместимость (сколько может поместиться) cap(make([]int, 0, 5)) → 5
append(s, x...) Добавляет элементы в конец среза append([]int{1}, 2, 3) → [1,2,3]
copy(dst, src) Копирует элементы из src в dst copy(dst, src)
s[i] Доступ к элементу по индексу s[0]
s[i:j] Создаёт новый срез из элементов с i по j-1 s[1:3]
s[i:j:k] То же, но с ограничением вместимости до k s[1:3:5]
📌 Заметка: В Go нет встроенных методов для удаления, поиска или сортировки в самом языке (как в Python). Для этого нужно писать свои функции или использовать пакеты вроде sort.

Примеры кода — от простого к сложному

Пример 1: Создание и вывод массива

package main

import "fmt"

func main() {
    // Создаём массив из 5 целых чисел
    var grades [5]int
    
    // Заполняем массив оценками
    grades[0] = 85
    grades[1] = 92
    grades[2] = 78
    grades[3] = 90
    grades[4] = 88
    
    // Выводим все оценки через цикл
    fmt.Println("Оценки ученика:")
    for i := 0; i < len(grades); i++ {
        fmt.Printf("Предмет %d: %d баллов
", i+1, grades[i])
    }
    
    // Выводим среднее арифметическое
    sum := 0
    for _, grade := range grades {
        sum += grade
    }
    average := float64(sum) / float64(len(grades))
    fmt.Printf("Средний балл: %.2f
", average)
}

Объяснение: Мы создали массив из 5 элементов, заполнили его вручную, прошлись циклом и посчитали среднее. Обрати внимание на for _, grade := range grades — это цикл по коллекции, где _ означает, что индекс нам не нужен.

Пример 2: Динамический список покупок (срез и append)

package main

import "fmt"

func main() {
    // Создаём пустой срез для списка покупок
    shoppingList := []string{}
    
    // Добавляем покупки (магазин открыт, мы добавляем товары)
    shoppingList = append(shoppingList, "Молоко")
    shoppingList = append(shoppingList, "Хлеб")
    shoppingList = append(shoppingList, "Яйца", "Сыр") // можно добавить сразу несколько
    
    fmt.Println("Список покупок:", shoppingList)
    fmt.Println("Количество покупок:", len(shoppingList))
    
    // Ой, забыли масло! Добавляем в начало (хитрость)
    shoppingList = append([]string{"Масло"}, shoppingList...)
    fmt.Println("Обновлённый список:", shoppingList)
    
    // Убираем последний элемент (Сыр)
    shoppingList = shoppingList[:len(shoppingList)-1]
    fmt.Println("После удаления сыра:", shoppingList)
}

Объяснение: append всегда добавляет в конец. Чтобы добавить в начало, мы создаём новый срез с одним элементом и «разворачиваем» старый срез через .... Удаление делается через срез среза: берём все элементы, кроме последнего.

Пример 3: Копирование срезов (чтобы не испортить оригинал)

package main

import "fmt"

func main() {
    original := []int{1, 2, 3, 4, 5}
    
    // Плохой способ: просто присвоить (это ссылка, а не копия!)
    badCopy := original
    badCopy[0] = 999
    fmt.Println("Оригинал после плохого копирования:", original) // [999, 2, 3, 4, 5] — испортился!
    
    // Хороший способ: используем copy
    goodCopy := make([]int, len(original))
    copied := copy(goodCopy, original) // copy возвращает количество скопированных элементов
    fmt.Println("Скопировано элементов:", copied)
    
    goodCopy[0] = 111
    fmt.Println("Оригинал после хорошего копирования:", original) // [999, 2, 3, 4, 5] — не изменился
    fmt.Println("Копия:", goodCopy) // [111, 2, 3, 4, 5]
}

Объяснение: Срезы — ссылочные. Простое присваивание badCopy := original не копирует данные, а заставляет обе переменные смотреть на один и тот же массив. copy же реально копирует элементы. Важно: целевой срез (goodCopy) должен быть заранее создан с нужной длиной.

Пример 4: Многомерные массивы (таблица умножения)

package main

import "fmt"

func main() {
    // Создаём двумерный массив 3x3
    var matrix [3][3]int
    
    // Заполняем таблицу умножения
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            matrix[i][j] = (i + 1) * (j + 1)
        }
    }
    
    // Выводим красиво
    fmt.Println("Таблица умножения 3x3:")
    for _, row := range matrix {
        for _, val := range row {
            fmt.Printf("%d	", val) // 	 — табуляция для ровных столбцов
        }
        fmt.Println() // переход на новую строку
    }
}

Объяснение: [3][3]int — это массив из 3 массивов по 3 элемента. Вложенные циклы позволяют обращаться к каждому элементу. Вывод с делает таблицу аккуратной.

Пример 5: Сортировка среза (пакет sort)

package main

import (
    "fmt"
    "sort"
)

func main() {
    // Срез чисел вперемешку
    numbers := []int{42, 13, 7, 99, 1, 56}
    
    fmt.Println("До сортировки:", numbers)
    
    // Сортируем по возрастанию
    sort.Ints(numbers)
    fmt.Println("После сортировки по возрастанию:", numbers)
    
    // Сортируем строки
    fruits := []string{"апельсин", "банан", "яблоко", "груша"}
    sort.Strings(fruits)
    fmt.Println("Фрукты по алфавиту:", fruits)
    
    // Сортировка в обратном порядке
    sort.Sort(sort.Reverse(sort.IntSlice(numbers)))
    fmt.Println("Числа по убыванию:", numbers)
}

Объяснение: Go не умеет сортировать «из коробки» без пакетов. sort.Ints и sort.Strings — самые простые функции. Для обратного порядка используем sort.Reverse. Пакет sort — твой лучший друг для работы с порядком.

Пример 6: Поиск элемента в срезе (своя функция)

package main

import "fmt"

// Функция ищет элемент в срезе и возвращает индекс (-1, если не найден)
func findIndex(slice []string, target string) int {
    for i, item := range slice {
        if item == target {
            return i // нашли — возвращаем индекс
        }
    }
    return -1 // не нашли
}

func main() {
    students := []string{"Анна", "Борис", "Виктор", "Галина", "Дмитрий"}
    
    name := "Виктор"
    index := findIndex(students, name)
    
    if index != -1 {
        fmt.Printf("Студент %s найден на позиции %d
", name, index)
    } else {
        fmt.Printf("Студент %s не найден
", name)
    }
    
    // Проверяем отсутствующего
    index = findIndex(students, "Елена")
    fmt.Printf("Поиск Елены: индекс %d
", index) // -1
}

Объяснение: В Go нет встроенного метода indexOf как в Python. Мы пишем свою функцию, которая проходит по всем элементам и сравнивает. Это отличный пример того, как методы коллекций реализуются вручную.

Пример 7: Фильтрация среза (оставляем только чётные числа)

package main

import "fmt"

// Функция фильтрации: оставляет только те элементы, для которых условие true
func filterEven(numbers []int) []int {
    result := []int{} // создаём пустой срез для результата
    for _, num := range numbers {
        if num%2 == 0 { // проверяем, чётное ли число
            result = append(result, num) // если да — добавляем
        }
    }
    return result
}

func main() {
    nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    
    fmt.Println("Исходный срез:", nums)
    
    evens := filterEven(nums)
    fmt.Println("Только чётные:", evens)
    
    // Можно сделать универсальную функцию с callback (функция как аргумент)
    // Но это уже продвинутый уровень
}

Объяснение: Фильтрация — это создание нового среза, куда мы добавляем только те элементы, которые удовлетворяют условию. В Go нет встроенного filter, как в Python, но написать свою функцию — дело одной минуты.

Сравнение с Python: чем похоже и чем отличается

Операция Python (список) Go (срез)
Создание lst = [1, 2, 3] slc := []int{1, 2, 3}
Длина len(lst) len(slc)
Добавление в конец lst.append(4) slc = append(slc, 4)
Добавление нескольких lst.extend([4,5]) slc = append(slc, 4, 5)
Доступ по индексу lst[0] slc[0]
Срез (подсписок) lst[1:3] slc[1:3]
Удаление по индексу del lst[2] или lst.pop(2) Нет встроенного. Нужно: slc = append(slc[:2], slc[3:]...)
Поиск элемента lst.index(5) Нет встроенного. Нужно писать цикл
Сортировка lst.sort() sort.Ints(slc) (пакет sort)
Фильтрация [x for x in lst if x > 2] Нет встроенного. Нужно писать цикл
Копирование lst2 = lst.copy() copy(dst, src)
Типизация Динамическая (можно смешивать типы) Статическая (все элементы одного типа)
🔑 Главное отличие: Python делает всё за тебя — много встроенных методов. Go даёт базовые инструменты и требует, чтобы ты сам писал логику. Это делает код более предсказуемым и быстрым, но требует больше кода.

Типичные ошибки и подводные камни

Ошибка 1: Выход за границы массива

arr := [3]int{1, 2, 3}
fmt.Println(arr[3]) // Ошибка! Индексы: 0, 1, 2. Индекса 3 нет.

Решение: Всегда проверяй, что индекс меньше длины. Используй len().

Ошибка 2: Путаница между nil-срезом и пустым срезом

var s1 []int       // nil-срез (len=0, cap=0, s1 == nil)
s2 := []int{}      // пустой срез (len=0, cap=0, s2 != nil)
s3 := make([]int, 0) // тоже пустой срез

// append работает с nil-срезом, но range — нет проблем
for _, v := range s1 { // ничего не выведет, но и не упадёт
    fmt.Println(v)
}

Решение: Если хочешь проверить, пуст ли срез, используй len(s) == 0, а не s == nil.

Ошибка 3: Изменение оригинального массива через срез

arr := [4]int{10, 20, 30, 40}
slice := arr[1:3] // [20, 30]
slice[0] = 99     // меняем элемент среза
fmt.Println(arr)  // [10, 99, 30, 40] — оригинал изменился!

Решение: Если нужно независимое копирование, используй copy или создавай новый срез через append.

Ошибка 4: Забыть присвоить результат append

slice := []int{1, 2, 3}
append(slice, 4) // Ошибка! append возвращает новый срез, а старый не меняется
fmt.Println(slice) // [1, 2, 3] — 4 не добавилось!

Решение: Всегда присваивай результат: slice = append(slice, 4).

Ошибка 5: Предполагать, что срезы сравниваются поэлементно

a := []int{1, 2, 3}
b := []int{1, 2, 3}
fmt.Println(a == b) // Ошибка компиляции! Срезы нельзя сравнивать через ==

Решение: Используй цикл для поэлементного сравнения или пакет reflect.DeepEqual (но он медленный).

Лучшие практики и советы профессионалов

  1. Всегда указывай вместимость (capacity) через make, если знаешь примерный размер. Это избегает лишних перераспределений памяти и ускоряет программу.
    // Плохо: append будет много раз пересоздавать массив
    nums := []int{}
    for i := 0; i < 1000; i++ {
        nums = append(nums, i)
    }
    
    // Хорошо: сразу выделяем память на 1000 элементов
    nums := make([]int, 0, 1000)
    for i := 0; i < 1000; i++ {
        nums = append(nums, i)
    }
  2. Не используй массивы, если не знаешь точный размер. В 99% случаев нужны срезы. Массивы полезны только для фиксированных структур (например, координаты точки [3]float64).
  3. Для удаления элемента используй идиому «append двух срезов».
    // Удаляем элемент с индексом i
    slice = append(slice[:i], slice[i+1:]...)
  4. Для вставки в середину — аналогично, но с созданием нового среза.
    // Вставляем элемент x на позицию i
    slice = append(slice[:i], append([]T{x}, slice[i:]...)...)
  5. Используй именованные типы для ясности. Вместо []string создай type Students []string и добавляй методы.
  6. Помни про «ловушку замыканий» в циклах. Если сохраняешь указатель на элемент среза в цикле, бери копию:
    for _, v := range slice {
        v := v // создаём локальную копию
        go func() { fmt.Println(v) }()
    }
  7. Для больших данных используй буферизированные каналы, а не срезы. Но это уже тема для продвинутых.

Вопросы для самопроверки

  1. Вопрос: Чем отличается массив от среза в Go?
    Ответ: Массив имеет фиксированный размер, задаваемый при создании. Срез — динамический, может расти. Массив — значение, срез — ссылка на часть массива.
  2. Вопрос: Что вернёт len([]int{})?
    Ответ: 0. Пустой срез имеет длину 0.
  3. Вопрос: Почему после append нужно присваивать результат обратно в переменную?
    Ответ: Потому что append может создать новый массив, если вместимость исчерпана. Старая переменная будет указывать на старый (возможно, уже невалидный) массив.
  4. Вопрос: Как скопировать один срез в другой, чтобы изменения в копии не влияли на оригинал?
    Ответ: Использовать функцию copy. Сначала создать целевой срез нужной длины через make, затем вызвать copy(dst, src).
  5. Вопрос: Можно ли сравнить два среза через ==?
    Ответ: Нет, это вызовет ошибку компиляции. Срезы можно сравнивать только с nil. Для поэлементного сравнения нужно писать цикл или использовать reflect.DeepEqual.
  6. Вопрос: Что такое вместимость (capacity) среза и зачем она нужна?
    Ответ: Вместимость — это количество элементов, которые можно добавить в срез без выделения новой памяти. Она оптимизирует производительность, уменьшая количество перераспределений.
  7. Вопрос: Как удалить элемент из середины среза?
    Ответ: Использовать идиому: slice = append(slice[:i], slice[i+1:]...), где i — индекс удаляемого элемента.
  8. Вопрос: В чём разница между var s []int и s := []int{}?
    Ответ: Первое — nil-срез (равен nil), второе — пустой срез (не равен nil). Оба имеют длину 0, но ведут себя по-разному при сериализации в JSON.
  9. Вопрос: Какой пакет нужно импортировать для сортировки среза?
    Ответ: Пакет "sort". Функции: sort.Ints(), sort.Strings(), sort.Slice() для пользовательских типов.
  10. Вопрос: Почему в Go нет встроенных методов filter, map, reduce как в Python?
    Ответ: Философия Go — простота и производительность. Встроенные методы усложняют язык и замедляют компиляцию. Вместо этого Go предлагает писать явные циклы, что делает код более предсказуемым и лёгким для оптимизации.

Итог и что изучить дальше

Мы прошли долгий путь от простых массивов до продвинутой работы со срезами. Ты узнал:

  • Что массивы — это фиксированные контейнеры, а срезы — динамические.
  • Как создавать, изменять, копировать и удалять элементы.
  • Как сортировать и искать данные.
  • В чём разница между Go и Python в работе с коллекциями.
  • Какие ошибки совершают новички и как их избежать.

Теперь ты готов двигаться дальше. Вот что стоит изучить следующим:

  1. Структуры (struct) — как создавать свои типы данных и хранить их в срезах.
  2. Пакет sort — глубже: сортировка пользовательских типов через sort.Interface.
  3. Пакет slices (Go 1.21+) — новые встроенные функции для работы со срезами: slices.Delete, slices.Insert, slices.Compact.
  4. Карты (map) — коллекции типа «ключ-значение» для быстрого поиска.
  5. Каналы (channel) — как передавать данные между горутинами (параллельными задачами).
  6. Дженерики (Generics) — как писать универсальные функции для любых типов срезов.
🎯 Совет на будущее: Не пытайся запомнить