HardПрактика10 min

Паттерны с дженериками

Обобщённые структуры данных, утилитарные функции, Result/Optional, стандартная библиотека

Обобщённые структуры данных

Stack[T] -- стек

package main

import (
    "errors"
    "fmt"
)

var ErrEmpty = errors.New("collection is empty")

// Stack is a generic LIFO container
type Stack[T any] struct {
    items []T
}

func NewStack[T any](capacity int) *Stack[T] {
    return &Stack[T]{items: make([]T, 0, capacity)}
}

func (s *Stack[T]) Push(vals ...T) {
    s.items = append(s.items, vals...)
}

func (s *Stack[T]) Pop() (T, error) {
    if len(s.items) == 0 {
        var zero T
        return zero, ErrEmpty
    }
    last := len(s.items) - 1
    val := s.items[last]
    s.items = s.items[:last]
    return val, nil
}

func (s *Stack[T]) Peek() (T, error) {
    if len(s.items) == 0 {
        var zero T
        return zero, ErrEmpty
    }
    return s.items[len(s.items)-1], nil
}

func (s *Stack[T]) Len() int   { return len(s.items) }
func (s *Stack[T]) Empty() bool { return len(s.items) == 0 }

func main() {
    s := NewStack[int](10)
    s.Push(1, 2, 3, 4, 5)

    for !s.Empty() {
        val, _ := s.Pop()
        fmt.Printf("%d ", val) // 5 4 3 2 1
    }
    fmt.Println()
}

Queue[T] -- очередь

package main

import "fmt"

// Queue is a generic FIFO container using ring buffer
type Queue[T any] struct {
    items []T
    head  int
    tail  int
    count int
}

func NewQueue[T any](capacity int) *Queue[T] {
    return &Queue[T]{items: make([]T, capacity)}
}

func (q *Queue[T]) Enqueue(val T) {
    if q.count == len(q.items) {
        q.grow()
    }
    q.items[q.tail] = val
    q.tail = (q.tail + 1) % len(q.items)
    q.count++
}

func (q *Queue[T]) Dequeue() (T, bool) {
    if q.count == 0 {
        var zero T
        return zero, false
    }
    val := q.items[q.head]
    var zero T
    q.items[q.head] = zero // help GC
    q.head = (q.head + 1) % len(q.items)
    q.count--
    return val, true
}

func (q *Queue[T]) Len() int   { return q.count }
func (q *Queue[T]) Empty() bool { return q.count == 0 }

func (q *Queue[T]) grow() {
    newCap := len(q.items) * 2
    if newCap == 0 {
        newCap = 8
    }
    newItems := make([]T, newCap)
    for i := range q.count {
        newItems[i] = q.items[(q.head+i)%len(q.items)]
    }
    q.items = newItems
    q.head = 0
    q.tail = q.count
}

func main() {
    q := NewQueue[string](4)
    q.Enqueue("first")
    q.Enqueue("second")
    q.Enqueue("third")

    for !q.Empty() {
        val, _ := q.Dequeue()
        fmt.Println(val) // first, second, third
    }
}

LinkedList[T] -- связанный список

package main

import "fmt"

// Node is a single element in the linked list
type Node[T any] struct {
    Value T
    Next  *Node[T]
}

// LinkedList is a generic singly-linked list
type LinkedList[T any] struct {
    head *Node[T]
    tail *Node[T]
    len  int
}

// Append adds element to the end
func (l *LinkedList[T]) Append(val T) {
    node := &Node[T]{Value: val}
    if l.tail == nil {
        l.head = node
        l.tail = node
    } else {
        l.tail.Next = node
        l.tail = node
    }
    l.len++
}

// Prepend adds element to the beginning
func (l *LinkedList[T]) Prepend(val T) {
    node := &Node[T]{Value: val, Next: l.head}
    l.head = node
    if l.tail == nil {
        l.tail = node
    }
    l.len++
}

// ToSlice converts list to slice
func (l *LinkedList[T]) ToSlice() []T {
    result := make([]T, 0, l.len)
    for n := l.head; n != nil; n = n.Next {
        result = append(result, n.Value)
    }
    return result
}

// ForEach iterates over all elements
func (l *LinkedList[T]) ForEach(fn func(T)) {
    for n := l.head; n != nil; n = n.Next {
        fn(n.Value)
    }
}

func (l *LinkedList[T]) Len() int { return l.len }

func main() {
    list := &LinkedList[int]{}
    list.Append(1)
    list.Append(2)
    list.Append(3)
    list.Prepend(0)

    fmt.Println(list.ToSlice()) // [0 1 2 3]

    list.ForEach(func(v int) {
        fmt.Printf("%d ", v) // 0 1 2 3
    })
    fmt.Println()
}

Утилитарные функции

Map, Filter, Reduce

package main

import "fmt"

// Map transforms each element of a slice
func Map[T, U any](s []T, fn func(T) U) []U {
    result := make([]U, len(s))
    for i, v := range s {
        result[i] = fn(v)
    }
    return result
}

// Filter returns elements matching predicate
func Filter[T any](s []T, fn func(T) bool) []T {
    result := make([]T, 0, len(s)/2) // estimate half will match
    for _, v := range s {
        if fn(v) {
            result = append(result, v)
        }
    }
    return result
}

// Reduce accumulates a value over a slice
func Reduce[T, U any](s []T, initial U, fn func(U, T) U) U {
    acc := initial
    for _, v := range s {
        acc = fn(acc, v)
    }
    return acc
}

// Contains checks if slice contains an element
func Contains[T comparable](s []T, target T) bool {
    for _, v := range s {
        if v == target {
            return true
        }
    }
    return false
}

// Index returns the first index of target, or -1
func Index[T comparable](s []T, target T) int {
    for i, v := range s {
        if v == target {
            return i
        }
    }
    return -1
}

// Unique returns a new slice with duplicates removed
func Unique[T comparable](s []T) []T {
    seen := make(map[T]struct{}, len(s))
    result := make([]T, 0, len(s))
    for _, v := range s {
        if _, ok := seen[v]; !ok {
            seen[v] = struct{}{}
            result = append(result, v)
        }
    }
    return result
}

func main() {
    nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    // Map: double each number
    doubled := Map(nums, func(n int) int { return n * 2 })
    fmt.Println("Doubled:", doubled)

    // Filter: keep evens
    evens := Filter(nums, func(n int) bool { return n%2 == 0 })
    fmt.Println("Evens:", evens)

    // Reduce: sum
    sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
    fmt.Println("Sum:", sum) // 55

    // Reduce: join strings
    words := []string{"Hello", "World", "Go"}
    joined := Reduce(words, "", func(acc, s string) string {
        if acc == "" {
            return s
        }
        return acc + " " + s
    })
    fmt.Println("Joined:", joined) // Hello World Go

    // Contains
    fmt.Println("Contains 5:", Contains(nums, 5))   // true
    fmt.Println("Contains 11:", Contains(nums, 11)) // false

    // Unique
    dupes := []int{1, 2, 2, 3, 3, 3, 4}
    fmt.Println("Unique:", Unique(dupes)) // [1 2 3 4]
}

Keys, Values для карт

package main

import (
    "fmt"
    "sort"
)

// Keys returns all keys of a map
func Keys[K comparable, V any](m map[K]V) []K {
    keys := make([]K, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

// Values returns all values of a map
func Values[K comparable, V any](m map[K]V) []V {
    vals := make([]V, 0, len(m))
    for _, v := range m {
        vals = append(vals, v)
    }
    return vals
}

// Invert swaps keys and values
func Invert[K, V comparable](m map[K]V) map[V]K {
    result := make(map[V]K, len(m))
    for k, v := range m {
        result[v] = k
    }
    return result
}

// Merge combines multiple maps (later maps override earlier)
func Merge[K comparable, V any](maps ...map[K]V) map[K]V {
    result := make(map[K]V)
    for _, m := range maps {
        for k, v := range m {
            result[k] = v
        }
    }
    return result
}

func main() {
    users := map[int]string{
        1: "Alice",
        2: "Bob",
        3: "Charlie",
    }

    keys := Keys(users)
    sort.Ints(keys)
    fmt.Println("Keys:", keys)     // [1 2 3]
    fmt.Println("Values:", Values(users))

    inverted := Invert(users)
    fmt.Println("Inverted:", inverted) // map[Alice:1 Bob:2 Charlie:3]

    defaults := map[string]int{"timeout": 30, "retries": 3}
    overrides := map[string]int{"timeout": 60}
    merged := Merge(defaults, overrides)
    fmt.Println("Merged:", merged) // map[retries:3 timeout:60]
}

Result[T] -- тип результата

Паттерн, вдохновлённый Rust, для работы с результатами, которые могут быть ошибкой:

package main

import (
    "errors"
    "fmt"
    "strconv"
)

// Result represents a value-or-error
type Result[T any] struct {
    value T
    err   error
}

// Ok creates a successful result
func Ok[T any](val T) Result[T] {
    return Result[T]{value: val}
}

// Err creates an error result
func Err[T any](err error) Result[T] {
    return Result[T]{err: err}
}

// IsOk returns true if result is successful
func (r Result[T]) IsOk() bool {
    return r.err == nil
}

// IsErr returns true if result is an error
func (r Result[T]) IsErr() bool {
    return r.err != nil
}

// Unwrap returns the value or panics
func (r Result[T]) Unwrap() T {
    if r.err != nil {
        panic(fmt.Sprintf("unwrap on error result: %v", r.err))
    }
    return r.value
}

// UnwrapOr returns the value or a default
func (r Result[T]) UnwrapOr(defaultVal T) T {
    if r.err != nil {
        return defaultVal
    }
    return r.value
}

// Error returns the error (or nil)
func (r Result[T]) Error() error {
    return r.err
}

// Map transforms the value if Ok
func MapResult[T, U any](r Result[T], fn func(T) U) Result[U] {
    if r.err != nil {
        return Err[U](r.err)
    }
    return Ok(fn(r.value))
}

// AndThen chains operations that may fail
func AndThen[T, U any](r Result[T], fn func(T) Result[U]) Result[U] {
    if r.err != nil {
        return Err[U](r.err)
    }
    return fn(r.value)
}

func main() {
    // Parse chain: string → int → validate → double
    parseAge := func(s string) Result[int] {
        n, err := strconv.Atoi(s)
        if err != nil {
            return Err[int](fmt.Errorf("invalid age: %w", err))
        }
        return Ok(n)
    }

    validate := func(age int) Result[int] {
        if age < 0 || age > 150 {
            return Err[int](errors.New("age out of range"))
        }
        return Ok(age)
    }

    // Success path
    result := AndThen(parseAge("25"), validate)
    doubled := MapResult(result, func(n int) string {
        return fmt.Sprintf("Age doubled: %d", n*2)
    })
    fmt.Println(doubled.Unwrap()) // Age doubled: 50

    // Error path
    result2 := AndThen(parseAge("abc"), validate)
    fmt.Println(result2.UnwrapOr(0)) // 0
    fmt.Println(result2.Error())      // invalid age: strconv.Atoi: ...

    // Validation error
    result3 := AndThen(parseAge("200"), validate)
    fmt.Println(result3.Error()) // age out of range
}

Optional[T] -- опциональный тип

package main

import "fmt"

// Optional represents a value that may or may not exist
type Optional[T any] struct {
    value T
    valid bool
}

// Some creates a present Optional
func Some[T any](val T) Optional[T] {
    return Optional[T]{value: val, valid: true}
}

// None creates an absent Optional
func None[T any]() Optional[T] {
    return Optional[T]{}
}

// IsPresent returns true if value exists
func (o Optional[T]) IsPresent() bool {
    return o.valid
}

// Get returns the value or false
func (o Optional[T]) Get() (T, bool) {
    return o.value, o.valid
}

// OrElse returns the value or a default
func (o Optional[T]) OrElse(defaultVal T) T {
    if o.valid {
        return o.value
    }
    return defaultVal
}

// MapOptional transforms the value if present
func MapOptional[T, U any](o Optional[T], fn func(T) U) Optional[U] {
    if !o.valid {
        return None[U]()
    }
    return Some(fn(o.value))
}

// FlatMap chains operations that return Optional
func FlatMap[T, U any](o Optional[T], fn func(T) Optional[U]) Optional[U] {
    if !o.valid {
        return None[U]()
    }
    return fn(o.value)
}

func main() {
    // Find user by ID
    findUser := func(id int) Optional[string] {
        users := map[int]string{1: "Alice", 2: "Bob"}
        if name, ok := users[id]; ok {
            return Some(name)
        }
        return None[string]()
    }

    // Success
    user := findUser(1)
    if name, ok := user.Get(); ok {
        fmt.Println("Found:", name) // Found: Alice
    }

    // Not found — with default
    unknown := findUser(999)
    fmt.Println("Name:", unknown.OrElse("Guest")) // Name: Guest

    // Chaining
    greeting := MapOptional(findUser(2), func(name string) string {
        return "Hello, " + name + "!"
    })
    fmt.Println(greeting.OrElse("Hello, stranger!")) // Hello, Bob!
}

Стандартная библиотека: slices и maps (Go 1.21+)

Начиная с Go 1.21, стандартная библиотека включает пакеты slices и maps с обобщёнными функциями:

package main

import (
    "cmp"
    "fmt"
    "maps"
    "slices"
)

func main() {
    // === slices package ===
    nums := []int{3, 1, 4, 1, 5, 9, 2, 6}

    // Sort
    slices.Sort(nums)
    fmt.Println("Sorted:", nums) // [1 1 2 3 4 5 6 9]

    // Sort with custom comparison
    slices.SortFunc(nums, func(a, b int) int {
        return cmp.Compare(b, a) // reverse order
    })
    fmt.Println("Reverse:", nums) // [9 6 5 4 3 2 1 1]

    // Contains
    fmt.Println("Contains 5:", slices.Contains(nums, 5)) // true

    // Index
    fmt.Println("Index of 4:", slices.Index(nums, 4)) // 4

    // Min, Max
    fmt.Println("Min:", slices.Min(nums)) // 1
    fmt.Println("Max:", slices.Max(nums)) // 9

    // Compact (remove consecutive duplicates from sorted)
    sorted := []int{1, 1, 2, 2, 3, 3, 3}
    unique := slices.Compact(sorted)
    fmt.Println("Compact:", unique) // [1 2 3]

    // Clip (free unused capacity)
    clipped := slices.Clip(unique)
    fmt.Printf("Clip: len=%d cap=%d\n", len(clipped), cap(clipped))

    // === maps package ===
    m := map[string]int{
        "go":     2009,
        "rust":   2010,
        "python": 1991,
    }

    // Keys and Values
    keys := slices.Sorted(maps.Keys(m))
    fmt.Println("Keys:", keys) // [go python rust]

    vals := slices.Collect(maps.Values(m))
    fmt.Println("Values:", vals)

    // Clone
    clone := maps.Clone(m)
    clone["java"] = 1995
    fmt.Println("Original:", m)    // no "java"
    fmt.Println("Clone:", clone)   // has "java"

    // Equal
    fmt.Println("Equal:", maps.Equal(m, m)) // true

    // DeleteFunc
    maps.DeleteFunc(clone, func(k string, v int) bool {
        return v < 2000 // delete languages from before 2000
    })
    fmt.Println("After delete:", clone) // [go:2009 rust:2010]

    // Collect — create map from iterator (Go 1.23+)
    // maps.Collect(iter)
}

Итераторы и slices (Go 1.23+)

package main

import (
    "fmt"
    "slices"
)

func main() {
    nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    // Chunk splits into sub-slices
    for chunk := range slices.Chunk(nums, 3) {
        fmt.Println(chunk)
    }
    // [1 2 3]
    // [4 5 6]
    // [7 8 9]
    // [10]

    // slices.All — iterator over index-value pairs (Go 1.23+)
    for i, v := range slices.All(nums) {
        if i >= 3 {
            break
        }
        fmt.Printf("  [%d]=%d\n", i, v)
    }

    // slices.Backward — reverse iterator (Go 1.23+)
    fmt.Println("Backward:")
    for _, v := range slices.Backward(nums[:5]) {
        fmt.Printf("%d ", v) // 5 4 3 2 1
    }
    fmt.Println()
}

Проверь себя

Какую функцию из пакета slices (Go 1.21+) можно использовать для удаления последовательных дубликатов из отсортированного слайса?

Чем Result[T] отличается от стандартного возврата (T, error) в Go?

Какой generic constraint нужен для функции Contains[T](slice []T, target T) bool?

С какой версии Go пакеты slices и maps стали частью стандартной библиотеки?