Go: Функциональный или ООП? Выбираем стиль программирования

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Вечный спор в мире Go

Go — язык, который сломал шаблоны. Он не прощает излишней сложности, но при этом даёт разработчику невероятную свободу. Каждый, кто начинает писать на Go (или переходит с Python или Java), рано или поздно упирается в стену: «А как же правильно? Писать как на Java с классами и наследованием или как на Haskell с функциями без побочных эффектов?»

Спойлер: Go не является ни чисто объектно-ориентированным, ни чисто функциональным языком. Он — прагматик. Но это не значит, что вы не можете (и не должны) использовать обе парадигмы. В этой статье мы разберём, как сочетать функциональный и объектно-ориентированный подход в Go, чтобы ваш код был читаемым, тестируемым и быстрым. Мы не будем читать лекции, а посмотрим на реальные примеры и антипаттерны.

Если вы ищете лучшие практики Go и хотите понять, как писать чистый код на этом языке — вы по адресу.

1. Объектно-ориентированный подход в Go: Без классов, но с душой

Многие считают, что раз в Go нет class, то ООП в нём нет. Это миф. ООП в Go строится на двух китах: структуры (structs) и интерфейсы (interfaces). Наследования нет, но есть композиция, которую создатели языка считают более гибкой.

Структуры и методы

Вы можете определять методы для любого типа. Это и есть основа объектно-ориентированного кода.

package main

import "fmt"

// Определяем интерфейс (контракт)
type Notifier interface {
    Send(message string) error
}

// Структура, реализующая интерфейс
type EmailNotifier struct {
    Email string
}

// Метод для EmailNotifier
func (e EmailNotifier) Send(message string) error {
    fmt.Printf("Отправка email на %s: %s\n", e.Email, message)
    return nil
}

// Ещё одна структура
type SMSNotifier struct {
    Phone string
}

func (s SMSNotifier) Send(message string) error {
    fmt.Printf("Отправка SMS на %s: %s\n", s.Phone, message)
    return nil
}

// Функция, которая принимает любой Notifier (полиморфизм)
func AlertAll(notifiers []Notifier, msg string) {
    for _, n := range notifiers {
        n.Send(msg)
    }
}

func main() {
    notifiers := []Notifier{
        EmailNotifier{Email: "user@example.com"},
        SMSNotifier{Phone: "+1234567890"},
    }
    AlertAll(notifiers, "Сервер упал!")
}

Плюсы ООП в Go:

  • Полиморфизм через интерфейсы. Вы пишете один раз функцию AlertAll, а она работает с любым типом, который реализует Notifier. Это слабая связанность.
  • Состояние. Структуры идеально подходят для моделей данных (User, Order, Config).
  • Инкапсуляция. Поля с маленькой буквы — приватные, с большой — публичные. Просто и эффективно.

Минусы:

  • Легко скатиться в «Java-стиль»: создавать глубокие иерархии, хотя Go их не поддерживает.
  • Мутабельность (изменение состояния) усложняет тестирование и отладку.

2. Функциональное программирование в Go: Чистота ради скорости

Функциональный стиль в Go — это не лямбда-исчисление, а скорее набор принципов: минимум побочных эффектов, неизменяемость, функции как аргументы. С появлением дженериков (Go 1.18+) этот стиль стал мощнее.

Чистые функции и неизменяемость

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

package main

import "fmt"

// Плохо: мутируем входящий срез (побочный эффект)
func BadAddPrefix(users []string, prefix string) {
    for i := range users {
        users[i] = prefix + users[i]
    }
}

// Хорошо: возвращаем новый срез (чистая функция)
func GoodAddPrefix(users []string, prefix string) []string {
    result := make([]string, len(users))
    for i, u := range users {
        result[i] = prefix + u
    }
    return result
}

func main() {
    original := []string{"Alice", "Bob"}
    modified := GoodAddPrefix(original, "Dr. ")
    
    fmt.Println("Original:", original) // ["Alice", "Bob"] - не изменился!
    fmt.Println("Modified:", modified) // ["Dr. Alice", "Dr. Bob"]
}

Функции высшего порядка и дженерики

До дженериков приходилось писать кучу однотипных функций для map, filter. Сейчас это выглядит элегантно.

package main

import "fmt"

// Generic функция Map (функциональный подход)
func Map[T any, U any](input []T, fn func(T) U) []U {
    result := make([]U, len(input))
    for i, val := range input {
        result[i] = fn(val)
    }
    return result
}

func main() {
    numbers := []int{1, 2, 3, 4}
    
    // Преобразуем int в string
    strings := Map(numbers, func(n int) string {
        return fmt.Sprintf("Number: %d", n)
    })
    
    fmt.Println(strings) // ["Number: 1", "Number: 2", ...]
    
    // Считаем длину строк
    names := []string{"Go", "Python", "Rust"}
    lengths := Map(names, func(s string) int {
        return len(s)
    })
    fmt.Println(lengths) // [2, 6, 4]
}

Плюсы ФП в Go:

  • Тестируемость. Чистые функции не зависят от глобального состояния. Вы просто передаёте данные и проверяете результат.
  • Параллелизм. Неизменяемые данные безопасно передавать в горутины. Никаких гонок данных!
  • Композиция. Маленькие функции легко собираются в большие цепочки.

Минусы:

  • Чрезмерное увлечение может привести к куче мелких функций, которые сложно читать.
  • Накладные расходы на выделение памяти (каждый раз новый срез). Для высоконагруженных систем это может быть критично.

3. Битва титанов: Когда что использовать?

В Go нет религии. Есть задачи. Давайте разберём сравнение подходов на конкретных сценариях.

Сценарий 1: Обработка HTTP запроса

Здесь идеально работает ООП: структура Handler с методами, интерфейсы для middleware.

type UserHandler struct {
    repo UserRepository
}

func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
    // ... используем h.repo
}

                
                

Блоги

Рекомендаций по книгам