Разработка бэкенда сайта на Go (Gin)
Go и Gin — это выбор, когда производительность и предсказуемость важнее скорости написания кода. Go компилируется в статический бинарник без зависимостей, потребляет ~10–20 МБ памяти на старте (против 200–500 МБ у JVM-приложений), обрабатывает тысячи одновременных соединений на горутинах. Gin — наиболее распространённый HTTP-фреймворк для Go с минимальным overhead поверх стандартной библиотеки.
Типичные сценарии: публичные API с высокой нагрузкой, микросервисы, замена тяжёлых Node.js/Python сервисов при масштабировании.
Структура проекта
Go-проекты принято организовывать по функциональным доменам, не по техническим слоям:
cmd/
api/
main.go # точка входа
internal/
config/
config.go # конфигурация через envconfig или viper
domain/
product/
handler.go # HTTP handlers
service.go # бизнес-логика
repository.go
model.go
user/
order/
middleware/
auth.go
logger.go
recovery.go
database/
postgres.go
migrations/
server/
router.go # регистрация всех маршрутов
server.go
pkg/
validator/
response/
Основной сервер
// cmd/api/main.go
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/myapp/internal/config"
"github.com/myapp/internal/database"
"github.com/myapp/internal/server"
)
func main() {
cfg := config.Load()
db := database.NewPostgres(cfg.DatabaseURL)
defer db.Close()
srv := server.New(cfg, db)
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx)
}
Роутер
// internal/server/router.go
package server
import (
"github.com/gin-gonic/gin"
"github.com/myapp/internal/middleware"
)
func (s *Server) setupRouter() *gin.Engine {
if s.cfg.Env == "production" {
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
r.Use(middleware.Logger())
r.Use(middleware.Recovery())
r.Use(middleware.CORS(s.cfg.AllowedOrigins))
v1 := r.Group("/api/v1")
{
auth := v1.Group("/auth")
auth.POST("/login", s.authHandler.Login)
auth.POST("/refresh", s.authHandler.Refresh)
products := v1.Group("/products")
products.GET("", s.productHandler.List)
products.GET("/:id", s.productHandler.Get)
products.Use(middleware.JWT(s.cfg.JWTSecret))
{
products.POST("", middleware.RequireRole("admin"), s.productHandler.Create)
products.PUT("/:id", middleware.RequireRole("admin"), s.productHandler.Update)
products.DELETE("/:id", middleware.RequireRole("admin"), s.productHandler.Delete)
}
}
return r
}
Handler
// internal/domain/product/handler.go
package product
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type Handler struct {
service *Service
}
func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
type ListQuery struct {
Page int `form:"page,default=1" binding:"min=1"`
Limit int `form:"limit,default=20" binding:"min=1,max=100"`
CategoryID *int `form:"category_id"`
Search string `form:"search"`
}
func (h *Handler) List(c *gin.Context) {
var q ListQuery
if err := c.ShouldBindQuery(&q); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
products, total, err := h.service.List(c.Request.Context(), ListParams{
Page: q.Page,
Limit: q.Limit,
CategoryID: q.CategoryID,
Search: q.Search,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
return
}
c.JSON(http.StatusOK, gin.H{
"data": products,
"pagination": gin.H{
"page": q.Page,
"limit": q.Limit,
"total": total,
},
})
}
type CreateRequest struct {
Name string `json:"name" binding:"required,min=2,max=255"`
Price float64 `json:"price" binding:"required,gt=0"`
CategoryID *int `json:"category_id"`
Description string `json:"description" binding:"max=5000"`
}
func (h *Handler) Create(c *gin.Context) {
var req CreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"errors": parseValidationErrors(err)})
return
}
product, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleServiceError(c, err)
return
}
c.JSON(http.StatusCreated, product)
}
Repository и pgx
// internal/domain/product/repository.go
package product
import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
)
type Repository struct {
db *pgxpool.Pool
}
func (r *Repository) FindAll(ctx context.Context, params ListParams) ([]*Product, int, error) {
offset := (params.Page - 1) * params.Limit
var countQuery = `SELECT COUNT(*) FROM products WHERE is_active = true`
var listQuery = `
SELECT p.id, p.name, p.slug, p.price, p.created_at,
c.id as category_id, c.name as category_name
FROM products p
LEFT JOIN categories c ON c.id = p.category_id
WHERE p.is_active = true
ORDER BY p.created_at DESC
LIMIT $1 OFFSET $2
`
var total int
if err := r.db.QueryRow(ctx, countQuery).Scan(&total); err != nil {
return nil, 0, err
}
rows, err := r.db.Query(ctx, listQuery, params.Limit, offset)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var products []*Product
for rows.Next() {
var p Product
if err := rows.Scan(&p.ID, &p.Name, &p.Slug, &p.Price, &p.CreatedAt,
&p.Category.ID, &p.Category.Name); err != nil {
return nil, 0, err
}
products = append(products, &p)
}
return products, total, rows.Err()
}
pgx/v5 — быстрейший PostgreSQL-драйвер для Go. pgxpool управляет пулом соединений.
JWT middleware
// internal/middleware/auth.go
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
UserID int `json:"sub"`
Role string `json:"role"`
jwt.RegisteredClaims
}
func JWT(secret string) gin.HandlerFunc {
return func(c *gin.Context) {
auth := c.GetHeader("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
token, err := jwt.ParseWithClaims(auth[7:], &Claims{}, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return []byte(secret), nil
})
if err != nil || !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
}
claims := token.Claims.(*Claims)
c.Set("userID", claims.UserID)
c.Set("role", claims.Role)
c.Next()
}
}
func RequireRole(roles ...string) gin.HandlerFunc {
roleSet := make(map[string]struct{}, len(roles))
for _, r := range roles {
roleSet[r] = struct{}{}
}
return func(c *gin.Context) {
role, _ := c.Get("role")
if _, ok := roleSet[role.(string)]; !ok {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "forbidden"})
return
}
c.Next()
}
}
Тестирование
func TestProductHandler_List(t *testing.T) {
gin.SetMode(gin.TestMode)
mockService := &MockProductService{}
h := NewHandler(mockService)
r := gin.New()
r.GET("/products", h.List)
mockService.On("List", mock.Anything, mock.AnythingOfType("ListParams")).
Return([]*Product{{ID: 1, Name: "Test"}}, 1, nil)
req := httptest.NewRequest(http.MethodGet, "/products?page=1&limit=20", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// ...
}
Сроки разработки
- Структура + конфигурация + БД — 3–5 дней
- Handlers + роутер + middleware — 1–1,5 недели
- Business logic + repository — 1–3 недели
- Тесты — 1 неделя
- Docker + CI — 2–3 дня
API для сайта или сервиса: 4–8 недель. Go требует больше кода, чем Python/Node.js, но даёт бинарник с предсказуемой производительностью и минимальными operational costs.







