- Home »

Как использовать дженерики в 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-типы
Не используй дженерики там, где они не нужны — простой код лучше сложного. Начни с малого: перепиши одну-две функции, которые дублируются для разных типов. Постепенно ты поймё
В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.
Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.