Микросервисы на Go: от идеи до продакшена
Go (Golang) завоевал репутацию идеального языка для построения микросервисной архитектуры. Благодаря встроенной поддержке конкурентности, быстрой компиляции, минимальному потреблению памяти и простому синтаксису, Go позволяет создавать легковесные и высокопроизводительные сервисы. В этой статье мы разберем ключевые принципы построения микросервисов на Go, рассмотрим практические примеры и лучшие практики, которые помогут вам избежать типичных ошибок.
Почему Go — идеальный выбор для микросервисов?
Перед тем как погрузиться в архитектуру, давайте разберем, почему Go стал стандартом де-факто для микросервисов:
- Быстрый старт: Go компилируется в единый бинарный файл, что упрощает деплой и масштабирование.
- Встроенная конкурентность: горутины и каналы позволяют эффективно обрабатывать тысячи запросов без сложных потоков.
- Минимальное потребление памяти: каждый микросервис на Go потребляет в разы меньше ресурсов, чем аналоги на Java или Python.
- Простота поддержки: статическая типизация и строгий компилятор снижают количество ошибок на этапе выполнения.
Архитектура микросервиса на Go: базовый шаблон
Рассмотрим типичную структуру микросервиса на Go. Для примера создадим простой сервис управления пользователями.
Структура проекта
user-service/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── handler/
│ │ └── user.go
│ ├── service/
│ │ └── user.go
│ ├── repository/
│ │ └── postgres.go
│ └── model/
│ └── user.go
├── pkg/
│ └── middleware/
│ └── logging.go
├── config/
│ └── config.go
├── go.mod
└── Dockerfile
Этот шаблон следует принципам Clean Architecture: зависимости направлены внутрь, а бизнес-логика изолирована от инфраструктуры.
Пример основного файла (cmd/server/main.go)
package main
import (
"log"
"net/http"
"os"
"user-service/internal/handler"
"user-service/internal/repository"
"user-service/internal/service"
)
func main() {
// Инициализация зависимостей
db, err := repository.NewPostgresDB("postgres://user:pass@localhost:5432/users")
if err != nil {
log.Fatal(err)
}
defer db.Close()
userRepo := repository.NewUserRepository(db)
userSvc := service.NewUserService(userRepo)
userHandler := handler.NewUserHandler(userSvc)
// Настройка маршрутов
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", userHandler.GetByID)
mux.HandleFunc("POST /users", userHandler.Create)
mux.HandleFunc("PUT /users/{id}", userHandler.Update)
// Запуск сервера
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("Server starting on :%s", port)
log.Fatal(http.ListenAndServe(":"+port, mux))
}
Реализация слоёв микросервиса
Разделим код на три основных слоя: handler (транспорт), service (бизнес-логика) и repository (доступ к данным).
Слой handler: обработка HTTP-запросов
// internal/handler/user.go
package handler
import (
"encoding/json"
"net/http"
"user-service/internal/model"
"user-service/internal/service"
)
type UserHandler struct {
svc *service.UserService
}
func NewUserHandler(svc *service.UserService) *UserHandler {
return &UserHandler{svc: svc}
}
func (h *UserHandler) GetByID(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
user, err := h.svc.GetByID(r.Context(), id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(user)
}
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
var user model.User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
created, err := h.svc.Create(r.Context(), &user)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(created)
}
Слой service: бизнес-логика
// internal/service/user.go
package service
import (
"context"
"user-service/internal/model"
"user-service/internal/repository"
)
type UserService struct {
repo *repository.UserRepository
}
func NewUserService(repo *repository.UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) GetByID(ctx context.Context, id string) (*model.User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, err
}
// Здесь можно добавить дополнительную бизнес-логику
// Например, проверку прав доступа или кэширование
return user, nil
}
func (s *UserService) Create(ctx context.Context, user *model.User) (*model.User, error) {
// Валидация данных
if user.Name == "" || user.Email == "" {
return nil, ErrInvalidInput
}
return s.repo.Save(ctx, user)
}
Слой repository: работа с базой данных
// internal/repository/postgres.go
package repository
import (
"context"
"database/sql"
"user-service/internal/model"
)
type UserRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) FindByID(ctx context.Context, id string) (*model.User, error) {
row := r.db.QueryRowContext(ctx, "SELECT id, name, email FROM users WHERE id = $1", id)
user := &model.User{}
err := row.Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
return nil, err
}
return user, nil
}
func (r *UserRepository) Save(ctx context.Context, user *model.User) (*model.User, error) {
err := r.db.QueryRowContext(ctx,
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
user.Name, user.Email).Scan(&user.ID)
if err != nil {
return nil, err
}
return user, nil
}