golang

Построение REST API на Go с использованием Gorilla Mux и MongoDB

  • пятница, 8 августа 2025 г. в 00:00:10
https://habr.com/ru/articles/935102/
go lang
go lang

Введение

В данной статье будет рассмотрена практическая интеграция MongoDB с веб-приложением на Go, построенным на базе маршрутизатора Gorilla Mux. Цель — получить минимальный, но функциональный REST API с поддержкой CRUD-операций над сущностью Book, при этом соблюдая лучшие практики структурирования кода.

Материал рассчитан на разработчиков, знакомых с Go, HTTP API и основами работы с базами данных.

Выбор стека

Go — компилируемый язык с лаконичным синтаксисом, встроенной поддержкой параллелизма и богатой стандартной библиотекой для работы с сетью. Эти качества делают его удобным выбором для разработки API-сервисов.

Почему Gorilla Mux

Gorilla Mux — зрелый, широко используемый HTTP-роутер для Go, который поддерживает:

  • маршрутизацию с шаблонами (/books/{id});

  • фильтрацию по HTTP-методу;

  • middleware.

Почему MongoDB

MongoDB — документно-ориентированная NoSQL-БД, оптимизированная для хранения неструктурированных или слабо структурированных данных, что делает её подходящей для быстрого прототипирования и гибких схем.

Архитектура проекта

Структура каталогов для примера:

/cmd
    /api
        main.go
/internal
    /books
        handler.go
        model.go
        repository.go
/pkg
    /db
        mongo.go
go.mod

Такое разделение позволяет изолировать доменную логику (internal/books) от инфраструктурных компонентов (pkg/db).

Подготовка окружения

Установка MongoDB (Linux, apt)

sudo apt update
sudo apt install -y mongodb

После установки убедитесь, что сервис запущен:

systemctl status mongodb

Подключение к shell:

  mongo

Cоздадим базу данных booksdb и коллекцию books:

use booksdb
db.books.insertOne({
  title: "A Song of Ice and Fire",
  authors: ["George R.R. Martin", "Phyllis Eisenstein"],
  publish_date: ISODate("1996-08-01"),
  publisher: {
    name: "Bantam Books",
    country: "US"
  }
})

Подключение зависимостей

go mod init github.com/username/mongo-gorilla-api
go get github.com/gorilla/mux
go get go.mongodb.org/mongo-driver/mongo
go get go.mongodb.org/mongo-driver/mongo/options

Примечание: В отличие от устаревшего mgo.v2, рекомендуется использовать официальный mongo-driver.

Инициализация подключения к MongoDB

/pkg/db/mongo.go:

package db

import (
    "context"
    "log"
    "time"

    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

func Connect(uri string) *mongo.Database {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri))
    if err != nil {
        log.Fatalf("Ошибка подключения к MongoDB: %v", err)
    }

    if err := client.Ping(ctx, nil); err != nil {
        log.Fatalf("MongoDB не отвечает: %v", err)
    }

    log.Println("Соединение с MongoDB установлено")
    return client.Database("booksdb")
}

Модель данных

/internal/books/model.go:

package books

import "go.mongodb.org/mongo-driver/bson/primitive"

type Book struct {
    ID          primitive.ObjectID `bson:"_id,omitempty" json:"id"`
    Title       string             `bson:"title" json:"title"`
    Authors     []string           `bson:"authors" json:"authors"`
    PublishDate string             `bson:"publish_date" json:"publish_date"`
    Publisher   Publisher          `bson:"publisher" json:"publisher"`
}

type Publisher struct {
    Name    string `bson:"name" json:"name"`
    Country string `bson:"country" json:"country"`
}

Репозиторий

/internal/books/repository.go:

package books

import (
    "context"
    "time"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
)

type Repository struct {
    col *mongo.Collection
}

func NewRepository(db *mongo.Database) *Repository {
    return &Repository{col: db.Collection("books")}
}

func (r *Repository) FindAll(ctx context.Context) ([]Book, error) {
    var books []Book
    cur, err := r.col.Find(ctx, bson.D{})
    if err != nil {
        return nil, err
    }
    defer cur.Close(ctx)

    for cur.Next(ctx) {
        var book Book
        if err := cur.Decode(&book); err != nil {
            return nil, err
        }
        books = append(books, book)
    }
    return books, nil
}

func (r *Repository) Insert(ctx context.Context, book Book) (*mongo.InsertOneResult, error) {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()
    return r.col.InsertOne(ctx, book)
}

HTTP-обработчики

/internal/books/handler.go:

package books

import (
    "context"
    "encoding/json"
    "net/http"
    "time"
)

type Handler struct {
    repo *Repository
}

func NewHandler(repo *Repository) *Handler {
    return &Handler{repo: repo}
}

func (h *Handler) GetBooks(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    books, err := h.repo.FindAll(ctx)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(books)
}

func (h *Handler) CreateBook(w http.ResponseWriter, r *http.Request) {
    var book Book
    if err := json.NewDecoder(r.Body).Decode(&book); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    _, err := h.repo.Insert(ctx, book)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.WriteHeader(http.StatusCreated)
}

Точка входа

/cmd/api/main.go:

package main

import (
    "log"
    "net/http"

    "github.com/gorilla/mux"
    "github.com/username/mongo-gorilla-api/internal/books"
    "github.com/username/mongo-gorilla-api/pkg/db"
)

func main() {
    database := db.Connect("mongodb://localhost:27017")

    repo := books.NewRepository(database)
    handler := books.NewHandler(repo)

    r := mux.NewRouter()
    r.HandleFunc("/books", handler.GetBooks).Methods("GET")
    r.HandleFunc("/books", handler.CreateBook).Methods("POST")

    log.Println("Сервер запущен на :8080")
    log.Fatal(http.ListenAndServe(":8080", r))
}

Проверка API

Создание книги:

curl -X POST http://localhost:8080/books \
     -H "Content-Type: application/json" \
     -d '{"title":"Go in Action","authors":["William Kennedy"],"publish_date":"2015-11-26","publisher":{"name":"Manning","country":"US"}}'

Получение списка:

curl http://localhost:8080/books

Заключение

В результате мы получили минималистичный, но масштабируемый REST API на Go, интегрированный с MongoDB. Использование официального драйвера и структурированного подхода к архитектуре позволяет легко расширять сервис, добавляя валидацию, аутентификацию и более сложные бизнес-правила.

В дальнейшем можно:

  • Вынести конфигурацию в .env и использовать viper или envconfig;

  • Добавить логирование (zerolog, zap);

  • Реализовать graceful shutdown;

  • Подключить Swagger/OpenAPI для документации.