Enterprise Go: как писать код, который не стыдно показать CTO

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

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

Начать курс

Go в большом бизнесе: почему это уже не просто «язык для микросервисов»

Когда вы слышите «Enterprise Go», что приходит в голову? Скучные корпоративные регламенты? Бесконечные code review? Или, может быть, ощущение, что Go — это только для стартапов и DevOps?

На самом деле, Go в enterprise — это совершенно другой уровень дисциплины и инженерной культуры. В крупных компаниях (Uber, Dropbox, Salesforce, Twitch) на Go пишут не просто микросервисы, а целые платформы, обрабатывающие миллионы запросов в секунду. И здесь «просто работает» — недостаточно.

В этой статье мы разберем ключевые особенности разработки на Go в enterprise: от проектирования пакетов до production-логирования. Будет много кода и минимум воды.

1. Архитектура: не повторяйте ошибку «монолит в микросервисах»

Самая частая проблема, которую я вижу в Go проектах enterprise-уровня — это попытка перенести Java-style архитектуру на Go. Результат: сотни интерфейсов, которые не нужны, и слои абстракции, которые только замедляют разработку.

Правило «Package by feature, not by layer»

Вместо того чтобы создавать папки /models, /handlers, /services — группируйте по бизнес-доменам:

myapp/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── user/          # Всё, что связано с пользователями
│   │   ├── handler.go
│   │   ├── service.go
│   │   ├── repository.go
│   │   └── user.go    # Модель данных
│   ├── payment/       # Всё, что связано с платежами
│   │   ├── handler.go
│   │   ├── service.go
│   │   ├── gateway.go
│   │   └── payment.go
│   └── pkg/           # Общие утилиты
└── go.mod

Этот подход — один из best practices Go для enterprise. Он уменьшает связанность (coupling) и делает код предсказуемым. Когда новый разработчик открывает проект, он сразу видит бизнес-логику, а не слои абстракции.

Интерфейсы: принимайте интерфейсы, возвращайте структуры

Это классический совет из Go-сообщества, но в enterprise он критичен. Интерфейсы должны быть маленькими (1-2 метода) и определяться там, где они используются, а не там, где реализуются.

// ПЛОХО: интерфейс определён рядом с реализацией
package user

type UserRepository interface {
    FindByID(id string) (*User, error)
    Save(user *User) error
}

type postgresRepo struct { ... }

// ХОРОШО: интерфейс там, где он нужен
package user

type Storage interface {
    Get(id string) (*User, error)
    Put(user *User) error
}

type service struct {
    db Storage
}

func NewService(db Storage) *service {
    return &service{db: db}
}

Почему это важно? Потому что в enterprise вы часто меняете бэкенды (PostgreSQL -> CockroachDB, Redis -> KeyDB). Интерфейсы, определённые на стороне потребителя, позволяют делать это без боли.

2. Обработка ошибок: перестаньте игнорировать _

В Go нет исключений, и это прекрасно. Но в enterprise-проектах ошибки — это не просто if err != nil. Это целая культура.

Wrap или не wrap?

Используйте fmt.Errorf с %w для создания цепочек ошибок. Это позволяет сохранить стек вызовов и упрощает отладку.

func (s *service) ProcessOrder(ctx context.Context, orderID string) error {
    order, err := s.orderRepo.Get(ctx, orderID)
    if err != nil {
        return fmt.Errorf("getting order %s: %w", orderID, err)
    }

    if err := s.paymentGateway.Charge(ctx, order.Amount); err != nil {
        return fmt.Errorf("charging for order %s: %w", orderID, err)
    }

    return nil
}

На верхнем уровне (в handler/middleware) используйте errors.Is() и errors.As() для проверки типов ошибок:

if errors.Is(err, ErrNotFound) {
    http.Error(w, "not found", http.StatusNotFound)
    return
}

var paymentErr *PaymentError
if errors.As(err, &paymentErr) {
    log.Printf("payment failed: %v", paymentErr)
    http.Error(w, "payment error", http.StatusPaymentRequired)
    return
}

Этот паттерн — основа обработки ошибок в enterprise Go. Он даёт полную прозрачность: вы знаете, где и почему произошла ошибка, и можете корректно ответить клиенту.

Sentinel errors vs custom types

Для enterprise лучше использовать custom error types, а не sentinel errors (вроде var ErrNotFound = errors.New("not found")). Почему? Потому что sentinel errors не несут контекста, и их сложно расширять.

type ValidationError struct {
    Field string
    Value interface{}
    Msg   string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error: field %s = %v: %s", e.Field, e.Value, e.Msg)
}

func (e *ValidationError) Unwrap() error {
    return nil
}

Теперь вы можете логировать и обрабатывать ошибки с богатой структурой, а не просто строкой.

3. Тестирование: не просто unit-тесты

Тестирование Go в enterprise — это не про 100% coverage. Это про уверенность, что ваш код не сломает production.

Table-driven tests — ваш лучший друг

Это стандарт Go, но в enterprise он обязателен. Вот пример для сервиса обработки заказов:

func TestProcessOrder(t *testing.T) {
    tests := []struct {
        name       string
        orderID    string
        setupMock  func(*mocks.OrderRepo, *mocks.PaymentGateway)
        wantErr    bool
        wantErrMsg string
    }{
        {
            name:    "successful order",
            orderID: "order-1",
            setupMock: func(repo *mocks.OrderRepo, gw *mocks.PaymentGateway) {
                repo.On("Get", mock.Anything, "order-1").
                    Return(&Order{Amount: 100}, nil)
                gw.On("Charge", mock.Anything, 100).
                    Return(nil)
            },
            wantErr: false,
        },
        {
            name:    "order not found",
            orderID: "order-404",
            setupMock: func(repo *mocks.OrderRepo, gw *mocks.PaymentGateway) {
                repo.On("Get", mock.Anything, "order-404").
                    Return(nil, ErrNotFound)
            },
            wantErr:    true,
            wantErrMsg: "not found",
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // Arrange
            repo := new(mocks.OrderRepo)
            gw := new(mocks.PaymentGateway)
            tt.setupMock(repo, gw)

            svc := NewService(repo, gw)

            // Act
            err := svc.ProcessOrder(context.Background(), tt.orderID)

            // Assert
            if tt.wantErr {
                assert.Error(t, err)
                assert.Contains(t, err.Error(), tt.wantErrMsg)
            } else {
                assert.NoError(t, err                

Блоги

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