← Назад к вопросам

Приведи пример неудачного решения задачи

1.0 Junior🔥 251 комментариев
#Soft Skills и карьера

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# **Пример неудачного решения задачи: реализация in-memory кэша**

Контекст задачи

Требуется создать простой in-memory кэш для временного хранения данных пользовательских сессий со следующими требованиями:

  • Хранение пар "ключ-значение"
  • Автоматическое удаление устаревших записей (TTL)
  • Высокая производительность при чтении
  • Потокобезопасность

Неудачная реализация

Ключевые проблемы решения

package main

import (
    "sync"
    "time"
)

// Проблема №1: Слишком сложная структура данных
type Cache struct {
    mu       sync.RWMutex
    data     map[string]interface{}
    ttl      map[string]time.Time
    janitor  *time.Ticker
    stopChan chan bool
}

// Проблема №2: Избыточная синхронизация
func (c *Cache) Set(key string, value interface{}, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    c.data[key] = value
    c.ttl[key] = time.Now().Add(ttl)
}

func (c *Cache) Get(key string) (interface{}, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    // Проблема №3: Не проверяется TTL при чтении
    val, ok := c.data[key]
    return val, ok
}

// Проблема №4: Реализация "дворника" с утечкой горутин
func (c *Cache) startJanitor(interval time.Duration) {
    c.janitor = time.NewTicker(interval)
    go func() {
        for {
            select {
            case <-c.janitor.C:
                c.cleanup()
            case <-c.stopChan:
                c.janitor.Stop()
                return
            }
        }
    }()
}

// Проблема №5: Неэффективная очистка O(n) при каждом вызове
func (c *Cache) cleanup() {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    now := time.Now()
    for key, expires := range c.ttl {
        if expires.Before(now) {
            delete(c.data, key)
            delete(c.ttl, key)
        }
    }
}

// Проблема №6: Нет обработки паники и race condition
func (c *Cache) Delete(key string) {
    c.mu.Lock()
    delete(c.data, key)
    delete(c.ttl, key)
    c.mu.Unlock()
}

Критический анализ недостатков

Архитектурные ошибки

  1. Нарушение SRP (Single Responsibility Principle)

    • Класс Cache отвечает за хранение, очистку и жизненный цикл
    • Смешана логика данных и фоновых процессов
  2. Проблемы с производительностью

    // Каждое чтение требует захвата RLock
    // При частых чтениях это создает contention
    func (c *Cache) GetAll() map[string]interface{} {
        c.mu.RLock()
        defer c.mu.RUnlock()
        
        // Создается полная копия map - дорогая операция
        result := make(map[string]interface{})
        for k, v := range c.data {
            result[k] = v
        }
        return result
    }
    
  3. Утечка ресурсов

    • Горутина janitor может остаться работающей после удаления объекта
    • Нет метода Close() для корректной остановки

Фундаментальные ошибки в реализации

  1. Race condition в TTL

    // Между проверкой TTL и возвратом значения 
    // запись может быть удалена другим потоком
    func (c *Cache) GetWithTTL(key string) (interface{}, bool) {
        c.mu.RLock()
        expires, exists := c.ttl[key]
        c.mu.RUnlock()
        
        if !exists || time.Now().After(expires) {
            return nil, false
        }
        
        // В этот момент другой поток может удалить запись!
        c.mu.RLock()
        val := c.data[key]
        c.mu.RUnlock()
        
        return val, true
    }
    
  2. Отсутствие обработки edge cases

    • Нет ограничения на размер кэша (может привести к OOM)
    • Нет стратегии вытеснения при переполнении
    • Не обрабатывается случай, когда TTL = 0
  3. Сложность тестирования

    // Тесты становятся хрупкими из-за зависимостей от времени
    func TestCacheExpiration(t *testing.T) {
        cache := NewCache()
        cache.Set("key", "value", time.Millisecond*100)
        
        time.Sleep(time.Millisecond * 150) // Ненадежные sleep в тестах
        
        // Результат зависит от времени выполнения теста
        _, ok := cache.Get("key")
        assert.False(t, ok)
    }
    

Улучшенный подход

Ключевые исправления

// Использование sync.Map для конкурентного доступа
// Реализация lazy expiration (проверка при чтении)
// Отказ от фонового "дворника" в пользу простых решений

type SimpleCache struct {
    sync.Map // Встроенный потокобезопасный map
}

type cacheEntry struct {
    value     interface{}
    expiresAt time.Time
}

func (c *SimpleCache) Set(key string, value interface{}, ttl time.Duration) {
    entry := &cacheEntry{
        value:     value,
        expiresAt: time.Now().Add(ttl),
    }
    c.Store(key, entry)
}

func (c *SimpleCache) Get(key string) (interface{}, bool) {
    val, ok := c.Load(key)
    if !ok {
        return nil, false
    }
    
    entry := val.(*cacheEntry)
    if time.Now().After(entry.expiresAt) {
        c.Delete(key) // Lazy deletion
        return nil, false
    }
    
    return entry.value, true
}

Выводы и уроки

Этот пример демонстрирует типичные антипаттерны в разработке на Go:

  • Преждевременная оптимизация без измерения производительности
  • Избыточная сложность там, где можно обойтись простыми решениями
  • Непонимание модели памяти Go и примитивов синхронизации
  • Игнорирование edge cases и обработки ошибок

Главный урок: всегда начинайте с простейшей работающей реализации, измеряйте производительность, и добавляйте сложность только там, где это действительно необходимо. Часто стандартные библиотеки Go (как sync.Map) уже решают проблемы лучше, чем кастомные реализации.

Приведи пример неудачного решения задачи | PrepBro