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