Prisma → sqlc (генерирует типизированный код из SQL)
@Injectable() + DI-контейнер → Wire (генерирует DI-код)
tsoa / swagger-codegen → oapi-codegen (генерирует сервер из OpenAPI)
graphql-codegen → gqlgen (GraphQL, не в этом уроке)
go generate — единая команда для всей генерацииmkdir go-codegen && cd go-codegen
go mod init go-codegen
# Устанавливаем инструменты
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
go install github.com/google/wire/cmd/wire@latest
go install github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen@latest
# Создаём структуру
mkdir -p internal/database
mkdir -p internal/database/queries
mkdir -p internal/di
mkdir -p internal/api
mkdir -p cmd/server
internal/database/schema.sql-- Схема БД для sqlc
CREATE TABLE IF NOT EXISTS authors (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
bio TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS books (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
author_id UUID NOT NULL REFERENCES authors(id) ON DELETE CASCADE,
price BIGINT NOT NULL, -- цена в центах
published BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_books_author ON books(author_id);
internal/database/queries/authors.sql-- name: CreateAuthor :one
INSERT INTO authors (name, bio)
VALUES ($1, $2)
RETURNING *;
-- name: GetAuthor :one
SELECT * FROM authors WHERE id = $1;
-- name: ListAuthors :many
SELECT * FROM authors
ORDER BY created_at DESC
LIMIT $1 OFFSET $2;
-- name: UpdateAuthor :one
UPDATE authors
SET name = $2, bio = $3
WHERE id = $1
RETURNING *;
-- name: DeleteAuthor :exec
DELETE FROM authors WHERE id = $1;
-- name: CountAuthors :one
SELECT COUNT(*) FROM authors;
internal/database/queries/books.sql-- name: CreateBook :one
INSERT INTO books (title, author_id, price, published)
VALUES ($1, $2, $3, $4)
RETURNING *;
-- name: GetBook :one
SELECT * FROM books WHERE id = $1;
-- name: ListBooksByAuthor :many
SELECT * FROM books
WHERE author_id = $1
ORDER BY created_at DESC;
-- name: UpdateBook :one
UPDATE books
SET title = $2, price = $3, published = $4
WHERE id = $1
RETURNING *;
-- name: DeleteBook :exec
DELETE FROM books WHERE id = $1;
-- name: GetAuthorWithBooks :many
SELECT
a.id AS author_id,
a.name AS author_name,
a.bio AS author_bio,
b.id AS book_id,
b.title AS book_title,
b.price AS book_price
FROM authors a
LEFT JOIN books b ON b.author_id = a.id
WHERE a.id = $1
ORDER BY b.created_at;
sqlc.yamlversion: "2"
sql:
- engine: "postgresql"
queries: "internal/database/queries"
schema: "internal/database/schema.sql"
gen:
go:
package: "database"
out: "internal/database"
sql_package: "pgx/v5"
emit_json_tags: true
emit_pointers_for_null_types: true
emit_interface: true # Генерирует Querier интерфейс!
overrides:
- db_type: "uuid"
go_type: "github.com/google/uuid.UUID"
- db_type: "timestamptz"
go_type: "time.Time"
internal/di/wire.go// go:build wireinject
// +build wireinject
package di
import (
"context"
"github.com/google/wire"
"github.com/jackc/pgx/v5/pgxpool"
"go-codegen/internal/database"
)
// ProviderSet — набор всех провайдеров
var ProviderSet = wire.NewSet(
ProvideDBPool,
ProvideQuerier,
ProvideServerConfig,
// Добавляйте новые провайдеры сюда
)
// ProvideDBPool — создаёт пул PostgreSQL
func ProvideDBPool(ctx context.Context, cfg ServerConfig) (*pgxpool.Pool, error) {
poolCfg, err := pgxpool.ParseConfig(cfg.DatabaseURL)
if err != nil {
return nil, err
}
poolCfg.MaxConns = 20
pool, err := pgxpool.NewWithConfig(ctx, poolCfg)
if err != nil {
return nil, err
}
if err := pool.Ping(ctx); err != nil {
return nil, err
}
return pool, nil
}
// ProvideQuerier — создаёт Querier (сгенерирован sqlc)
func ProvideQuerier(pool *pgxpool.Pool) database.Querier {
return database.New(pool)
}
// ServerConfig — конфигурация сервера
type ServerConfig struct {
Port string
DatabaseURL string
}
func ProvideServerConfig() ServerConfig {
return ServerConfig{
Port: "8080",
DatabaseURL: "postgres://postgres:secret@localhost:5432/appdb?sslmode=disable",
}
}
// InitializeApp — точка входа для Wire (будет сгенерировано)
func InitializeApp(ctx context.Context) (*App, error) {
wire.Build(
ProviderSet,
wire.Struct(new(App), "*"),
)
return nil, nil
}
// App — главная структура приложения
type App struct {
Config ServerConfig
Pool *pgxpool.Pool
Querier database.Querier
}
internal/api/openapi.yamlopenapi: "3.0.3"
info:
title: Bookstore API
version: "1.0.0"
description: API для управления книгами и авторами
servers:
- url: http://localhost:8080/api/v1
paths:
/authors:
post:
operationId: createAuthor
summary: Создать автора
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateAuthorRequest'
responses:
'201':
description: Автор создан
content:
application/json:
schema:
$ref: '#/components/schemas/Author'
get:
operationId: listAuthors
summary: Список авторов
parameters:
- name: limit
in: query
schema:
type: integer
default: 10
- name: offset
in: query
schema:
type: integer
default: 0
responses:
'200':
description: Список авторов
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Author'
/authors/{id}:
get:
operationId: getAuthor
summary: Получить автора
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Автор
content:
application/json:
schema:
$ref: '#/components/schemas/Author'
'404':
description: Не найден
/books:
post:
operationId: createBook
summary: Создать книгу
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateBookRequest'
responses:
'201':
description: Книга создана
content:
application/json:
schema:
$ref: '#/components/schemas/Book'
components:
schemas:
Author:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
bio:
type: string
created_at:
type: string
format: date-time
CreateAuthorRequest:
type: object
required: [name]
properties:
name:
type: string
minLength: 1
bio:
type: string
Book:
type: object
properties:
id:
type: string
format: uuid
title:
type: string
author_id:
type: string
format: uuid
price:
type: integer
description: Цена в центах
published:
type: boolean
created_at:
type: string
format: date-time
CreateBookRequest:
type: object
required: [title, author_id, price]
properties:
title:
type: string
author_id:
type: string
format: uuid
price:
type: integer
published:
type: boolean
default: false
oapi-codegen.yamlpackage: api
output: internal/api/server.gen.go
generate:
std-http-server: true
models: true
strict-server: true
cmd/server/main.gopackage main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"go-codegen/internal/api"
"go-codegen/internal/database"
"go-codegen/internal/di"
)
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
log.Println("🚀 Запуск приложения (sqlc + Wire + oapi-codegen)...")
ctx := context.Background()
// 1. Инициализация через Wire (DI)
// В реальном коде — использовать сгенерированный wire_gen.go
// app, err := di.InitializeApp(ctx)
// Для демонстрации — ручная инициализация
cfg := di.ProvideServerConfig()
pool, err := di.ProvideDBPool(ctx, cfg)
if err != nil {
log.Fatalf("БД: %v", err)
}
defer pool.Close()
querier := database.New(pool)
// 2. Создаём API-сервер (сгенерирован oapi-codegen)
apiServer := api.NewServer(querier)
// 3. Настраиваем роутер
handler := api.HandlerWithOptions(
api.NewStrictHandler(apiServer, nil),
api.StdHTTPServerOptions{
BaseURL: "/api/v1",
Middlewares: []api.MiddlewareFunc{loggingMiddleware},
},
)
mux := http.NewServeMux()
mux.Handle("/", handler)
server := &http.Server{Addr: ":" + cfg.Port, Handler: mux}
go func() {
log.Printf("✅ Сервер на http://localhost:%s/api/v1", cfg.Port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Сервер: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("🛑 Выключение...")
server.Shutdown(context.Background())
}
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
// В реальном проекте вам понадобятся:
// - internal/database/ (сгенерировано sqlc)
// - internal/api/ (сгенерировано oapi-codegen)
// - internal/di/wire_gen.go (сгенерировано Wire)
// - internal/api/server.go (ваша реализация интерфейса)
internal/api/server.go (реализация)package api
import (
"context"
"net/http"
"go-codegen/internal/database"
"github.com/google/uuid"
)
// Server — реализация сгенерированного интерфейса StrictServerInterface
type Server struct {
db database.Querier
}
func NewServer(db database.Querier) *Server {
return &Server{db: db}
}
// CreateAuthor — POST /authors
func (s *Server) CreateAuthor(ctx context.Context, req CreateAuthorRequest) (CreateAuthorResponse, error) {
params := database.CreateAuthorParams{
Name: req.Name,
Bio: ptrToString(req.Bio),
}
author, err := s.db.CreateAuthor(ctx, params)
if err != nil {
return nil, err
}
return CreateAuthor201JSONResponse{
Id: author.ID,
Name: author.Name,
Bio: author.Bio,
CreatedAt: author.CreatedAt,
}, nil
}
// GetAuthor — GET /authors/{id}
func (s *Server) GetAuthor(ctx context.Context, req GetAuthorRequest) (GetAuthorResponse, error) {
author, err := s.db.GetAuthor(ctx, req.Id)
if err != nil {
return GetAuthor404JSONResponse{Message: "Author not found"}, nil
}
return GetAuthor200JSONResponse{
Id: author.ID,
Name: author.Name,
Bio: author.Bio,
CreatedAt: author.CreatedAt,
}, nil
}
// ListAuthors — GET /authors
func (s *Server) ListAuthors(ctx context.Context, req ListAuthorsRequest) (ListAuthorsResponse, error) {
limit := int32(10)
offset := int32(0)
if req.Limit != nil {
limit = *req.Limit
}
if req.Offset != nil {
offset = *req.Offset
}
authors, err := s.db.ListAuthors(ctx, database.ListAuthorsParams{
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, err
}
var resp ListAuthors200JSONResponse
for _, a := range authors {
resp = append(resp, Author{
Id: a.ID,
Name: a.Name,
Bio: a.Bio,
CreatedAt: a.CreatedAt,
})
}
return resp, nil
}
// CreateBook — POST /books
func (s *Server) CreateBook(ctx context.Context, req CreateBookRequest) (CreateBookResponse, error) {
params := database.CreateBookParams{
Title: req.Title,
AuthorID: req.AuthorId,
Price: req.Price,
Published: req.Published,
}
book, err := s.db.CreateBook(ctx, params)
if err != nil {
return nil, err
}
return CreateBook201JSONResponse{
Id: book.ID,
Title: book.Title,
AuthorId: book.AuthorID,
Price: book.Price,
Published: book.Published,
CreatedAt: book.CreatedAt,
}, nil
}
func ptrToString(s *string) string {
if s == nil {
return ""
}
return *s
}
Makefile# ╔══════════════════════════════════════════════════════════╗
# ║ КОМАНДЫ ГЕНЕРАЦИИ КОДА ║
# ╚══════════════════════════════════════════════════════════╝
.PHONY: generate generate-sqlc generate-wire generate-oapi
# Вся генерация одной командой
generate: generate-sqlc generate-wire generate-oapi
@echo "✅ Весь код сгенерирован"
# sqlc: генерация типизированного кода из SQL
generate-sqlc:
@echo "📦 Генерация sqlc..."
sqlc generate
# Wire: генерация DI-кода
generate-wire:
@echo "🔌 Генерация Wire..."
cd internal/di && wire
# oapi-codegen: генерация HTTP-сервера из OpenAPI
generate-oapi:
@echo "📄 Генерация oapi-codegen..."
oapi-codegen -config oapi-codegen.yaml internal/api/openapi.yaml
# Альтернатива: go generate (вызывает всё)
# Добавьте в generate.go:
# //go:generate sqlc generate
# //go:generate wire ./internal/di
# //go:generate oapi-codegen -config oapi-codegen.yaml internal/api/openapi.yaml
# Установка инструментов (один раз)
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
go install github.com/google/wire/cmd/wire@latest
go install github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen@latest
# Генерация всего кода
make generate
# Или по отдельности:
sqlc generate # Генерирует internal/database/*.go
cd internal/di && wire # Генерирует internal/di/wire_gen.go
oapi-codegen -config oapi-codegen.yaml internal/api/openapi.yaml
# Проверяем, что всё скомпилировалось
go build ./...
# Запуск
go run ./cmd/server/main.go
| Инструмент | Из чего генерирует | Что генерирует | Аналог в Node.js |
|---|---|---|---|
| sqlc | SQL-запросы | Типизированные Go-функции | Prisma, kysely |
| Wire | Провайдеры (Go-функции) | Код инициализации DI | InversifyJS, tsyringe |
| oapi-codegen | OpenAPI/Swagger YAML | HTTP-сервер + модели | tsoa, swagger-codegen |
| gqlgen | GraphQL-схема | GraphQL-сервер | graphql-codegen |
| protoc-gen-go | .proto файлы | gRPC-сервер/клиент | protobuf-ts |
💡 Best practices от сеньоров:
//go:generate в файл generate.go для единой команды.
💡 Для Node.js разработчика: