Home » Как использовать дженерики в Go — объяснение параметров типов
Как использовать дженерики в Go — объяснение параметров типов

Как использовать дженерики в Go — объяснение параметров типов

Дженерики в Go — это то, что Go-разработчики ждали целых 10 лет, и наконец-то получили в версии 1.18. Если ты админишь серверы и пишешь скрипты для автоматизации, то понимание дженериков может серьёзно упростить твою жизнь. Больше никаких костылей с `interface{}`, никаких дублирований кода для разных типов данных, никаких тайп-ассертов в каждой второй строке.

Статья поможет тебе разобраться, как использовать параметры типов в Go для создания более гибких и безопасных скриптов автоматизации. Покажу конкретные примеры того, как дженерики могут упростить обработку конфигов, работу с API и создание универсальных утилит для администрирования.

Как работают дженерики в Go

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

Базовый синтаксис выглядит так:

func MyFunction[T any](param T) T {
    return param
}

// Использование
result := MyFunction[string]("hello")
number := MyFunction[int](42)

Ключевые концепции:

  • Параметры типов — определяются в квадратных скобках после имени функции
  • Ограничения типов — что может быть этим типом (например, `any`, `comparable`)
  • Инстанциация — создание конкретной версии дженерик-функции с определённым типом

Практические примеры для админов

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

Универсальная функция для чтения конфигов

package main

import (
    "encoding/json"
    "fmt"
    "os"
)

// Дженерик-функция для чтения любых JSON-конфигов
func ReadConfig[T any](filename string) (T, error) {
    var config T
    
    data, err := os.ReadFile(filename)
    if err != nil {
        return config, err
    }
    
    err = json.Unmarshal(data, &config)
    return config, err
}

// Структуры для разных типов конфигов
type ServerConfig struct {
    Host string `json:"host"`
    Port int    `json:"port"`
    SSL  bool   `json:"ssl"`
}

type DatabaseConfig struct {
    Driver   string `json:"driver"`
    Host     string `json:"host"`
    Database string `json:"database"`
    Username string `json:"username"`
    Password string `json:"password"`
}

func main() {
    // Читаем конфиг сервера
    serverCfg, err := ReadConfig[ServerConfig]("server.json")
    if err != nil {
        fmt.Printf("Error reading server config: %v\n", err)
        return
    }
    
    // Читаем конфиг базы данных
    dbCfg, err := ReadConfig[DatabaseConfig]("database.json")
    if err != nil {
        fmt.Printf("Error reading database config: %v\n", err)
        return
    }
    
    fmt.Printf("Server: %s:%d, SSL: %v\n", serverCfg.Host, serverCfg.Port, serverCfg.SSL)
    fmt.Printf("Database: %s@%s/%s\n", dbCfg.Username, dbCfg.Host, dbCfg.Database)
}

Универсальная функция для работы с API

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

// Дженерик-клиент для API
type APIClient struct {
    BaseURL string
    Client  *http.Client
}

func NewAPIClient(baseURL string, timeout time.Duration) *APIClient {
    return &APIClient{
        BaseURL: baseURL,
        Client: &http.Client{
            Timeout: timeout,
        },
    }
}

// Дженерик-метод для GET-запросов
func (c *APIClient) Get[T any](endpoint string) (T, error) {
    var result T
    
    resp, err := c.Client.Get(c.BaseURL + endpoint)
    if err != nil {
        return result, err
    }
    defer resp.Body.Close()
    
    if resp.StatusCode != http.StatusOK {
        return result, fmt.Errorf("API returned status %d", resp.StatusCode)
    }
    
    err = json.NewDecoder(resp.Body).Decode(&result)
    return result, err
}

// Дженерик-метод для POST-запросов
func (c *APIClient) Post[T any, R any](endpoint string, data T) (R, error) {
    var result R
    
    jsonData, err := json.Marshal(data)
    if err != nil {
        return result, err
    }
    
    resp, err := c.Client.Post(c.BaseURL+endpoint, "application/json", bytes.NewBuffer(jsonData))
    if err != nil {
        return result, err
    }
    defer resp.Body.Close()
    
    if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
        return result, fmt.Errorf("API returned status %d", resp.StatusCode)
    }
    
    err = json.NewDecoder(resp.Body).Decode(&result)
    return result, err
}

// Пример использования
type ServerStatus struct {
    Name   string `json:"name"`
    Status string `json:"status"`
    CPU    float64 `json:"cpu"`
    Memory float64 `json:"memory"`
}

type RestartRequest struct {
    ServerName string `json:"server_name"`
    Force      bool   `json:"force"`
}

type RestartResponse struct {
    Success bool   `json:"success"`
    Message string `json:"message"`
}

func main() {
    client := NewAPIClient("https://api.example.com", 30*time.Second)
    
    // Получаем статус сервера
    status, err := client.Get[ServerStatus]("/servers/web-01/status")
    if err != nil {
        fmt.Printf("Error getting server status: %v\n", err)
        return
    }
    
    fmt.Printf("Server: %s, Status: %s, CPU: %.2f%%, Memory: %.2f%%\n", 
        status.Name, status.Status, status.CPU, status.Memory)
    
    // Перезапускаем сервер если CPU > 90%
    if status.CPU > 90 {
        restart := RestartRequest{
            ServerName: "web-01",
            Force:      true,
        }
        
        response, err := client.Post[RestartRequest, RestartResponse]("/servers/restart", restart)
        if err != nil {
            fmt.Printf("Error restarting server: %v\n", err)
            return
        }
        
        fmt.Printf("Restart result: %v - %s\n", response.Success, response.Message)
    }
}

Создание универсальных коллекций

Одна из самых полезных областей применения дженериков — создание типобезопасных коллекций. Вот пример стека, который может пригодиться в скриптах:

package main

import (
    "fmt"
    "sync"
)

// Потокобезопасный дженерик-стек
type Stack[T any] struct {
    items []T
    mutex sync.RWMutex
}

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

func (s *Stack[T]) Push(item T) {
    s.mutex.Lock()
    defer s.mutex.Unlock()
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    s.mutex.Lock()
    defer s.mutex.Unlock()
    
    var zero T
    if len(s.items) == 0 {
        return zero, false
    }
    
    index := len(s.items) - 1
    item := s.items[index]
    s.items = s.items[:index]
    return item, true
}

func (s *Stack[T]) Peek() (T, bool) {
    s.mutex.RLock()
    defer s.mutex.RUnlock()
    
    var zero T
    if len(s.items) == 0 {
        return zero, false
    }
    
    return s.items[len(s.items)-1], true
}

func (s *Stack[T]) Size() int {
    s.mutex.RLock()
    defer s.mutex.RUnlock()
    return len(s.items)
}

func (s *Stack[T]) IsEmpty() bool {
    s.mutex.RLock()
    defer s.mutex.RUnlock()
    return len(s.items) == 0
}

// Пример использования для обработки задач
type Task struct {
    ID          int
    Name        string
    Priority    int
    CreatedAt   time.Time
}

func main() {
    // Стек для задач
    taskStack := NewStack[Task]()
    
    // Добавляем задачи
    taskStack.Push(Task{1, "Backup database", 1, time.Now()})
    taskStack.Push(Task{2, "Update packages", 2, time.Now()})
    taskStack.Push(Task{3, "Restart services", 3, time.Now()})
    
    // Обрабатываем задачи в порядке LIFO
    for !taskStack.IsEmpty() {
        task, _ := taskStack.Pop()
        fmt.Printf("Processing task: %s (Priority: %d)\n", task.Name, task.Priority)
    }
    
    // Стек для строк (например, для логов)
    logStack := NewStack[string]()
    logStack.Push("INFO: Server started")
    logStack.Push("WARN: High CPU usage")
    logStack.Push("ERROR: Database connection failed")
    
    // Выводим последние логи
    for !logStack.IsEmpty() {
        log, _ := logStack.Pop()
        fmt.Println(log)
    }
}

Ограничения типов и интерфейсы

Самое мощное в дженериках — это возможность ограничить типы, которые можно использовать. Это позволяет создавать более специализированные и безопасные функции:

package main

import (
    "fmt"
    "golang.org/x/exp/constraints"
)

// Дженерик-функция для работы только с числами
func Max[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// Дженерик-функция для суммы слайса чисел
func Sum[T constraints.Signed | constraints.Unsigned | constraints.Float](slice []T) T {
    var sum T
    for _, v := range slice {
        sum += v
    }
    return sum
}

// Интерфейс для объектов, которые можно очистить
type Cleaner interface {
    Clean() error
}

// Дженерик-функция для пакетной очистки
func CleanAll[T Cleaner](items []T) []error {
    var errors []error
    for _, item := range items {
        if err := item.Clean(); err != nil {
            errors = append(errors, err)
        }
    }
    return errors
}

// Примеры структур, реализующих Cleaner
type TempFile struct {
    Path string
}

func (tf *TempFile) Clean() error {
    fmt.Printf("Cleaning temp file: %s\n", tf.Path)
    // Здесь был бы код удаления файла
    return nil
}

type CacheEntry struct {
    Key string
}

func (ce *CacheEntry) Clean() error {
    fmt.Printf("Cleaning cache entry: %s\n", ce.Key)
    // Здесь был бы код очистки кэша
    return nil
}

func main() {
    // Работа с числами
    fmt.Printf("Max of 10 and 20: %d\n", Max(10, 20))
    fmt.Printf("Max of 3.14 and 2.71: %.2f\n", Max(3.14, 2.71))
    
    // Сумма слайса
    intSlice := []int{1, 2, 3, 4, 5}
    fmt.Printf("Sum of %v: %d\n", intSlice, Sum(intSlice))
    
    floatSlice := []float64{1.1, 2.2, 3.3}
    fmt.Printf("Sum of %v: %.2f\n", floatSlice, Sum(floatSlice))
    
    // Пакетная очистка
    tempFiles := []*TempFile{
        {Path: "/tmp/file1.tmp"},
        {Path: "/tmp/file2.tmp"},
    }
    
    cacheEntries := []*CacheEntry{
        {Key: "user:123"},
        {Key: "session:abc"},
    }
    
    // Очищаем файлы
    if errors := CleanAll(tempFiles); len(errors) > 0 {
        fmt.Printf("Errors cleaning temp files: %v\n", errors)
    }
    
    // Очищаем кэш
    if errors := CleanAll(cacheEntries); len(errors) > 0 {
        fmt.Printf("Errors cleaning cache: %v\n", errors)
    }
}

Дженерики в структурах и методах

Дженерики можно использовать не только в функциях, но и в структурах. Это открывает возможности для создания универсальных контейнеров и сервисов:

package main

import (
    "fmt"
    "sync"
    "time"
)

// Дженерик-кэш с TTL
type Cache[K comparable, V any] struct {
    items map[K]cacheItem[V]
    mutex sync.RWMutex
    defaultTTL time.Duration
}

type cacheItem[V any] struct {
    value      V
    expiration time.Time
}

func NewCache[K comparable, V any](defaultTTL time.Duration) *Cache[K, V] {
    cache := &Cache[K, V]{
        items:      make(map[K]cacheItem[V]),
        defaultTTL: defaultTTL,
    }
    
    // Запускаем горутину для очистки устаревших элементов
    go cache.cleanup()
    
    return cache
}

func (c *Cache[K, V]) Set(key K, value V, ttl ...time.Duration) {
    c.mutex.Lock()
    defer c.mutex.Unlock()
    
    expiration := time.Now().Add(c.defaultTTL)
    if len(ttl) > 0 {
        expiration = time.Now().Add(ttl[0])
    }
    
    c.items[key] = cacheItem[V]{
        value:      value,
        expiration: expiration,
    }
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
    c.mutex.RLock()
    defer c.mutex.RUnlock()
    
    item, exists := c.items[key]
    if !exists {
        var zero V
        return zero, false
    }
    
    if time.Now().After(item.expiration) {
        var zero V
        return zero, false
    }
    
    return item.value, true
}

func (c *Cache[K, V]) Delete(key K) {
    c.mutex.Lock()
    defer c.mutex.Unlock()
    delete(c.items, key)
}

func (c *Cache[K, V]) cleanup() {
    ticker := time.NewTicker(time.Minute)
    defer ticker.Stop()
    
    for range ticker.C {
        c.mutex.Lock()
        now := time.Now()
        for key, item := range c.items {
            if now.After(item.expiration) {
                delete(c.items, key)
            }
        }
        c.mutex.Unlock()
    }
}

func (c *Cache[K, V]) Size() int {
    c.mutex.RLock()
    defer c.mutex.RUnlock()
    return len(c.items)
}

// Пример использования
type ServerInfo struct {
    Name      string
    IP        string
    Status    string
    LastCheck time.Time
}

type UserSession struct {
    UserID    int
    Username  string
    LoginTime time.Time
}

func main() {
    // Кэш для информации о серверах
    serverCache := NewCache[string, ServerInfo](5 * time.Minute)
    
    // Добавляем информацию о серверах
    serverCache.Set("web-01", ServerInfo{
        Name:      "web-01",
        IP:        "192.168.1.10",
        Status:    "running",
        LastCheck: time.Now(),
    })
    
    serverCache.Set("db-01", ServerInfo{
        Name:      "db-01", 
        IP:        "192.168.1.20",
        Status:    "running",
        LastCheck: time.Now(),
    })
    
    // Получаем информацию о сервере
    if info, exists := serverCache.Get("web-01"); exists {
        fmt.Printf("Server: %s (%s) - %s\n", info.Name, info.IP, info.Status)
    }
    
    // Кэш для сессий пользователей
    sessionCache := NewCache[string, UserSession](30 * time.Minute)
    
    sessionCache.Set("session_abc123", UserSession{
        UserID:    1,
        Username:  "admin",
        LoginTime: time.Now(),
    })
    
    // Проверяем сессию
    if session, exists := sessionCache.Get("session_abc123"); exists {
        fmt.Printf("User: %s (ID: %d) logged in at %s\n", 
            session.Username, session.UserID, session.LoginTime.Format(time.RFC3339))
    }
    
    fmt.Printf("Server cache size: %d\n", serverCache.Size())
    fmt.Printf("Session cache size: %d\n", sessionCache.Size())
}

Сравнение с другими подходами

Давайте сравним дженерики с традиционными подходами в Go:

Подход Преимущества Недостатки Когда использовать
Дженерики • Типобезопасность
• Переиспользование кода
• Производительность
• Читаемость
• Сложность синтаксиса
• Увеличение времени компиляции
• Возможная over-инженерия
• Универсальные коллекции
• API-клиенты
• Математические функции
interface{} • Простота
• Гибкость
• Быстрая компиляция
• Потеря типобезопасности
• Необходимость type assertion
• Runtime ошибки
• Быстрые прототипы
• Неопределённые типы данных
Дублирование кода • Полная типобезопасность
• Оптимальная производительность
• Простота отладки
• Много кода
• Сложность поддержки
• Высокая вероятность ошибок
• Критичные к производительности участки
• Специализированные функции
Кодогенерация • Типобезопасность
• Оптимальная производительность
• Автоматизация
• Сложность сборки
• Дополнительные зависимости
• Трудность отладки
• Большие проекты
• Стандартизированные паттерны

Работа с ошибками и дженерики

Дженерики отлично подходят для создания универсальных обработчиков ошибок и результатов:

package main

import (
    "fmt"
    "strconv"
    "strings"
)

// Дженерик-тип Result для работы с ошибками в стиле Rust
type Result[T any] struct {
    value T
    err   error
}

func Ok[T any](value T) Result[T] {
    return Result[T]{value: value}
}

func Err[T any](err error) Result[T] {
    return Result[T]{err: err}
}

func (r Result[T]) IsOk() bool {
    return r.err == nil
}

func (r Result[T]) IsErr() bool {
    return r.err != nil
}

func (r Result[T]) Unwrap() T {
    if r.err != nil {
        panic(r.err)
    }
    return r.value
}

func (r Result[T]) UnwrapOr(defaultValue T) T {
    if r.err != nil {
        return defaultValue
    }
    return r.value
}

func (r Result[T]) Error() error {
    return r.err
}

// Map применяет функцию к значению, если нет ошибки
func Map[T, U any](r Result[T], f func(T) U) Result[U] {
    if r.IsErr() {
        return Err[U](r.err)
    }
    return Ok(f(r.value))
}

// FlatMap применяет функцию, которая возвращает Result
func FlatMap[T, U any](r Result[T], f func(T) Result[U]) Result[U] {
    if r.IsErr() {
        return Err[U](r.err)
    }
    return f(r.value)
}

// Примеры функций, возвращающих Result
func ParseInt(s string) Result[int] {
    value, err := strconv.Atoi(s)
    if err != nil {
        return Err[int](err)
    }
    return Ok(value)
}

func Divide(a, b int) Result[float64] {
    if b == 0 {
        return Err[float64](fmt.Errorf("division by zero"))
    }
    return Ok(float64(a) / float64(b))
}

func ProcessConfigValue(value string) Result[string] {
    if strings.TrimSpace(value) == "" {
        return Err[string](fmt.Errorf("empty config value"))
    }
    return Ok(strings.ToUpper(strings.TrimSpace(value)))
}

func main() {
    // Пример 1: Парсинг числа
    result1 := ParseInt("42")
    if result1.IsOk() {
        fmt.Printf("Parsed number: %d\n", result1.Unwrap())
    }
    
    result2 := ParseInt("not_a_number")
    if result2.IsErr() {
        fmt.Printf("Parse error: %v\n", result2.Error())
    }
    
    // Пример 2: Цепочка операций
    result3 := FlatMap(ParseInt("10"), func(a int) Result[float64] {
        return FlatMap(ParseInt("2"), func(b int) Result[float64] {
            return Divide(a, b)
        })
    })
    
    if result3.IsOk() {
        fmt.Printf("Division result: %.2f\n", result3.Unwrap())
    }
    
    // Пример 3: Обработка конфигурации
    configs := []string{"  SERVER_NAME  ", "", "DATABASE_URL", "   "}
    
    for i, config := range configs {
        result := ProcessConfigValue(config)
        fmt.Printf("Config %d: %s\n", i+1, result.UnwrapOr("DEFAULT_VALUE"))
    }
    
    // Пример 4: Map для преобразования
    result4 := Map(ParseInt("100"), func(x int) string {
        return fmt.Sprintf("Value: %d", x)
    })
    
    if result4.IsOk() {
        fmt.Printf("Mapped result: %s\n", result4.Unwrap())
    }
}

Интеграция с внешними пакетами

Дженерики хорошо работают с популярными пакетами для работы с сетью, базами данных и другими инструментами:

package main

import (
    "context"
    "database/sql"
    "encoding/json"
    "fmt"
    "net/http"
    "time"
    
    _ "github.com/lib/pq" // PostgreSQL driver
)

// Дженерик-репозиторий для работы с базой данных
type Repository[T any] struct {
    db        *sql.DB
    tableName string
}

func NewRepository[T any](db *sql.DB, tableName string) *Repository[T] {
    return &Repository[T]{
        db:        db,
        tableName: tableName,
    }
}

// Дженерик-интерфейс для сканирования из базы данных
type Scanner interface {
    Scan(dest ...interface{}) error
}

// Дженерик-функция для сканирования результатов
func ScanRow[T any](row Scanner, scanFunc func(Scanner) (T, error)) (T, error) {
    return scanFunc(row)
}

// Дженерик-HTTP хэндлер
type Handler[T any] struct {
    processor func(T) (interface{}, error)
}

func NewHandler[T any](processor func(T) (interface{}, error)) *Handler[T] {
    return &Handler[T]{processor: processor}
}

func (h *Handler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }
    
    var req T
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    
    result, err := h.processor(req)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(result)
}

// Примеры структур
type User struct {
    ID    int    `json:"id" db:"id"`
    Name  string `json:"name" db:"name"`
    Email string `json:"email" db:"email"`
}

type Server struct {
    ID     int    `json:"id" db:"id"`
    Name   string `json:"name" db:"name"`
    IP     string `json:"ip" db:"ip"`
    Status string `json:"status" db:"status"`
}

type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

type CreateServerRequest struct {
    Name string `json:"name"`
    IP   string `json:"ip"`
}

// Функции для работы с базой данных
func (r *Repository[User]) FindByID(id int) (User, error) {
    query := fmt.Sprintf("SELECT id, name, email FROM %s WHERE id = $1", r.tableName)
    row := r.db.QueryRow(query, id)
    
    return ScanRow(row, func(scanner Scanner) (User, error) {
        var user User
        err := scanner.Scan(&user.ID, &user.Name, &user.Email)
        return user, err
    })
}

func (r *Repository[Server]) FindByID(id int) (Server, error) {
    query := fmt.Sprintf("SELECT id, name, ip, status FROM %s WHERE id = $1", r.tableName)
    row := r.db.QueryRow(query, id)
    
    return ScanRow(row, func(scanner Scanner) (Server, error) {
        var server Server
        err := scanner.Scan(&server.ID, &server.Name, &server.IP, &server.Status)
        return server, err
    })
}

func main() {
    // Подключение к базе данных
    db, err := sql.Open("postgres", "host=localhost dbname=test sslmode=disable")
    if err != nil {
        fmt.Printf("Database connection error: %v\n", err)
        return
    }
    defer db.Close()
    
    // Создаём репозитории
    userRepo := NewRepository[User](db, "users")
    serverRepo := NewRepository[Server](db, "servers")
    
    // Создаём хэндлеры
    createUserHandler := NewHandler(func(req CreateUserRequest) (interface{}, error) {
        // Здесь была бы логика создания пользователя
        user := User{
            ID:    1,
            Name:  req.Name,
            Email: req.Email,
        }
        fmt.Printf("Created user: %+v\n", user)
        return user, nil
    })
    
    createServerHandler := NewHandler(func(req CreateServerRequest) (interface{}, error) {
        // Здесь была бы логика создания сервера
        server := Server{
            ID:     1,
            Name:   req.Name,
            IP:     req.IP,
            Status: "pending",
        }
        fmt.Printf("Created server: %+v\n", server)
        return server, nil
    })
    
    // Настраиваем маршруты
    http.Handle("/users", createUserHandler)
    http.Handle("/servers", createServerHandler)
    
    // Пример работы с репозиториями
    fmt.Println("Testing repositories...")
    
    // Имитация данных для тестирования
    user, err := userRepo.FindByID(1)
    if err != nil {
        fmt.Printf("Error finding user: %v\n", err)
    } else {
        fmt.Printf("Found user: %+v\n", user)
    }
    
    server, err := serverRepo.FindByID(1)
    if err != nil {
        fmt.Printf("Error finding server: %v\n", err)
    } else {
        fmt.Printf("Found server: %+v\n", server)
    }
    
    // Запуск сервера
    fmt.Println("Starting server on :8080...")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        fmt.Printf("Server error: %v\n", err)
    }
}

Производительность и бенчмарки

Дженерики в Go работают через мономорфизацию — компилятор создаёт отдельную версию функции для каждого типа. Это означает, что производительность почти такая же, как у специализированных функций.

Пример бенчмарка:

package main

import (
    "fmt"
    "testing"
    "time"
)

// Дженерик-функция
func GenericMax[T ~int | ~float64](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// Специализированные функции
func MaxInt(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func MaxFloat64(a, b float64) float64 {
    if a > b {
        return a
    }
    return b
}

// Функция с interface{}
func MaxInterface(a, b interface{}) interface{} {
    switch a := a.(type) {
    case int:
        if b, ok := b.(int); ok && a > b {
            return a
        }
        return b
    case float64:
        if b, ok := b.(float64); ok && a > b {
            return a
        }
        return b
    default:
        return b
    }
}

func BenchmarkGenericMaxInt(b *testing.B) {
    for i := 0; i < b.N; i++ {
        GenericMax(10, 20)
    }
}

func BenchmarkMaxInt(b *testing.B) {
    for i := 0; i < b.N; i++ {
        MaxInt(10, 20)
    }
}

func BenchmarkMaxInterface(b *testing.B) {
    for i := 0; i < b.N; i++ {
        MaxInterface(10, 20)
    }
}

func BenchmarkGenericMaxFloat64(b *testing.B) {
    for i := 0; i < b.N; i++ {
        GenericMax(10.5, 20.5)
    }
}

func BenchmarkMaxFloat64(b *testing.B) {
    for i := 0; i < b.N; i++ {
        MaxFloat64(10.5, 20.5)
    }
}

func main() {
    // Простое сравнение производительности
    iterations := 1000000
    
    // Дженерик
    start := time.Now()
    for i := 0; i < iterations; i++ {
        GenericMax(i, i+1)
    }
    genericTime := time.Since(start)
    
    // Специализированная функция
    start = time.Now()
    for i := 0; i < iterations; i++ {
        MaxInt(i, i+1)
    }
    specializedTime := time.Since(start)
    
    // Interface{}
    start = time.Now()
    for i := 0; i < iterations; i++ {
        MaxInterface(i, i+1)
    }
    interfaceTime := time.Since(start)
    
    fmt.Printf("Generic: %v\n", genericTime)
    fmt.Printf("Specialized: %v\n", specializedTime)
    fmt.Printf("Interface: %v\n", interfaceTime)
    fmt.Printf("Generic/Specialized ratio: %.2f\n", float64(genericTime)/float64(specializedTime))
    fmt.Printf("Interface/Generic ratio: %.2f\n", float64(interfaceTime)/float64(genericTime))
}

Практические советы и best practices

  • Не используй дженерики везде — они нужны только там, где действительно помогают избежать дублирования кода
  • Предпочитай простые ограничения — `any`, `comparable`, стандартные интерфейсы
  • Именуй параметры типов осмысленно — `T` для одного типа, `K, V` для key-value, `R` для результата
  • Используй пакет golang.org/x/exp/constraints для числовых типов
  • Не создавай слишком сложные иерархии — дженерики должны упрощать, а не усложнять код

Автоматизация с дженериками

Дженерики открывают новые возможности для создания универсальных инструментов автоматизации:

package main

import (
    "fmt"
    "log"
    "os"
    "os/exec"
    "strings"
    "time"
)

// Дженерик-планировщик задач
type TaskScheduler[T any] struct {
    tasks []ScheduledTask[T]
}

type ScheduledTask[T any] struct {
    Name     string
    Data     T
    Handler  func(T) error
    Interval time.Duration
    NextRun  time.Time
}

func NewTaskScheduler[T any]() *TaskScheduler[T] {
    return &TaskScheduler[T]{
        tasks: make([]ScheduledTask[T], 0),
    }
}

func (ts *TaskScheduler[T]) AddTask(name string, data T, handler func(T) error, interval time.Duration) {
    task := ScheduledTask[T]{
        Name:     name,
        Data:     data,
        Handler:  handler,
        Interval: interval,
        NextRun:  time.Now().Add(interval),
    }
    ts.tasks = append(ts.tasks, task)
}

func (ts *TaskScheduler[T]) Start() {
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()
    
    for range ticker.C {
        now := time.Now()
        for i := range ts.tasks {
            task := &ts.tasks[i]
            if now.After(task.NextRun) {
                go func(t *ScheduledTask[T]) {
                    if err := t.Handler(t.Data); err != nil {
                        log.Printf("Task %s failed: %v", t.Name, err)
                    } else {
                        log.Printf("Task %s completed successfully", t.Name)
                    }
                }(task)
                task.NextRun = now.Add(task.Interval)
            }
        }
    }
}

// Типы для разных задач
type BackupTask struct {
    SourcePath string
    TargetPath string
}

type ServiceTask struct {
    ServiceName string
    Action      string // restart, stop, start
}

type CleanupTask struct {
    Directory string
    MaxAge    time.Duration
}

type MonitorTask struct {
    URL           string
    ExpectedCode  int
    TimeoutSecond int
}

// Обработчики задач
func handleBackup(task BackupTask) error {
    cmd := exec.Command("rsync", "-av", task.SourcePath, task.TargetPath)
    output, err := cmd.CombinedOutput()
    if err != nil {
        return fmt.Errorf("backup failed: %v, output: %s", err, output)
    }
    return nil
}

func handleService(task ServiceTask) error {
    cmd := exec.Command("systemctl", task.Action, task.ServiceName)
    output, err := cmd.CombinedOutput()
    if err != nil {
        return fmt.Errorf("service %s %s failed: %v, output: %s", 
            task.ServiceName, task.Action, err, output)
    }
    return nil
}

func handleCleanup(task CleanupTask) error {
    cmd := exec.Command("find", task.Directory, "-type", "f", "-mtime", 
        fmt.Sprintf("+%d", int(task.MaxAge.Hours()/24)), "-delete")
    output, err := cmd.CombinedOutput()
    if err != nil {
        return fmt.Errorf("cleanup failed: %v, output: %s", err, output)
    }
    return nil
}

func handleMonitor(task MonitorTask) error {
    cmd := exec.Command("curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", 
        "--max-time", fmt.Sprintf("%d", task.TimeoutSecond), task.URL)
    output, err := cmd.Output()
    if err != nil {
        return fmt.Errorf("monitor request failed: %v", err)
    }
    
    code := strings.TrimSpace(string(output))
    expectedCode := fmt.Sprintf("%d", task.ExpectedCode)
    if code != expectedCode {
        return fmt.Errorf("unexpected HTTP code: got %s, expected %s", code, expectedCode)
    }
    
    return nil
}

func main() {
    // Создаём планировщики для разных типов задач
    backupScheduler := NewTaskScheduler[BackupTask]()
    serviceScheduler := NewTaskScheduler[ServiceTask]()
    cleanupScheduler := NewTaskScheduler[CleanupTask]()
    monitorScheduler := NewTaskScheduler[MonitorTask]()
    
    // Добавляем задачи бэкапа
    backupScheduler.AddTask("Daily Database Backup", BackupTask{
        SourcePath: "/var/lib/postgresql/data",
        TargetPath: "/backup/db/",
    }, handleBackup, 24*time.Hour)
    
    backupScheduler.AddTask("Hourly Config Backup", BackupTask{
        SourcePath: "/etc/",
        TargetPath: "/backup/etc/",
    }, handleBackup, time.Hour)
    
    // Добавляем задачи управления сервисами
    serviceScheduler.AddTask("Restart Nginx", ServiceTask{
        ServiceName: "nginx",
        Action:      "restart",
    }, handleService, 7*24*time.Hour)
    
    serviceScheduler.AddTask("Restart PostgreSQL", ServiceTask{
        ServiceName: "postgresql",
        Action:      "restart",
    }, handleService, 30*24*time.Hour)
    
    // Добавляем задачи очистки
    cleanupScheduler.AddTask("Clean Temp Files", CleanupTask{
        Directory: "/tmp",
        MaxAge:    24 * time.Hour,
    }, handleCleanup, 6*time.Hour)
    
    cleanupScheduler.AddTask("Clean Old Logs", CleanupTask{
        Directory: "/var/log",
        MaxAge:    7 * 24 * time.Hour,
    }, handleCleanup, 24*time.Hour)
    
    // Добавляем задачи мониторинга
    monitorScheduler.AddTask("Check Web Server", MonitorTask{
        URL:           "http://localhost:80",
        ExpectedCode:  200,
        TimeoutSecond: 10,
    }, handleMonitor, 5*time.Minute)
    
    monitorScheduler.AddTask("Check API", MonitorTask{
        URL:           "http://localhost:8080/health",
        ExpectedCode:  200,
        TimeoutSecond: 5,
    }, handleMonitor, time.Minute)
    
    // Запускаем планировщики в отдельных горутинах
    go backupScheduler.Start()
    go serviceScheduler.Start()
    go cleanupScheduler.Start()
    go monitorScheduler.Start()
    
    log.Println("Task schedulers started. Press Ctrl+C to stop.")
    
    // Ждём сигнала завершения
    select {}
}

Развёртывание и хостинг

Для запуска приложений с дженериками тебе понадобится современная инфраструктура. Если ты ищешь надёжный хостинг для своих Go-приложений, рекомендую посмотреть на VPS-серверы с поддержкой Go 1.18+. Для высоконагруженных проектов лучше взять выделенный сервер.

Заключение и рекомендации

Дженерики в Go — это мощный инструмент, который стоит использовать осознанно. Они особенно полезны для:

  • Создания универсальных утилит — парсеры, конвертеры, валидаторы
  • Работы с коллекциями — стеки, очереди, кэши, множества
  • API-клиентов — типобезопасные HTTP-клиенты
  • Математических операций — функции для работы с числами
  • Обработки ошибок — Result-типы, Maybe-типы

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


В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.

Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.

Leave a reply

Your email address will not be published. Required fields are marked