Avatar ·

Как проверить, содержит ли срез (slice) в Go определенный элемент? Аналог contains() для среза

Всем привет! Я только начал изучать Go после Python и столкнулся с неожиданной для меня проблемой. В Python есть удобный оператор `in`, который позволяет проверить, есть ли элемент в списке: `if element in my_list:`. В Go я работаю со срезами (slice) и мне нужно часто проверять, содержит ли срез определенную строку или число. Например, у меня есть срез строк: ```go fruits := []string{"apple", "banana", "cherry"} ``` Я хочу проверить, есть ли в нем "banana". В стандартной библиотеке Go я не нашел встроенного метода типа `contains()` для срезов. Я пробовал написать цикл сам, но мне кажется, что это слишком много кода для такой простой операции. Может быть, есть какой-то стандартный способ или идиоматическое решение в Go? Или нужно каждый раз писать свою функцию? Заранее спасибо за помощь! <<>>

Правильный ответ: в Go нет встроенного contains(), но это идиоматично — писать свою функцию

Вы правы: в стандартной библиотеке Go нет универсального метода contains() для срезов. Это осознанное решение разработчиков языка. В Go предпочитают явные циклы, чтобы код был простым и предсказуемым. Однако это не значит, что вам нужно писать цикл каждый раз вручную. Идиоматическое решение — написать небольшую вспомогательную функцию один раз и переиспользовать её.

Базовое решение для строк

Самый простой и понятный способ — это цикл for range:

package main

import "fmt"

// contains проверяет, содержит ли срез строк элемент target
func contains(slice []string, target string) bool {
    for _, s := range slice {
        if s == target {
            return true
        }
    }
    return false
}

func main() {
    fruits := []string{"apple", "banana", "cherry"}
    fmt.Println(contains(fruits, "banana")) // true
    fmt.Println(contains(fruits, "grape"))  // false
}

Универсальное решение с дженериками (Go 1.18+)

Начиная с версии Go 1.18, вы можете использовать дженерики, чтобы написать одну функцию для любого типа, поддерживающего сравнение:

package main

import "fmt"

// containsGeneric работает для любого comparable типа (string, int, float64 и т.д.)
func containsGeneric[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {
            return true
        }
    }
    return false
}

func main() {
    strings := []string{"a", "b", "c"}
    ints := []int{1, 2, 3}
    floats := []float64{1.1, 2.2, 3.3}

    fmt.Println(containsGeneric(strings, "b")) // true
    fmt.Println(containsGeneric(ints, 4))      // false
    fmt.Println(containsGeneric(floats, 2.2))  // true
}

Если вам нужно часто — используйте map

Если вы проверяете наличие элементов очень часто (миллионы раз), и порядок элементов не важен, конвертируйте срез в map. Поиск по ключу в map — O(1), тогда как в срезе — O(n):

package main

import "fmt"

func main() {
    fruits := []string{"apple", "banana", "cherry"}
    fruitSet := make(map[string]struct{}, len(fruits))
    for _, f := range fruits {
        fruitSet[f] = struct{}{}
    }

    // Теперь проверка мгновенная
    _, exists := fruitSet["banana"]
    fmt.Println(exists) // true
}

Важные замечания

  • Производительность: Для маленьких срезов (до 100 элементов) разница между циклом и map незаметна. Для больших срезов map быстрее, но требует больше памяти.
  • Сложные типы: Если вы храните в срезе структуры, дженерики не помогут — нужно писать функцию с кастомной логикой сравнения.
  • Сторонние библиотеки: Не стоит подключать целую библиотеку (например, samber/lo) только ради одной функции contains. Лучше написать 3 строки кода.

В итоге: да, в Go нет contains() встроенного, но написать свою функцию — это нормальная практика. Go поощряет явность и простоту, даже если это означает чуть больше кода.

<<>>

Альтернативный подход: использование пакета slices (Go 1.21+)

Начиная с версии Go 1.21, в стандартной библиотеке появился пакет slices, который включает функцию slices.Contains(). Это самое современное и рекомендуемое решение, если вы используете Go 1.21 или новее:

package main

import (
    "fmt"
    "slices"
)

func main() {
    fruits := []string{"apple", "banana", "cherry"}
    fmt.Println(slices.Contains(fruits, "banana")) // true
    fmt.Println(slices.Contains(fruits, "grape"))  // false

    numbers := []int{10, 20, 30}
    fmt.Println(slices.Contains(numbers, 25)) // false
}

Плюсы этого подхода:

  • Не нужно писать свою функцию
  • Работает для любого comparable типа (строки, числа, булевы значения, указатели)
  • Оптимизирован разработчиками Go

Минусы:

  • Не работает для структур (нужна своя логика сравнения)
  • Требует Go 1.21+ (если вы работаете на старой версии, используйте дженерики из первого ответа)

Если ваш проект использует актуальную версию Go, просто используйте slices.Contains() — это самый простой и правильный путь.