Добавили закрытые сообщества🔒
Контент доступен только подписчикам
Let’s go🙃
Циклы в Go
В Go только один вид цикла - for. Но он закрывает все случаи, которые в других языках решаются через while, do-while, foreach.
Базовый for
Классический трёхкомпонентный цикл, как в C/Java.
for init; condition; post {
// тело
}
Пример:
for i := 0; i < 5; i++ {
fmt.Println(i) // 0 1 2 3 4
}
for как while
Убираем init и post - получаем аналог while.
n := 1
for n < 100 {
n *= 2
}
fmt.Println(n) // 128
Бесконечный цикл
for {
// выполняется вечно
// выход - только через break или return
}
Практический пример - сервер/воркер:
func worker(jobs <-chan int) {
for {
job, ok := <-jobs
if !ok {
return // канал закрыт - выходим
}
process(job)
}
}
for range
Итерация по слайсу. Возвращает index, value.
Срез (slice)
nums := []int{10, 20, 30}
for i, v := range nums {
fmt.Printf("index=%d value=%d\n", i, v)
}
// index=0 value=10
// index=1 value=20
// index=2 value=30
Map
scores := map[string]int{"Alice": 95, "Bob": 87}
for name, score := range scores {
fmt.Printf("%s: %d\n", name, score)
}
⚠️ Порядок итерации по map не гарантирован.
Строка (string)
for i, r := range "hello" {
fmt.Printf("%d: %c\n", i, r)
}
// итерация по рунам (unicode), не байтам
Канал (channel)
ch := make(chan int, 3)
ch <- 1; ch <- 2; ch <- 3
close(ch)
for v := range ch {
fmt.Println(v) // 1 2 3
}
// range по каналу завершится только после close(ch)
Только индекс / только значение
// только индекс
for i := range nums { }
// только значение (индекс игнорируем)
for _, v := range nums { }
Управление циклом
break
Выход из текущего цикла.
for i := 0; i < 10; i++ {
if i == 5 {
break
}
fmt.Println(i) // 0 1 2 3 4
}
continue
Пропуск текущей итерации.
for i := 0; i < 5; i++ {
if i%2 == 0 {
continue
}
fmt.Println(i) // 1 3
}
Labels — выход из вложенных циклов
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
break outer // выходим из ВНЕШНЕГО цикла
}
fmt.Printf("i=%d j=%d\n", i, j)
}
}
// i=0 j=0
Подводные камни
1. Захват переменной в горутине
// ❌ Неправильно — все горутины напечатают последнее значение i
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i)
}()
}
// ✅ Правильно — передаём i как аргумент
for i := 0; i < 3; i++ {
go func(i int) {
fmt.Println(i)
}(i)
}
Go 1.22+: в новых версиях поведение изменено — переменная цикла создаётся заново на каждой итерации. Но знать про эту проблему всё равно нужно.
2. Изменение слайса во время итерации
// ❌ Не удаляй элементы из слайса в range - работаешь с копией
nums := []int{1, 2, 3}
for _, v := range nums {
nums = append(nums, v*10) // бесконечного цикла не будет, но данные будут неожиданными
}
// ✅ Если нужно собрать новый слайс — используй отдельный
var result []int
for _, v := range nums {
result = append(result, v*10)
}
3. range по map — детерминированность
// Если нужен стабильный порядок — сначала отсортируй ключи
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, m[k])
}
Давайте подведем итог
- В Go один цикл -
for, но он покрывает все сценарии for range— основной инструмент для слайсов- Следи за захватом переменных в горутинах
- Labels — редкий, но иногда незаменимый инструмент
Каналы в Go
Каналы в go очень помогают при проектировании конкурентного кода, давайте разберемся как с ними работать, начнем)
Что Под Капотом
Канал — это типобезопасная очередь для синхронизации goroutine’ов. Под капотом это структура с внутренним буфером, mutex’ами и условными переменными.
type hchan struct {
qcount uint // кол-во элементов в очереди
dataqsiz uint // размер буфера
buf unsafe.Pointer // указатель на буфер
elemsize uint16 // размер элемента
closed uint32 // флаг закрытия
elemtype *_type // тип элемента
sendx uint // индекс отправки
recvx uint // индекс приёма
recvq waitq // очередь goroutine'ов, ждущих приёма
sendq waitq // очередь goroutine'ов, ждущих отправки
lock mutex
}
При отправке в канал:
- Захватывается lock
- Если есть ждущий receiver — он просыпается, данные копируются напрямую
- Иначе данные пишутся в буфер (если есть место)
- Если буфер полон — goroutine паркуется в sendq
При приёме — всё наоборот.
Типы Каналов и Их Поведение
Небуферизованные (синхронные)
// Блокирует до момента, когда receiver готов
ch := make(chan int)
go func() {
ch <- 42 // ждёт, пока кто-то прочитает
}()
fmt.Println(<-ch) // 42
Когда использовать: Строгая синхронизация, handshake между goroutine’ами, гарантия выполнения.
Буферизованные (асинхронные)
ch := make(chan int, 3)
ch <- 1 // не блокирует
ch <- 2 // не блокирует
ch <- 3 // не блокирует
// ch <- 4 // panic: send on closed channel
fmt.Println(<-ch) // 1
Подводный камень: Если горутина паникует после отправки, данные потеряются. Проверяй закрытие канала.
Directional Channels (только отправка/приём)
func producer(ch chan<- int) {
ch <- 42
}
func consumer(ch <-chan int) {
fmt.Println(<-ch)
}
func main() {
ch := make(chan int)
go producer(ch)
consumer(ch)
}
Плюс: Компилятор гарантирует, что функция не закроет чужой канал.
Select: Многоплексирование
select {
case val := <-ch1:
fmt.Println("from ch1:", val)
case val := <-ch2:
fmt.Println("from ch2:", val)
case ch3 <- result:
fmt.Println("sent to ch3")
case <-time.After(1 * time.Second):
fmt.Println("timeout")
default:
fmt.Println("no channel ready")
}
Как работает: Select ждёт, пока один из case’ов не будет готов. Если готовы несколько — выбирает случайно.
Реальная задача — работер с таймаутом:
func worker(jobs <-chan string, results chan<- string, ctx context.Context) {
for {
select {
case job, ok := <-jobs:
if !ok {
return // канал закрыт
}
results <- process(job)
case <-ctx.Done():
return // контекст отменён
}
}
}
Паттерны
1. Worker Pool с Rate Limiting
type WorkerPool struct {
jobs chan Task
results chan Result
wg sync.WaitGroup
}
type Task struct {
ID int
Data string
}
type Result struct {
TaskID int
Output string
Err error
}
func NewWorkerPool(numWorkers, bufferSize int) *WorkerPool {
return &WorkerPool{
jobs: make(chan Task, bufferSize),
results: make(chan Result, bufferSize),
}
}
func (wp *WorkerPool) Start(ctx context.Context, numWorkers int) {
for i := 0; i < numWorkers; i++ {
wp.wg.Add(1)
go func(workerID int) {
defer wp.wg.Done()
for {
select {
case task, ok := <-wp.jobs:
if !ok {
return
}
// Симуляция работы
time.Sleep(time.Millisecond * 100)
wp.results <- Result{
TaskID: task.ID,
Output: fmt.Sprintf("Processed: %s", task.Data),
}
case <-ctx.Done():
return
}
}
}(i)
}
}
func (wp *WorkerPool) Submit(task Task) {
wp.jobs <- task
}
func (wp *WorkerPool) Close() {
close(wp.jobs)
wp.wg.Wait()
close(wp.results)
}
func (wp *WorkerPool) Results() <-chan Result {
return wp.results
}
// Использование
pool := NewWorkerPool(5, 100)
ctx := context.Background()
pool.Start(ctx, 5)
for i := 0; i < 20; i++ {
pool.Submit(Task{ID: i, Data: fmt.Sprintf("task-%d", i)})
}
pool.Close()
for result := range pool.Results() {
fmt.Printf("Task %d: %s\n", result.TaskID, result.Output)
}
2. Fan-Out / Fan-In (Pipeline)
// Fan-Out: распределение работы
func distribute(input <-chan int, numWorkers int) []<-chan int {
channels := make([]<-chan int, numWorkers)
for i := 0; i < numWorkers; i++ {
ch := make(chan int, 10)
channels[i] = ch
go func(out chan<- int, id int) {
for val := range input {
if val%numWorkers == id {
out <- val * 2
}
}
close(out)
}(ch, i)
}
return channels
}
// Fan-In: сбор результатов
func merge(channels ...<-chan int) <-chan int {
out := make(chan int, 10)
var wg sync.WaitGroup
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for val := range c {
out <- val
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
// Использование
input := make(chan int, 100)
go func() {
for i := 0; i < 100; i++ {
input <- i
}
close(input)
}()
workers := distribute(input, 4)
results := merge(workers...)
for result := range results {
fmt.Println(result)
}
3. Graceful Shutdown
type Server struct {
requests chan *http.Request
done chan struct{}
mu sync.Mutex
closed bool
}
func NewServer() *Server {
return &Server{
requests: make(chan *http.Request, 100),
done: make(chan struct{}),
}
}
func (s *Server) Start() {
go func() {
for {
select {
case req := <-s.requests:
s.handleRequest(req)
case <-s.done:
return
}
}
}()
}
func (s *Server) Submit(req *http.Request) error {
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return fmt.Errorf("server closed")
}
s.mu.Unlock()
select {
case s.requests <- req:
return nil
case <-time.After(1 * time.Second):
return fmt.Errorf("timeout")
}
}
func (s *Server) Shutdown(ctx context.Context) error {
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return nil
}
s.closed = true
s.mu.Unlock()
// Даём время на обработку текущих запросов
select {
case <-time.After(5 * time.Second):
close(s.done)
case <-ctx.Done():
close(s.done)
return ctx.Err()
}
return nil
}
func (s *Server) handleRequest(req *http.Request) {
time.Sleep(100 * time.Millisecond)
fmt.Printf("Processed: %s\n", req.URL)
}
Deadlock’и и Как их Избежать
Проблема: Циклическая зависимость
// ❌ DEADLOCK
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
<-ch1
ch2 <- 1
}()
ch1 <- 0
<-ch2
Почему: main ждёт ch2, а goroutine ждёт ch1.
Решение: Используй буферизацию или контекст
// ✅ SAFE
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch1 <- 0
ch2 <- 1
<-ch1
<-ch2
Отладка
// Debug: посмотри статус канала через reflect
func chanStatus(ch interface{}) {
v := reflect.ValueOf(ch)
fmt.Printf("Cap: %d, Len: %d\n", v.Cap(), v.Len())
}
// Используй pprof для поиска утечек
import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Открой http://localhost:6060/debug/pprof/goroutine
Итог
- Небуферизованные - синхронизация
- Буферизованные - decoupling с контролем нагрузки
- Select - многоплексирование + graceful shutdown
- Directional - compile-time safety
Главное: только sender закрывает канал, остальное управляй контекстом и sync.WaitGroup.
Graceful Shutdown
Когда приложение получает сигнал о завершении, оно должно корректно завершить все работающие операции. Graceful shutdown - это гарантия того, что сервис закрывается чисто, без потери данных и с завершением всех текущих запросов.
Почему graceful shutdown важен
1. Сохранение данных
При внезапной остановке приложения могут потеряться несохраненные данные. Graceful shutdown дает возможность завершить текущие операции и записать все необходимые данные перед выходом.
2. Завершение соединений
Активные соединения с базами данных, кэшем и другими сервисами должны закрываться корректно. Это предотвращает зависание соединений на стороне других сервисов.
3. Завершение запросов
Клиенты ожидают, что их запросы будут обработаны. Graceful shutdown гарантирует, что текущие запросы завершатся нормально, и новые не будут приняты.
4. Микросервисная архитектура
В контейнерах и оркестрации (Kubernetes) сигнал SIGTERM может быть отправлен в любой момент. Правильная обработка этого сигнала критична для надежности системы.
Базовый пример реализации
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
server := &http.Server{
Addr: ":8080",
Handler: http.HandlerFunc(handleRequest),
}
// Канал для получения сигналов ОС
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, syscall.SIGTERM, syscall.SIGINT)
// Запуск сервера в отдельной горутине
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Printf("Ошибка при запуске сервера: %v\n", err)
}
}()
// Ожидание сигнала завершения
<-shutdown
fmt.Println("Получен сигнал завершения, начинаем graceful shutdown...")
// Создаем контекст с timeout для завершения
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Останавливаем сервер
if err := server.Shutdown(ctx); err != nil {
fmt.Printf("Ошибка при завершении сервера: %v\n", err)
}
fmt.Println("Сервер остановлен")
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Привет!")
}
Продвинутый пример с errgroup и context
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"golang.org/x/sync/errgroup"
)
type Server struct {
httpServer *http.Server
eg *errgroup.Group
ctx context.Context
cancel context.CancelFunc
}
func NewServer(addr string) *Server {
ctx, cancel := context.WithCancel(context.Background())
eg, egCtx := errgroup.WithContext(ctx)
return &Server{
httpServer: &http.Server{
Addr: addr,
Handler: http.HandlerFunc(handleRequest),
},
eg: eg,
ctx: egCtx,
cancel: cancel,
}
}
func (s *Server) Start() {
s.eg.Go(func() error {
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return fmt.Errorf("ошибка при запуске сервера: %w", err)
}
return nil
})
}
func (s *Server) WaitForShutdown() error {
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, syscall.SIGTERM, syscall.SIGINT)
s.eg.Go(func() error {
<-shutdown
fmt.Println("Сигнал завершения получен")
s.Shutdown()
return nil
})
return s.eg.Wait()
}
func (s *Server) Shutdown() {
fmt.Println("Начинаем graceful shutdown...")
s.cancel()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := s.httpServer.Shutdown(ctx); err != nil {
fmt.Printf("Ошибка при завершении: %v\n", err)
}
fmt.Println("Все горутины завершены, приложение закрывается")
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.printf(w, "OK")
}
func main() {
server := NewServer(":8080")
server.Start()
if err := server.WaitForShutdown(); err != nil {
fmt.Printf("Ошибка приложения: %v\n", err)
os.Exit(1)
}
}
Лучшие практики
Почему errgroup лучше, чем sync.WaitGroup
errgroup имеет ряд преимуществ перед обычным WaitGroup:
- Встроенный контекст позволяет управлять отменой всех горутин сразу
- Первая ошибка отменяет контекст, и все остальные горутины получают сигнал через ctx.Done()
- Возвращаемая ошибка содержит информацию о том, что пошло не так
- Упрощает обработку ошибок - не нужно вручную отслеживать каждую ошибку в канале
1. Используйте правильный timeout
Timeout должен быть достаточно длинным для нормального завершения, но не бесконечным:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
server.Shutdown(ctx)
2. Обрабатывайте оба сигнала
SIGTERM в Kubernetes и SIGINT в терминале:
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, syscall.SIGTERM, syscall.SIGINT)
3. Логируйте процесс завершения
Это поможет отладке и мониторингу:
fmt.Println("Начинаем graceful shutdown...")
// ... операции завершения
fmt.Println("Graceful shutdown завершен")
4. Используйте errgroup для управления горутинами
Вместо sync.WaitGroup:
eg, ctx := errgroup.WithContext(context.Background())
eg.Go(func() error {
// что-то происходит
return nil
})
if err := eg.Wait(); err != nil {
fmt.Printf("Ошибка: %v\n", err)
}
errgroup автоматически отменяет контекст при первой ошибке, что упрощает graceful shutdown.
5. Проверяйте контекст в долгоживущих операциях
Если операция долгая, проверяйте контекст:
select {
case <-ctx.Done():
return ctx.Err()
case result := <-someChannel:
// обработка
}
Заключение
Graceful shutdown не просто деталь реализации - это критический компонент надежного приложения. В микросервисной архитектуре и контейнерных окружениях правильное завершение приложения напрямую влияет на стабильность всей системы.
Используйте эти паттерны, адаптируя их под свои нужды, и ваши приложения будут завершаться чисто и без потери данных.
gRPC в Go
gRPC - современный RPC-фреймворк от Google, построенный на HTTP/2 и Protocol Buffers. В Go он особенно популярен благодаря скорости, минимальному оверхеду и качественной официальной поддержке.
Как работает gRPC
- Пишешь контракт в .proto-файле (сообщения + сервисы)
- Генерируешь код с помощью protoc + плагинов
- структуры сообщений (.pb.go)
- интерфейс сервера и клиент (.grpc.pb.go)
- Реализуешь интерфейс сервера
- Запускаешь grpc-сервер на net.Listen
- Клиент подключается и вызывает методы как обычные функции
Общение идет по HTTP/2, бинарная сериализация через protobuf, мультиплексирование, deadlines, метаданные, интерсепторы, четыре вида RPC.
Четыре типа RPC в gRPC
- Unary - классический запрос-ответ
- Server streaming - клиент один запрос, сервер шлет поток ответов
- Client streaming - клиент шлет поток, сервер дает один ответ
- Bidirectional streaming - обе стороны шлют сколько угодно и когда угодно
Пример (Unary)
proto/greeter.proto
syntax = "proto3";
package greeter;
option go_package = "github.com/yourname/project/proto;proto";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}
Генерация:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
protoc --go_out=. --go-grpc_out=. proto/greeter.proto
server/main.go:
package main
import (
"context"
"fmt"
"log"
"net"
pb "github.com/yourname/project/proto"
"google.golang.org/grpc"
)
type greeterServer struct {
pb.UnimplementedGreeterServer
}
func (s *greeterServer) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
msg := fmt.Sprintf("Привет, %s!", in.Name)
return &pb.HelloResponse{Message: msg}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("не удалось открыть порт: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &greeterServer{})
log.Printf("gRPC сервер запущен на :50051")
if err := s.Serve(lis); err != nil {
log.Fatalf("ошибка запуска: %v", err)
}
}
client/main.go:
package main
import (
"context"
"log"
"os"
"time"
pb "github.com/yourname/project/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.NewClient(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("не удалось подключиться: %v", err)
}
defer conn.Close()
client := pb.NewGreeterClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
name := "Вася"
if len(os.Args) > 1 {
name = os.Args[1]
}
resp, err := client.SayHello(ctx, &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("ошибка вызова: %v", err)
}
log.Printf("Ответ сервера: %s", resp.Message)
}
Какие есть особенности?
Плюсы
- Высокая производительность (часто в 5-12 раз быстрее JSON+REST)
- HTTP/2 из коробки: мультиплексирование, меньшая задержка
- Строгая типизация и генерация кода - минимум ошибок в контракте
- Четыре вида стриминга - мощный инструмент для реального времени
- Встроенные deadlines, cancellation, metadata
- Интерсепторы для логирования, метрик, авторизации, retry
- Поддержка reflection, TLS, gzip, keepalive
За и против
Недостатки:
- Инструментов меньше, чем у REST (нет нативного swagger)
- Браузеры не понимают без grpc-web или прокси
- Отладка сложнее (помогают grpcurl, evans, bloomrpc)
- Бинарный протокол - не читается глазами
Удобнее использовать для:
- Микросервисы внутри кластера
- Мобильные приложения и бэкенд (экономия трафика и батареи)
- Высоконагруженные стриминговые сервисы
- Системы с жесткими требованиями к задержке
- Полиглотные команды (один .proto - клиенты на многих языках)
Удачи с gRPC - одним из самых удобных и быстрых инструментов для микросервисов😌
Массивы и слайсы: что внутри?
В Go массив и слайс - это принципиально разные типы данных с совершенно разным представлением в памяти.
Массив [N]T
- Это непрерывный блок памяти размером ровно N × sizeof(T)
- Размер N - неотъемлемая часть типа (
[16]byte, [32]byte и тд) - При присваивании, возврате из функции или передаче по значению = полное побайтовое копирование всего блока
- Нулевое значение - массив, все элементы которого имеют нулевое значение типа T
- Никогда не может быть nil
- Размер известен на этапе компиляции и неизменяем
Из-за этого массивы:
- очень дорогие при копировании (особенно большие)
- почти никогда не передаются по значению в реальном коде
- используются только там, где фиксированный размер важен для типа (ключи в map, [4]uint8, [16]byte для SHA-256 и т.п.)
Слайс []T
Слайс - это структура-дескриптор размером 24 байта (на 64-битных системах), состоящая из трёх полей:
- указатель на базовый массив (
*T) - 8 байт - текущая длина (
len) - 8 байт - ёмкость (
cap) - 8 байт
Когда вы создаёте слайс, в памяти происходит примерно следующее:
переменная s (24 байта)
├── указатель ──► [начало данных в куче]
├── len = текущая видимая длина
└── cap = доступная ёмкость от указателя до конца выделенного блока
Основные технические свойства слайсов:
- Присваивание, передача в функцию, возврат из функции - копируется только 24 байта дескриптора
- Сам массив - основа не копируется при таких операциях
- Несколько слайсов могут ссылаться на одну и ту же область памяти (разные указатели, len и cap)
- Может быть nil (все три поля = 0 / nil)
- Может иметь len == 0, но уже подготовленную cap (пустой слайс с зарезервированной памятью)
- Может указывать на произвольную середину другого массива или слайса
- При append, если cap недостаточно - происходит аллокация: новый больший блок - копирование элементов - обновление указателя, len и cap в дескрипторе
- Сравнение через == запрещено компилятором (кроме сравнения с
nil)
Основные технические различия
-
Размер в памяти самой переменной
массив - N × sizeof(T)
слайс - всегда 24 байта -
Что копируется при присваивании / передаче в функцию
массив - весь массив
слайс - только дескриптор (указатель + len + cap) -
Возможность быть nil
массив - нет
слайс - да -
Возможность иметь len == 0 при cap > 0
массив - нет
слайс - да -
Возможность указывать на часть другого массива
массив - нет
слайс - да -
Изменение данных через один слайс видно в другом (если общая область памяти)
массив - нет
слайс - да -
Поддержка динамического роста (
append)
массив - нет
слайс - да (с возможной аллокацией) -
Можно ли использовать как ключ в map
массив - да (если размер фиксирован)
слайс - нет
Если коротко, то:
Массив - это сами данные, которые имеют ограничения по памяти и никогда не меняют ее размер
Слайс - это указатель на какие-то данные, которые могут перемещаться в другую ячейку памяти(при необходимости)
Поэтому в коде редко используются именно массивы, они неудобны и намного «дороже» слайсов. Используйте их только при необходимости, в остальных случаях выбирайте слайс
golang-migrate: почему стоит остановиться именно на нем?
Если вам нужен инструмент миграций, который:
- работает абсолютно одинаково в Go, Python, Node.js, Rust, CI/CD-скриптах
- поддерживает почти все популярные БД (PostgreSQL, MySQL, SQLite)
- максимально предсказуем и прозрачен
- легко встраивается в любые пайплайны деплоя
то golang-migrate - это по-сути лучший выбор.
Почему он так хорош?
Коротко - это SQL-файлы со скриптами. Нет никакой магии, нет сюрпризов и упрощения. Но и нет подводных камней, которые часто скрывают библиотеки
Как это работает?
- Одна миграция - одно логическое изменение
- Всегда создаете файлы .up.sql и .down.sql (даже если не откатываете таблицу)
- Делайте миграции идемпотентными где возможно: IF NOT EXISTS, CREATE OR REPLACE
- Не храните данные в миграциях (кроме случаев, когда они должны быть заполнены статично)
- Называйте файлы последовательно и описательно
20260130123456_users.up.sql - Тестируйте up → down → up локально перед выкаткой в прод
Пример миграции (Postgres)
-- 20260130123456_create_users.up.sql
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_users_email ON users USING btree (email);
-- 20260130123456_create_users.down.sql
DROP TABLE IF EXISTS users CASCADE;UPDATE users SET email = old_email;
ALTER TABLE users
ALTER COLUMN email SET NOT NULL,
ADD CONSTRAINT users_email_unique UNIQUE (email);(В down-миграции — обратный процесс)
Как интегрировать в проект
Можно через Makefile / CI
migrate:
migrate -path ./migrations -database "${DATABASE_URL}" up
Это будет самы лучший вариант, в ci/cd можно добавить вызов этой этого скрипта перед сборкой приложения
В каких случаях точно стоит использовать
- Микросервисы на разных языках
- Строгий GitOps / CI/CD (ArgoCD, GitHub Actions, etc.)
- Нужно поддерживать много разных БД в компании
- Важна максимальная предсказуемость и минимум магии
Личный опыт
На мой взгляд это самый лучший вариант миграций, он более "долгий" в процессе разработки новых реп, но в конечном итоге - более предсказуемый, а также более ультимативный. Вы можете добавить туда любой sql без костылей, что упростит вам развитие сервиса в дальнейшем, и я почти уверен, что вам никогда не придется его переписывать на библиотеку и всякие "авто-миграторы" из структур. Что может быть лучше?)
Ну и еще одно преимущество, которое можно упустить.- низкий порог входа. Любой бекенд-разработчик знает sql, и легко напишет такую миграцию. А вот со всякими либами нужно порой разбираться отдельно…
Мониторинг в Go: когда ваш сервис прилег
Сегодня поговорим о способах мониторинга состояния в Go-приложениях: метриках и логах.
Чем отличаются метрики и логи?
Логи — это детальные записи о событиях в приложении, хронология происходящего.
Метрики — числовые показатели, которые можно агрегировать и анализировать.
Логирование с zap - быстрее стандартного log. Uber создал zap не просто так. Стандартный log в Go медленный из-за кучи аллокаций памяти.
Проблема стандартного логгера:
log.Printf("User %s failed to login from IP %s", username, ip)
// Каждый вызов создаёт строку в heap -> нагрузка на GC
Решение с zap:
import "go.uber.org/zap"
func main() {
// Production-конфиг: JSON + быстрая работа
logger, _ := zap.NewProduction()
defer logger.Sync()
// Структурированное логирование
logger.Info("User login failed",
zap.String("username", username),
zap.String("ip", ip),
zap.Int("attempt", 3),
zap.Duration("response_time", 105*time.Millisecond),
)
// Sugar-синтаксис для удобства
sugar := logger.Sugar()
sugar.Infof("User %s failed to login from %s", username, ip)
}
Что это даёт?
· В 10+ раз быстрее стандартного логгера
· Структурированный вывод (JSON)
· Контекстные поля вместо склеивания строк
· Разные уровни логирования (Debug, Info, Warn, Error)
Но зачем же тогда нужны метрики?
Prometheus стал стандартом для сбора метрик в Kubernetes-мире.
Типичные проблемы, когда нет метрик:
· "Почему сервис медленный?" — непонятно
· "Сколько ошибок в час?" — нужно парсить логи
· "Когда масштабировать?" — гадание на кофейной гуще
Решение с prometheus:
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
// Counter — только увеличивается (запросы, ошибки)
requestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "path", "status"},
)
// Histogram — распределение значений (время ответа)
responseTime = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_response_time_seconds",
Help: "HTTP response time distribution",
Buckets: prometheus.DefBuckets, // предопределённые корзины
},
[]string{"method", "path"},
)
// Gauge — может увеличиваться и уменьшаться (текущие соединения)
activeConnections = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "active_connections",
Help: "Number of active connections",
},
)
)
func init() {
prometheus.MustRegister(requestsTotal, responseTime, activeConnections)
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Логика обработки запроса
// ...
// Запись метрик
requestsTotal.WithLabelValues(r.Method, r.URL.Path, "200").Inc()
responseTime.WithLabelValues(r.Method, r.URL.Path).Observe(time.Since(start).Seconds())
activeConnections.Inc()
defer activeConnections.Dec()
}
Экспорт метрик:
// Отдельный эндпоинт для Prometheus (нужно запустить в отдельной горутине)
http.Handle("/metrics", promhttp.Handler())
go http.ListenAndServe(":2112", nil)
// Теперь Prometheus может скрапировать метрики с localhost:2112/metrics
Чистая архитектура и Hexagonal: Как избежать отложенного выстрела в ногу
Если вы когда-нибудь работали над проектом, который начинался как простой сервис, а через год превратился в монолитный спагетти-код, где бизнес-логика перемешана с HTTP-обработчиками, базой данных и внешними API - эта статья для вас.
Зачем это всё нужно?
Чтобы не было ошибки импортов конечно же
Чтобы все работало отдельно друг от друга, легко читалось, и менялось. Вот представь себе пк, ты же можешь поменять монитор на другой? С архитектурой должно быть также, ты должен быстро менять что-то "налету" и от этого не должен падать весь проект мгновенно. Я конечно же говорю про такие вещи как база данных(поменяли mongo на postgres), подключение(перекатились на grpc с http) и так далее
Эти подходы решают фундаментальные проблемы:
- Тесная связанность - бизнес-логика зависит от фреймворков, баз данных, UI.
- Сложность тестирования - нельзя протестировать логику без запуска всей инфраструктуры.
- Трудности с изменениями - поменять базу данных или добавить новый интерфейс (gRPC вместо HTTP) требует рефакторинга всего кода.
- Размытая доменная модель - разработчики не понимают бизнес, код не отражает реальные сущности предметной области.
Результат: проект становится кашей(не будем говорить какой именно😁)
Какие проблемы решают эти архитектуры?
-
Независимость от внешнего мира
Бизнес-логика не знает о Gin, Echo, PostgreSQL или RabbitMQ. Можно сменить любой внешний компонент без боли. -
Чёткое разделение ответственности
Каждый слой знает только о том, что ему положено. -
Лёгкое тестирование
Use cases и доменные сущности тестируются в изоляции - чистые unit-тесты без моков баз данных. -
Быстрый onboarding
Новичок сразу видит, где лежит бизнес-логика, а где - инфраструктура.
Можно реализовать такую структуру:
internal/
├── application/ // можно хранить здесь dto, actions
├── transport/ // HTTP handlers, gRPC
└── repository/ // DB connections, external clients, config
Тут я бы хотел отметить роль dto, в каждом слое лучше добавлять отдельную сущность, которая будет ориентироваться на dto приложения, но при этом дополняться нужными тегами при необходимости. Dto самого приложения не должна быть связана с другими слоями и иметь тегов. Все другие слои будут конвертировать свою dto в application dto, чтобы работать с бизнес-логикой
Наглядный пример: Система заказов в интернет-магазине
Допустим, у нас есть сущность Order (заказ).
1. Application слой
// internal/domain/order.go
type Order struct {
id string
customerID string
items []OrderItem
status OrderStatus
total Money
}
type OrderStatus string
const (
StatusNew OrderStatus = "new"
StatusConfirmed OrderStatus = "confirmed"
StatusShipped OrderStatus = "shipped"
StatusCancelled OrderStatus = "cancelled"
)
func (o *Order) CanConfirm() bool {
return o.status == StatusNew
}
func (o *Order) Confirm() error {
if !o.CanConfirm() {
return errors.New("cannot confirm order in current status")
}
o.status = StatusConfirmed
return nil
}
Здесь вся бизнес-логика внутри слоя приложеня. Никаких зависимостей от баз данных.
2. Интерфейс репозиторя
// internal/domain/order_repository.go
type OrderRepository interface {
FindByID(ctx context.Context, id string) (*Order, error)
Save(ctx context.Context, order *Order) error
}
3. Action
// internal/application/action/confirm_order/confirm_order.go
type ConfirmOrder struct {
orderRepo OrderRepository
}
func NewConfirmOrder(orderRepo OrderRepository) *ConfirmOrder {
return &ConfirmOrder{orderRepo: orderRepo}
}
func (a *ConfirmOrder) Execute(ctx context.Context, orderID string) error {
order, err := a.orderRepo.FindByID(ctx, orderID)
if err != nil {
return err
}
if err := order.Confirm(); err != nil {
return err
}
return a.orderRepo.Save(ctx, order)
}
Action знает только о домене и какие будут методы репозитория. Никакого HTTP, никакого SQL.
4. Транспортный слой
// internal/transport/http/handler.go
type ConfirmOrderAction interface {
Execute(ctx context.Context, orderID string) error
}
type Handler struct {
confirmOrder ConfirmOrderAction
}
func (h *Handler) ConfirmOrderHandler(w http.ResponseWriter, r *http.Request) {
orderID := chi.URLParam(r, "id")
err := h.confirmOrder.Execute(r.Context(), orderID)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
}
5. Реализация репозитория (инфраструктура)
// internal/repository/postgres/order/order_repo.go
type PostgresOrderRepository struct {
db *sql.DB
}
func (r *PostgresOrderRepository) FindByID(ctx context.Context, id string) (*domain.Order, error) {
// SQL-запросы...
}
func (r *PostgresOrderRepository) Save(ctx context.Context, order *domain.Order) error {
// INSERT/UPDATE...
}
Если завтра захотим перейти на MongoDB — меняем только эту реализацию.
Почему стоит использовать именно в Go?
Go поощряет простоту и явные зависимости. Пакеты легко контролировать, интерфейсы минималистичны
Плюс сообщество активно продвигает эти подходы: посмотрите на проекты вроде go-clean-arch или реальные open-source сервисы (например, в области fintech).
Также, не забывайте про использование интерфейсов, это быстро, и может сэкономить время в дальнейшем, ну и из них можно генерить моки, что довольно удобно:)
Паттерны конкурентности в Go
Golang был создан с нуля для эпохи многоядерных процессоров и распределённых систем. Его слоган «Do not communicate by sharing memory; instead, share memory by communicating» идеально отражает философию языка
Паттерны конкурентности в Go не просто «возможны» - они встроены в язык. Это делает параллельный код в Go:
- Простым и читаемым
- Масштабируемым (десятки тысяч горутин без overhead’а)
- Производительным (минимальные контекстные переключения)
Примеры паттернов
1. Producer-Consumer (через каналы)
Каналы - это по-сути безопасные очереди
package main
import (
"fmt"
"time"
)
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
fmt.Printf("Произвёл: %d\n", i)
ch <- i
time.Sleep(time.Millisecond * 300)
}
close(ch) // Не забудь закрыть:)
}
func consumer(ch <-chan int) {
for val := range ch {
fmt.Printf("Потребил: %d\n", val)
time.Sleep(time.Millisecond * 500)
}
}
func main() {
ch := make(chan int, 3) // Буферизированный канал
go producer(ch)
go consumer(ch)
time.Sleep(time.Second * 3)
fmt.Println("Готово")
}
Канал автоматически синхронизирует доступ: отправка блокируется при полном буфере, получение - при пустом.
2. Worker Pool
Определенное число горутин обрабатывают задачи из очереди
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Воркер %d обработал задачу %d\n", id, job)
// Симулируем работу
// time.Sleep(time.Second)
}
}
func main() {
const numJobs = 10
const numWorkers = 3
jobs := make(chan int, numJobs)
var wg sync.WaitGroup
// Запускаем воркеров
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, &wg)
}
// Отправляем задачи
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
wg.Wait()
fmt.Println("Все задачи выполнены")
}
WaitGroup гарантирует, что main дождётся завершения всех воркеров.
3. Pipeline
Данные проходят через цепочку горутин, каждая выполняет свою стадию обработки.
package main
import "fmt"
func generator(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
func main() {
in := generator(1, 2, 3, 4, 5)
out := square(in)
for result := range out {
fmt.Println(result) // 1, 4, 9, 16, 25
}
}
Легко масштабировать и добавлять этапы для параллельной обработки.
4. Проверяем ctx
Важно помнить про ctx, так как параллельные операции зачастую применяются в загруженных участках кода, и их лучше завершать при необходимости. Например, клиент дернул http-ручку, но решил отменить запрос. Нет смысла производить дальнейшую обработку. В таком случае нам нужно завершить выполнение всех горутин, и не тратить ресурсы впустую
Ну или если у нас есть определенный timeout для выполнения кода. Например, мы знаем, что обработка не может превышать час, то мы можем добавить context.WithTimeout, чтобы воркер просто завершился при неожиданно появившемся баге, и не простаивал бесконечно
package main
import (
"context"
"fmt"
"time"
)
func longTask(ctx context.Context) error {
select {
case <-time.After(2 * time.Second):
fmt.Println("Задача завершена")
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := longTask(ctx); err != nil {
fmt.Println("Задача отменена:", err)
}
}
Инструменты для конкурентности в go мне очень нравятся. Они производительные, просто читаются и удобные в использовании. Но тут просто нужно быть осторожным, можно жестко запороть все из-за банальной ошибки, поэтому нужно смотреть в оба👀(Но опять же, это не проблема go, это просто зачастую связанно с большим кол-вом логики во всех этих обработках🫠)
Подводные камни миграции на Go 1.26 ⚠️
В прошлом посте я рассказывал, почему стоит обновиться до Go 1.26 - новые фичи, производительность, удобства. Да, в go 1.26 есть несколько интересных фич, но давайте рассмотрим подводные камни для данной миграции, чтобы не уронить проект такой доработкой
Хорошая новость: breaking changes минимальны. Go команда традиционно избегает радикальных ломок. Плохая - в 1.26 есть несколько изменений, которые могут уронить ваш проект в редких случаях. Давайте разберём основные подводные камни и как их обойти.
1. Новый дефолтный go в go.mod при go mod init
Что изменилось?
go mod init теперь ставит более консервативную версию Go в новый go.mod:
- При использовании Go 1.26 - дефолт go 1.25.0
- В RC - даже go 1.24.0
Проблема:
Если создаёте новый модуль, он не будет сразу использовать фичи Go 1.26 (например, new(expr)).
Решение:
Сразу после init обновите версию:
go get go@1.26
# или вручную в go.mod: go 1.26
go mod tidy
2. Удалённые инструменты: cmd/doc и go tool doc
Что случилось?
Эти устаревшие команды полностью удалены.
Проблема:
Если в скриптах или Makefile используете go tool doc - сломается.
Решение:
Перейдите на go doc - функционал тот же:
go doc ./... # вместо go tool doc
3. Новый GC (Green Tea) и специализированный allocator по умолчанию
Что нового?
- Green Tea GC включён всегда (был экспериментальным).
- Small allocations (<512B) - до 30% быстрее.
- Больше объектов на стеке.
Проблема:
В редких случаях (allocation-heavy код, unsafe, race detector) может измениться поведение или производительность. Иногда регресс ~1-5%.
Решение:
Если заметили проблемы - временно отключите:
GOEXPERIMENT=nogreenteagc,nosizespecializedmalloc go run ./...Но учтите: эти флаги удалят в Go 1.27 - лучше просто поправьте сразу.
Для диагностики stack allocations:
go build -gcflags=all=-d=variablemakehash=0
4. Строгий парсинг URL в net/url.Parse
Что изменилось?
Теперь отвергает хосты с несколькими : (типа localhost:8080:80).
Проблема:
Если ваш код парсит "креативные" URL - может начать падать.
Решение:
Временно верните старое поведение:
GODEBUG=urlstrictcolons=0 ./your-binaryНо лучше почистить код/входные данные.
5. Изменения в crypto: игнор custom rand.Reader
Что изменилось?
Функции вроде ecdsa.GenerateKey игнорируют переданный rand.Reader - всегда используют системный secure random.
Проблема:
Если в тестах передавали детерминированный reader - теперь ключи будут случайными.
Решение:
Используйте новый testing/cryptotest.SetGlobalRandom для детерминированных тестов.
6. go fix стал агрессивнее
Теперь использует современные analyzers и может переписать больше кода (например, заменить циклы на slices.Contains).
Проблема:
Может изменить стиль кода неожиданно.
Решение:
Запускайте с флагами и ревьюьте изменения:
go fix -forvar ./... # пример selective
Как по мне, лучше не торопиться с обновлением и подождать какое-то время. За это время уже какое-то кол-во проектов пройдет процесс миграции, и можно будет подхватить "костыли"/фиксы налету и интегрировать в свой проект, чтобы отделаться "малой кровью"
Что происходит с Go прямо сейчас (январь 2026)
На 17 января 2026 Go 1.26 ещё официально не вышел — релиз ожидается в феврале. Draft release notes уже висят на go1.26, и там всё по-честному описано.
Что интересного в 1.26 (по draft)
- Green Tea GC становится дефолтным сборщиком мусора. Это эксперимент из 1.25 (GOEXPERIMENT=greenteagc), который сильно улучшает производительность: лучше работает с кэшем процессора, меньше пауз, на AVX-512 даже векторное ускорение добавили. Для типичных нагрузок GC-CPU падает на 10–40%. Если приложение много мусорит — заметно полегчает без изменений в коде.
- Новый пакет crypto/hpke — гибридное шифрование (RFC 9180), включая post-quantum варианты. Полезно для крипто-штук.
- Экспериментальный simd/archsimd (GOEXPERIMENT=simd) — прямой доступ к SIMD-инструкциям.
- runtime/secret (эксперимент) — для безопасного стирания временных секретов в памяти.
- Улучшения в new: теперь можно писать new(expression) с инициализацией.
- go fix теперь умнее — применяет современные идиомы через анализаторы (старые фиксеры убрали).
- Ещё куча мелочей: рандомизация heap, быстрее cgo в простых случаях, профили утечек goroutine стали лучше.
В общем, февральский апдейт выглядит солидно — особенно GC и безопасность.
А что уже вышло в январе
15 января вышли security-фиксы:
- go1.25.6 и go1.24.12
Закрыли дырки в archive/zip (memory exhaustion), crypto/tls, net/http, net/url — в основном DoS через кривые входные данные. Если у тебя публичный сервис — лучше обновиться.
(Есть источники, которые пишут, будто 1.26 уже вышел 16 января, но официально на go.dev это всё ещё draft + февраль. Возможно, путаница с патчами.)
Тренды вокруг Go
Go продолжает расти: топ-7 в TIOBE, миллионы разработчиков, компании вроде Google/Uber/Netflix/Cloudflare на нём.
В 2026 он остаётся сильным выбором для backend, микросервисов, K8s, инфраструктуры — concurrency из коробки, быстрый билд, мало памяти.
Сравнивают с Rust (Go проще и быстрее писать) и Python (Go быстрее в проде). Появляется всё больше нативных GPU/графики-библиотек в чистом Go — может, через пару лет GUI на нём станет реальнее.
Если сидишь на старой версии — можно уже потыкать tip.golang.org или включить Green Tea в 1.25, чтобы прикинуть, как полетит.
А ты на какой версии сейчас? Пробовал Green Tea?
Context и graceful shutdown в Go
Как не терять запросы и не плодить утечки горутин
Как не стоит завершать приложение:
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
server.Close() // ← все запросы в полёте умирают
Последствия:
- клиенты получают 499 / EOF / таймаут
- горутины висят или паникуют
- в Kubernetes под застревает в TERM → SIGKILL через grace period
Современный подход (Go 1.16+)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
srv := &http.Server{
Addr: ":8080",
Handler: router,
BaseContext: func(net.Listener) context.Context {
return ctx // все новые запросы получают контекст, который отменится при SIGTERM
},
}
// Запускаем сервер в отдельной горутине
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Printf("HTTP server error: %v", err)
}
}()
// Ждём сигнал
<-ctx.Done()
log.Println("Shutdown signal received")
// Даём время на завершение запросов (25–30 секунд — типичный таймаут)
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("Graceful shutdown error: %v", err)
// Опционально: srv.Close() для жёсткого завершения
}
log.Println("Server stopped gracefully")
}
Ключевые моменты, которые спасают от утечек
-
Передавайте
ctxво все длительные операции- БД: QueryContext, ExecContext
- Redis: Do(ctx, cmd)
- gRPC: ctx в каждом вызове
- Свои горутины: select { case <-ctx.Done(): return }
-
Readiness / Liveness в Kubernetes
Как только получили сигнал → /readyz → 503 → kube перестаёт слать трафик. -
Фоновые воркеры
Используйте sync.WaitGroup + общий ctx:
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case job := <-queue:
process(job)
}
}
}()
}
// В shutdown: после srv.Shutdown() делаем wg.Wait()
Что стоит учитывать
- signal.NotifyContext → глобальный appCtx
- BaseContext в http.Server
- srv.Shutdown(with timeout)
- Все длительные операции обязательно используют context
- sync.WaitGroup / errgroup для фоновых воркеров
Будте здоровы и не забывайте про контекст и длительные операции🙃
Почему в 2026 я выбираю pgx вместо GORM
GORM — можно быстро создавать модели, associations, миграции — и начинать работу
Но стоит проекту подрасти (миллионы строк, высокая нагрузка, сложные запросы, PostgreSQL-фичи на полную) — начинается цирк: тормоза, сюрпризы, «что здесь вообще происходит?».
А pgx в 2026 — это уже другой экспириенс. Вот почему он побеждает:
-
Быстрее на 30–50%
Особенно bulk-вставки, батчи, высокая concurrency. Бенчмарки 2025–2026 это стабильно подтверждают. -
Нативно поддерживает всё, что умеет PostgreSQL
arrays, JSONB, range, hstore, геометрия, векторы, LISTEN/NOTIFY, супербыстрый COPY — без костылей и custom Valuer/Scanner.
В GORM либо теряешь половину мощи базы, либо пишешь тонну хардкода, чтобы стабилизировать работу. -
Ты видишь настоящий SQL
Никакой магии рефлексии, preload’ов, которые иногда грузят лишний гигабайт, хуков, которые срабатывают не там.
Написал → знаешь, что будет выполнено базой. DBA в восторге, slow query логи читаются без боли. -
Меньше багов и техдолга
Реальные истории найденные мной в 2025–2026:- «Перешли с GORM на pgx + sqlc → CPU на БД-слой упал на 40%»
- «GORM задыхался на 10+ млн строк и сложных join’ах — pgx дышит свободно»
- «Перестали ловить странные откаты транзакций и preload-косяки»
-
Современный Go-стек 2026 года
pgx — это уже почти дефолт в серьёзных проектах.
GORM остался в основном для:- быстрого MVP
- админок
- небольших проектов с низкой нагрузкой
-
Меньше overhead’а
pgx — чистый драйвер + удобный toolkit.
GORM тянет рефлексию, плагины, скрытые аллокации → binary тяжелее, cold-start хуже, потребление памяти больше.
Когда GORM всё ещё ок (даже в 2026)
- Нужно за вечер сделать прототип / side-project
- Команда обожает ActiveRecord-подобный синтаксис и боится писать SQL руками
- Проект маленький и точно не вырастет в монстра
Я решил выбрать pgx, все-таки лучше прозрачно видеть запрос, который будет выполняться. А какой ORM выбираешь ты?:)
Горутины vs Потоки: В чём разница?
Если вы давно занимаетесь разработкой, то наверняка слышали про горутины. А потоки - это классика из других языков. Давайте разберёмся, чем они отличаются на простых примерах.
1. Что это?
- Потоки: Это как отдельные вкладки в браузере. Каждый поток - это независимая единица, которая выполняет код. ОС сама решает, когда переключаться между ними. Они используются в языках вроде Java, C++ или Python (с модулем threading).
- Горутины: Это как потоки, но только реализуемые внутри языка Go. Они созданы, чтобы упростить многопоточность. Горутины запускаются ключевым словом
go, и их можно запускать в огромных количествах.
2. Сравнение потребления ресурсов
- Потоки: Каждый поток требует много памяти (обычно 1-8 МБ на стек) и времени на запуск. Если запустить 1000 потоков, система может замедлиться или даже упасть - ОС не любит, когда их слишком много.
- Горутины: Каждая горутина берёт всего 2-4 КБ памяти. Вы можете запустить тысячи или даже миллионы - Go-runtime (внутренняя система Go) справится с этим быстро.
3. Как это работает?
- Потоки: Каждый поток работает отдельно от других. ОС планирует их на процессорные ядра. Переключение между потоками - это как остановить грузовик и запустить другой: медленно и энергозатратно.
- Горутины: Они "мультиплексируются" на меньшее количество реальных потоков. Go-рантайм сам распределяет горутины по потокам (обычно по одному на ядро процессора). Переключение - лёгкое: быстро и без лишних усилий. Если горутина ждёт (например, сетевой запрос), рантайм сразу даёт работу другой.
Пример кода с горутинами
Чтобы наглядно увидеть, вот простой пример на Go. Мы запустим две горутины: одна печатает "Hello", вторая - "World". Они будут работать параллельно.
package main
import (
"fmt"
"time"
)
func sayHello() {
for i := 0; i < 5; i++ {
fmt.Println("Hello")
time.Sleep(100 * time.Millisecond) // Задержка, чтобы увидеть параллельность
}
}
func sayWorld() {
for i := 0; i < 5; i++ {
fmt.Println("World")
time.Sleep(100 * time.Millisecond) // Вторая задержка, чтобы увидеть параллельность
}
}
func main() {
go sayHello() // Запускаем горутину для sayHello
go sayWorld() // Запускаем горутину для sayWorld
// Ждём, чтобы горутины завершились (в реальном коде используйте sync.WaitGroup)
time.Sleep(1 * time.Second)
fmt.Println("Done!")
}
Что здесь происходит?
- В
mainмы запускаем две горутины с помощьюgo. Они стартуют параллельно основному потоку. - Каждая печатает слово 5 раз с задержкой.
- Без
time.Sleepв main программа могла бы завершиться раньше. При написании реальной программы используй -sync.WaitGroup.
Если запустишь, увидишь перемешанный вывод: Hello, World, Hello... Это и есть параллельность!
Подведем итоги, горутины лишь эмулируют потоки, но не являются ими технически(в рамках потоков ОС)
Буду рад, если вы поделитесь опытом использования горутин, какие возникали с ними проблемы или сложности. Любая информация, которая поможет предостеречь новичков от огромных проблем🙂