Введение в микросервисы на Go
Микросервисная архитектура стала стандартом для построения масштабируемых и отказоустойчивых приложений. Язык Go (Golang) идеально подходит для разработки микросервисов благодаря своей производительности, встроенной поддержке конкурентности и простоте развертывания. В этой статье мы рассмотрим основные принципы построения микросервисов на Go, создадим простой сервис с REST API, подключим базу данных и покажем, как контейнеризировать приложение с помощью Docker.
Основы микросервисной архитектуры
Микросервисы — это подход, при котором приложение разбивается на небольшие, независимо развертываемые сервисы. Каждый сервис отвечает за свою бизнес-логику и взаимодействует с другими через легковесные протоколы (обычно HTTP/REST или gRPC).
Преимущества микросервисов на Go
- Производительность: Go компилируется в машинный код, что дает высокую скорость выполнения.
- Конкурентность: горутины и каналы позволяют эффективно обрабатывать тысячи запросов.
- Простота развертывания: бинарный файл не требует внешних зависимостей.
- Встроенная стандартная библиотека: пакеты
net/http,encoding/jsonи другие покрывают большинство задач.
Ключевые принципы проектирования
- Единственная ответственность: каждый сервис решает одну задачу.
- Независимость: сервисы можно разрабатывать, тестировать и развертывать отдельно.
- Слабая связанность: изменения в одном сервисе не должны ломать другие.
- Устойчивость к сбоям: отказ одного сервиса не должен обрушить всю систему.
Практика: создаем микросервис на Go
Давайте создадим простой микросервис для управления пользователями. Он будет предоставлять REST API для создания, чтения, обновления и удаления пользователей (CRUD).
Шаг 1: Инициализация проекта
Создайте директорию проекта и инициализируйте модуль Go:
mkdir user-service
cd user-service
go mod init github.com/yourusername/user-service
Шаг 2: Структура проекта
Организуем код по пакетам:
user-service/
├── cmd/
│ └── main.go
├── internal/
│ ├── handler/
│ │ └── user.go
│ ├── model/
│ │ └── user.go
│ └── repository/
│ └── user.go
├── go.mod
└── go.sum
Шаг 3: Определяем модель данных
Создадим структуру пользователя в файле internal/model/user.go:
package model
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
Шаг 4: Реализуем хранилище (репозиторий)
Для простоты используем in-memory хранилище. Создадим internal/repository/user.go:
package repository
import (
"sync"
"user-service/internal/model"
)
type UserRepository struct {
mu sync.RWMutex
users map[int]model.User
nextID int
}
func NewUserRepository() *UserRepository {
return &UserRepository{
users: make(map[int]model.User),
nextID: 1,
}
}
func (r *UserRepository) Create(user model.User) model.User {
r.mu.Lock()
defer r.mu.Unlock()
user.ID = r.nextID
r.nextID++
r.users[user.ID] = user
return user
}
func (r *UserRepository) GetByID(id int) (model.User, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
user, ok := r.users[id]
return user, ok
}
func (r *UserRepository) GetAll() []model.User {
r.mu.RLock()
defer r.mu.RUnlock()
result := make([]model.User, 0, len(r.users))
for _, user := range r.users {
result = append(result, user)
}
return result
}
func (r *UserRepository) Update(id int, user model.User) (model.User, bool) {
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.users[id]; !ok {
return model.User{}, false
}
user.ID = id
r.users[id] = user
return user, true
}
func (r *UserRepository) Delete(id int) bool {
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.users[id]; ok {
delete(r.users, id)
return true
}
return false
}
Шаг 5: Обработчики HTTP-запросов
Создадим internal/handler/user.go с обработчиками для CRUD операций:
package handler
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"user-service/internal/model"
"user-service/internal/repository"
)
type UserHandler struct {
repo *repository.UserRepository
}
func NewUserHandler(repo *repository.UserRepository) *UserHandler {
return &UserHandler{repo: repo}
}
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Извлекаем ID из URL: /users/{id}
path := strings.TrimPrefix(r.URL.Path, "/users")
path = strings.TrimPrefix(path, "/")
switch r.Method {
case http.MethodGet:
if path == "" {
h.getAll(w, r)
} else {
h.getByID(w, r, path)
}
case http.MethodPost:
if path == "" {
h.create(w, r)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
case http.MethodPut:
if path != "" {
h.update(w, r, path)
} else {
http.Error(w, "Bad request", http.StatusBadRequest)
}
case http.MethodDelete:
if path != "" {
h.delete(w, r, path)
} else {
http.Error(w, "Bad request", http.StatusBadRequest)
}
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (h *UserHandler) getAll(w http.ResponseWriter, r *http.Request) {
users := h.repo.GetAll()
writeJSON(w, http.StatusOK, users)
}
func (h *UserHandler) getByID(w http.ResponseWriter, r *http.Request, idStr string) {
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
user, ok