Фильтрация коллекции
Фильтрация — это выбор из коллекции тех элементов, которые подходят под условие.
Условие записывается как функция, которая принимает один элемент и возвращает true (оставить его) или false (пропустить его). Такую функцию называют предикатом.
Фильтрация создаёт новый слайс. Исходный слайс не изменяется.
В Go нет встроенного метода filter у слайсов. Фильтровать можно циклом for с if или один раз написать небольшой обобщённый хелпер и переиспользовать его. Хелпер ниже работает со слайсом любого типа.
В стандартной библиотеке есть slices.DeleteFunc(), которая удаляет элементы, подходящие под условие. Будьте внимательны: она изменяет исходный слайс, а не создаёт новый.
Синтаксис обобщённого хелпера Filter
T обозначает тип элемента. keep — это предикат: элементы, для которых он возвращает true, копируются в result.
func Filter[T any](items []T, keep func(T) bool) []T {
var result []T
for _, v := range items {
if keep(v) {
result = append(result, v)
}
}
return result
}
Фильтрация по предикату
Самый простой способ отфильтровать — передать предикат хелперу Filter. Хелпер проходит по слайсу и оставляет каждый элемент, который проходит проверку.
У map своего хелпера нет. Чтобы отфильтровать map, переберите его с помощью range и скопируйте подходящие пары ключ:значение в новый map.
Если условию нужна ещё и позиция элемента, используйте индекс, который даёт range, вместе со значением.
Чтобы оставить элементы, которые не подходят под условие, поставьте перед условием !. Отдельной функции filterNot в Go нет.
Слайс типа []any может хранить значения разных типов. Приведение типа вида v.(string) проверяет, является ли значение строкой, и возвращает его с этим типом. Указатель может быть nil, что означает «нет значения»; сравнивайте с nil, чтобы пропустить такие записи.
Фильтрация слайса
Оставляем только слова длиннее 3 букв. Условие len(s) > 3 передаётся в Filter как функция.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
// Filter возвращает новый слайс только с теми элементами, на которые keep() отвечает «да».
func Filter[T any](items []T, keep func(T) bool) []T {
var result []T
for _, v := range items {
if keep(v) {
result = append(result, v)
}
}
return result
}
func main() {
words := []string{"one", "two", "three", "four"}
longerThan3 := Filter(words, func(s string) bool { return len(s) > 3 })
fmt.Println(longerThan3)
}
Фильтрация map
Перебираем map и оставляем пары, ключ которых заканчивается на "1", а значение больше 10. strings.HasSuffix() проверяет окончание строки.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"strings"
)
func main() {
numbersMap := map[string]int{"ключ1": 1, "ключ2": 2, "ключ3": 3, "ключ11": 11}
// У map нет встроенной фильтрации, поэтому перебираем его и копируем подходящие пары в новый map.
filteredMap := map[string]int{}
for key, value := range numbersMap {
if strings.HasSuffix(key, "1") && value > 10 {
filteredMap[key] = value
}
}
fmt.Println(filteredMap)
}
Фильтрация по индексу
range даёт индекс и значение. Оставляем слово только тогда, когда его индекс не равен 0 и в нём меньше 5 букв.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
func main() {
numbers := []string{"one", "two", "three", "four"}
// range даёт и индекс, и значение, поэтому условие может использовать оба.
var filteredIdx []string
for index, s := range numbers {
if index != 0 && len(s) < 5 {
filteredIdx = append(filteredIdx, s)
}
}
fmt.Println(filteredIdx)
}
Фильтрация с отрицанием условия
Отбрасываем слова из 3 букв или короче. Условие len(s) = 3 инвертируется с помощью !, поэтому остаются только более длинные слова.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
func Filter[T any](items []T, keep func(T) bool) []T {
var result []T
for _, v := range items {
if keep(v) {
result = append(result, v)
}
}
return result
}
func main() {
numbers := []string{"one", "two", "three", "four"}
// В Go нет filterNot: инвертируем условие с помощью ! и используем Filter.
filteredNot := Filter(numbers, func(s string) bool { return !(len(s) <= 3) })
fmt.Println(filteredNot)
}
Фильтрация по типу
Слайс содержит nil, целое число, две строки и число с плавающей точкой. Приведение типа v.(string) вместе со значением ok находит строки. Каждая из них печатается заглавными буквами.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"strings"
)
func main() {
numbers := []any{nil, 1, "two", 3.0, "four"}
// v.(string) спрашивает: является ли это значение строкой? ok равен true, только если да.
for _, v := range numbers {
if s, ok := v.(string); ok {
fmt.Println(strings.ToUpper(s))
}
}
}
Отсеивание значений nil
Слайс содержит указатели на строки, и некоторые из них равны nil. Оставляем указатели, которые не равны nil, и собираем значения, на которые они указывают, с помощью *p.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
func main() {
one, two := "one", "two"
// Указатель может быть nil, что означает «нет значения».
nullableList := []*string{nil, &one, nil, &two}
var values []string
for _, p := range nullableList {
if p != nil {
values = append(values, *p)
}
}
fmt.Println(values)
}
Разделение на две части
Иногда нужны обе группы: и элементы, которые подходят под условие, и те, которые не подходят.
В Go нет встроенного partition. Это делает небольшой обобщённый хелпер: он проходит по слайсу один раз и добавляет каждый элемент либо в слайс match, либо в слайс rest. Хелпер возвращает оба слайса, потому что функция в Go может возвращать больше одного значения.
Разделение слайса надвое
Слова длиннее 3 букв попадают в match, все остальные — в rest. Печатаются оба слайса.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
// Partition разделяет items на два слайса: подходящие элементы и все остальные.
func Partition[T any](items []T, test func(T) bool) ([]T, []T) {
var match, rest []T
for _, v := range items {
if test(v) {
match = append(match, v)
} else {
rest = append(rest, v)
}
}
return match, rest
}
func main() {
numbers := []string{"one", "two", "three", "four"}
match, rest := Partition(numbers, func(s string) bool { return len(s) > 3 })
fmt.Println(match)
fmt.Println(rest)
}
Проверка предикатов
Иногда нужны не сами подходящие элементы, а только ответ «да» или «нет».
- any: проходит ли проверку хотя бы один элемент? На это отвечает
slices.ContainsFunc(). - none: не проходит ли проверку ни один элемент? Поставьте
!передslices.ContainsFunc(). - all: проходит ли проверку каждый элемент? В Go нет функции для этого, поэтому используйте небольшой обобщённый хелпер
All.
Без условия вопрос «есть ли хоть один элемент?» — это len(s) > 0, а «нет ни одного элемента?» — это len(s) == 0.
На пустом слайсе All возвращает true. Нет ни одного элемента, который не прошёл бы проверку, поэтому условие ни разу не нарушено. Это называется тривиальной истинностью.
any
Проверяем, заканчивается ли хотя бы одно слово на "e", а затем проверяем, есть ли в слайсе хоть один элемент.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"slices"
"strings"
)
func main() {
numbers := []string{"one", "two", "three", "four"}
// slices.ContainsFunc возвращает true, если проверку проходит хотя бы один элемент.
fmt.Println(slices.ContainsFunc(numbers, func(s string) bool { return strings.HasSuffix(s, "e") }))
// «Есть ли хоть один элемент?» — это просто проверка длины.
fmt.Println(len(numbers) > 0)
}
none
Проверяем, что ни одно слово не заканчивается на "a", а затем проверяем, пуст ли слайс.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"slices"
"strings"
)
func main() {
numbers := []string{"one", "two", "three", "four"}
// «none» — противоположность «any»: проверку не проходит ни один элемент.
fmt.Println(!slices.ContainsFunc(numbers, func(s string) bool { return strings.HasSuffix(s, "a") }))
// «Нет ни одного элемента?» — это просто проверка длины.
fmt.Println(len(numbers) == 0)
}
all
Проверяем, заканчивается ли каждое слово на "e". Затем вызываем All на пустом слайсе, что даёт true.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"strings"
)
// All равно true, только если проверку проходит каждый элемент.
func All[T any](items []T, test func(T) bool) bool {
for _, v := range items {
if !test(v) {
return false
}
}
return true
}
func main() {
numbers := []string{"one", "two", "three", "four"}
empty := []string{}
fmt.Println(All(numbers, func(s string) bool { return strings.HasSuffix(s, "e") }))
// В пустом слайсе нет элемента, который не прошёл бы проверку, поэтому All равно true.
fmt.Println(All(empty, func(s string) bool { return strings.HasSuffix(s, "e") }))
}