«А где же contains?» — первая боль новичка в Go
Представьте: вы пишете на Go, у вас есть слайс строк ["python", "go", "rust", "javascript"], и вам нужно проверить, есть ли в нём язык "go". Если вы пришли из Python, Ruby или JavaScript, вы на автомате пишете slice.contains("go") — и получаете красную ошибку компиляции. «Что за ерунда?» — думаете вы. «В любой уважающей себя коллекции есть метод contains!»
Спокойствие, только спокойствие. В Go действительно нет встроенного метода contains для слайсов. И это не баг, а фича. Разработчики языка решили, что одна универсальная функция не покроет все варианты (сравнение строк, чисел, структур), и оставили реализацию за программистом. Звучит страшно, но на деле всё просто и элегантно. В этой статье мы разберём, как написать contains для слайса в Go, рассмотрим производительность, типичные грабли и покажем готовые решения.
Почему в Go нет contains для слайса?
Go — минималистичный язык. В нём нет дженериков (до версии 1.18) и перегрузки методов. Слайс — это не объект с методами, а структура данных: указатель на массив, длина и ёмкость. Разработчики Go решили, что функция contains должна быть явной, чтобы программист всегда понимал, как именно происходит сравнение. Это снижает количество багов и делает код предсказуемым.
Кроме того, contains для слайса — это всегда линейный поиск (O(n)), и если вы ожидаете O(1), то используете не ту структуру (нужна map). Go не хочет маскировать эту сложность.
Самый простой contains для слайса в Go (ручной перебор)
Давайте напишем contains для слайса строк. Это классика, которую вы увидите в любом production-коде:
package main
import "fmt"
// Contains проверяет, есть ли элемент в слайсе строк
func Contains(slice []string, target string) bool {
for _, item := range slice {
if item == target {
return true
}
}
return false
}
func main() {
languages := []string{"python", "go", "rust", "javascript"}
fmt.Println(Contains(languages, "go")) // true
fmt.Println(Contains(languages, "csharp")) // false
}
Всё просто: проходим циклом по слайсу, сравниваем каждый элемент с искомым. Если нашли — true, иначе false. Важно: оператор == работает для строк, чисел, булевых значений и указателей. Для структур или слайсов внутри слайса (вложенных) так не прокатит.
Contains для чисел
Для слайса целых чисел код почти идентичен:
func ContainsInt(slice []int, target int) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
func main() {
numbers := []int{10, 20, 30, 40, 50}
fmt.Println(ContainsInt(numbers, 30)) // true
fmt.Println(ContainsInt(numbers, 99)) // false
}
Неудобно? Да. Но это Go — явность и простота превыше всего.
Универсальный contains для слайса в Go (дженерики, Go 1.18+)
С выходом Go 1.18 в языке появились дженерики (обобщения). Теперь можно написать одну функцию contains для любого типа, поддерживающего сравнение:
package main
import "fmt"
// ContainsGeneric работает для любого comparable типа
func ContainsGeneric[T comparable](slice []T, target T) bool {
for _, item := range slice {
if item == target {
return true
}
}
return false
}
func main() {
strs := []string{"a", "b", "c"}
ints := []int{1, 2, 3}
floats := []float64{1.1, 2.2, 3.3}
fmt.Println(ContainsGeneric(strs, "b")) // true
fmt.Println(ContainsGeneric(ints, 5)) // false
fmt.Println(ContainsGeneric(floats, 2.2)) // true
}
Ограничение comparable означает, что тип поддерживает операторы == и !=. Это покрывает 90% случаев: строки, числа, указатели, каналы, структуры без слайсов/мап внутри. Для структур с полями-слайсами придётся писать свою логику сравнения.
Альтернативы contains: map и sort.Search
Если вы часто проверяете наличие элементов, не используйте contains для слайса — это O(n) каждый раз. Вот два быстрых варианта:
1. Использовать map (O(1) поиск)
Преобразуйте слайс в map и проверяйте наличие ключа. Идеально для частых проверок:
package main
import "fmt"
func main() {
langs := []string{"python", "go", "rust"}
// Создаём map для быстрого поиска
set := make(map[string]struct{}, len(langs))
for _, v := range langs {
set[v] = struct{}{}
}
// Поиск
_, exists := set["go"]
fmt.Println(exists) // true
_, exists = set["java"]
fmt.Println(exists) // false
}
struct{}{} — это пустая структура, она не занимает память (0 байт). Такой трюк стандартен для Go-сообщества.
2. Отсортированный слайс + sort.Search (O(log n))
Если слайс отсортирован, можно использовать бинарный поиск из пакета sort:
package main
import (
"fmt"
"sort"
)
func main() {
numbers := []int{10, 20, 30, 40, 50}
// Слайс должен быть отсортирован!
sort.Ints(numbers)
idx := sort.Search(len(numbers), func(i int) bool {
return numbers[i] >= 30
})
if idx < len(numbers) && numbers[idx] == 30 {
fmt.Println("Найден!")
} else {
fmt.Println("Не найден")
}
}
Этот метод быстрее линейного поиска, но требует отсортированных данных.
Типичные ошибки новичков с contains в Go
- Сравнение структур с полями-слайсами.
==не работает, если структура содержит слайс, мапу или функцию. Используйтеreflect.DeepEqual(медленно!) или пишите свой компаратор. - Поиск в nil слайсе. В Go можно итерироваться по nil слайсу — цикл просто не выполнится. Но если вы передаёте nil в contains, функция вернёт false, что логично.
- Забыли про регистр строк.
"Go" != "go". Если нужно искать без учёта регистра, используйтеstrings.EqualFoldили приводите к нижнему регистру. - Проверка наличия в большом слайсе. Если слайс на 100 000 элементов, линейный поиск — плохая идея. Используйте map.