Go и PostgreSQL: мини-API с базой данных

В этом уроке мы соединим Go и PostgreSQL без лишней архитектурной мишуры: подключимся к базе через pgxpool, создадим endpoint /tasks, прочитаем строки из таблицы и вернем JSON. Это не «идеальный production-шаблон», а честный первый backend-пример, который можно повторить руками и потом развивать

Если вы уже прошли HTTP server на Go: первый API без фреймворка и Go modules: go mod init, tidy и зависимости, здесь все начнет складываться в нормальный мини-проект

Что получится в конце

Запрос:

curl http://localhost:8080/tasks

вернет:

[
  {"id":1,"title":"Разобрать Go и PostgreSQL","done":false}
]

Данные будут приходить не из массива в коде, а из таблицы PostgreSQL

Что нужно заранее

Нужны Go, PostgreSQL и пустая база для эксперимента. Для локального урока удобно создать базу go_lessons и таблицу tasks

SQL:

create table tasks (
    id serial primary key,
    title text not null,
    done boolean not null default false
);

insert into tasks (title, done)
values ('Разобрать Go и PostgreSQL', false);

Строка подключения в примерах будет такой:

postgres://postgres:postgres@localhost:5432/go_lessons

Замените пользователя, пароль, порт и имя базы под свою локальную установку

Создаем проект и ставим pgx

mkdir go-postgres-api
cd go-postgres-api
go mod init example.com/go-postgres-api
go get github.com/jackc/pgx/v5/pgxpool

pgx — популярный PostgreSQL-драйвер и toolkit для Go. Для API удобнее брать pgxpool, потому что серверу нужен пул соединений, а не одно глобальное соединение на все запросы

Базовый код сервера

Создайте main.go:

package main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"
    "os"

    "github.com/jackc/pgx/v5/pgxpool"
)

type Task struct {
    ID    int    `json:"id"`
    Title string `json:"title"`
    Done  bool   `json:"done"`
}

type App struct {
    db *pgxpool.Pool
}

func main() {
    databaseURL := os.Getenv("DATABASE_URL")
    if databaseURL == "" {
        databaseURL = "postgres://postgres:postgres@localhost:5432/go_lessons"
    }

    ctx := context.Background()

    db, err := pgxpool.New(ctx, databaseURL)
    if err != nil {
        log.Fatal("connect database:", err)
    }
    defer db.Close()

    app := App{db: db}

    mux := http.NewServeMux()
    mux.HandleFunc("/tasks", app.tasksHandler)

    log.Println("server started on http://localhost:8080")
    if err := http.ListenAndServe(":8080", mux); err != nil {
        log.Fatal(err)
    }
}

Пока endpoint еще не написан, но уже есть главное: конфигурация, подключение к базе, defer db.Close() и структура App, которая хранит зависимости сервера

Handler для чтения задач

Добавьте в этот же файл:

func (a App) tasksHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodGet {
        writeJSON(w, http.StatusMethodNotAllowed, map[string]string{
            "error": "method not allowed",
        })
        return
    }

    tasks, err := a.listTasks(r.Context())
    if err != nil {
        log.Println("list tasks:", err)
        writeJSON(w, http.StatusInternalServerError, map[string]string{
            "error": "internal server error",
        })
        return
    }

    writeJSON(w, http.StatusOK, tasks)
}

Обратите внимание на r.Context(). В HTTP-сервере это лучше, чем везде использовать context.Background(): если клиент отменит запрос, контекст запроса тоже может быть отменен

Запрос к PostgreSQL

Добавьте функцию:

func (a App) listTasks(ctx context.Context) ([]Task, error) {
    rows, err := a.db.Query(ctx, `
        select id, title, done
        from tasks
        order by id
    `)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    tasks := make([]Task, 0)

    for rows.Next() {
        var task Task
        if err := rows.Scan(&task.ID, &task.Title, &task.Done); err != nil {
            return nil, err
        }
        tasks = append(tasks, task)
    }

    if err := rows.Err(); err != nil {
        return nil, err
    }

    return tasks, nil
}

Три детали здесь особенно важны

Первая: после Query нужно закрыть rows через defer rows.Close()

Вторая: Scan должен соответствовать порядку колонок в select

Третья: после цикла нужно проверить rows.Err(). Ошибка может возникнуть не только в момент запроса, но и во время чтения строк

JSON-ответ

Добавьте helper:

func writeJSON(w http.ResponseWriter, status int, data any) {
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.WriteHeader(status)

    if err := json.NewEncoder(w).Encode(data); err != nil {
        log.Println("write json:", err)
    }
}

Это тот же прием, что и в уроке про HTTP server: не собираем JSON руками, а отдаем работу encoding/json

Запуск и проверка

Запустите сервер:

go run .

В другом окне:

curl http://localhost:8080/tasks

Если база доступна и таблица создана, вы получите JSON. Если пароль, порт или имя базы неверные, сервер не стартует или вернет ошибку подключения

Как добавить POST позже

Следующий шаг после чтения — создать endpoint POST /tasks. Для этого нужно:

  • прочитать JSON body через json.NewDecoder(r.Body).Decode(...);
  • проверить, что title не пустой;
  • выполнить insert into tasks (title) values ($1) returning id, title, done;
  • вернуть созданную задачу со статусом 201 Created.

Но в первом материале лучше не смешивать чтение, создание, валидацию и миграции. Сначала важно понять цепочку: HTTP handler -> функция работы с базой -> SQL -> JSON response

Частые ошибки

connection refused. PostgreSQL не запущен, слушает другой порт или строка подключения указывает не туда

password authentication failed. Неверный пользователь или пароль. Проверьте DATABASE_URL

relation "tasks" does not exist. Таблица не создана в той базе, к которой подключился сервер

can't scan into dest. Колонки в SQL не совпадают с типами или порядком аргументов в rows.Scan

Сервер зависает или соединения копятся. Проверьте, что rows.Close() вызывается, и не забывайте использовать пул соединений для серверного приложения

Что может быть еще интересно по этой теме

Почему pgxpool, а не database/sql? Оба подхода рабочие. database/sql — стандартный интерфейс, а pgxpool дает PostgreSQL-ориентированный пул и удобные возможности pgx. Для учебного PostgreSQL-примера pgxpool получается прямее

Нужно ли хранить DATABASE_URL прямо в коде? В реальном проекте нет. В уроке fallback удобен, но для публикации, Docker и сервера строку подключения выносят в переменные окружения

Где должны жить SQL-миграции? Не в main.go. Для первого запуска мы создали таблицу руками, но в проекте нужны миграции отдельным инструментом или отдельной командой

Можно ли сразу делать полноценный CRUD? Можно, но лучше наращивать по шагам: сначала GET, потом POST, потом PATCH/DELETE, затем валидация и тесты

Что открыть дальше

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x