В этом уроке мы соединим 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, затем валидация и тесты
Что открыть дальше
- HTTP server на Go: первый API без фреймворка — база для handler, status code и JSON.
- Go modules: go mod init, tidy и зависимости — чтобы понимать, как добавился
pgx. - Ошибки в Go: error, panic и recover без суеты — следующий важный урок для работы с базой.
- Struct и методы в Go: первый объект без ООП-магии —
TaskиAppздесь построены именно наstruct.



