> Почему программа падает при работе с указателем на структуру и как это связано с арифметикой указателей в Go (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Лига Цифровой Экономики

Стек: Go

> Пример ответа

Короткий ответ

Падение при работе с указателем на структуру в Go чаще всего вызвано разыменованием nil-указателя или обращением к памяти за пределами выделенного слайса через арифметику указателей. В Go арифметика указателей запрещена на уровне языка - вы не можете делать ptr++ или ptr + n, как в C. Любая попытка обойти это через unsafe.Pointer и uintptr приводит к неопределённому поведению, включая segmentation fault, если адрес выходит за границы аллоцированной памяти.

Подробное объяснение

В Go указатели - это типизированные ссылки на память, а не произвольные адреса. Язык гарантирует безопасность при обычной работе с ними: компилятор и runtime проверяют границы при доступе к слайсам, массивам и полям структур. Однако unsafe.Pointer позволяет преобразовать указатель в uintptr и выполнить арифметические операции. Это опасно, потому что:

  • uintptr - это просто целое число, GC не знает, что оно ссылается на объект. Если объект переместится (Go использует moving GC), адрес станет невалидным.
  • Арифметика может вывести вас за пределы аллоцированного блока. Например, если структура занимает 32 байта, а вы добавляете 40 - вы читаете чужую память.
  • Приведение uintptr обратно к указателю не проверяет валидность адреса. Runtime не может перехватить такую ошибку, поэтому процесс падает с SIGSEGV.

Связь с арифметикой указателей прямая: в Go нет синтаксиса для ptr++, но через unsafe вы можете эмулировать C-стиль. Именно эта эмуляция и приводит к падениям, когда вы ошибаетесь в размере шага или границах.

На практике

В реальном backend-коде на Go вы почти никогда не используете unsafe. Падения с указателями обычно возникают из-за:

  • Разделяемого указателя между горутинами без синхронизации - data race приводит к чтению частично записанной структуры.
  • Хранения указателя на локальную переменную, которая вышла из области видимости - Go компилятор обычно поднимает её в heap, но если вы передаёте адрес в unsafe контекст, гарантии исчезают.
  • Неправильного использования cgo - когда C-код возвращает указатель, а вы пытаетесь работать с ним как с Go-структурой.

Если вы видите падение в проде, первым делом проверьте: не используется ли unsafe рядом с местом падения. Если нет - ищите data race через go test -race или некорректную работу со слайсами (например, slice[0] на пустом слайсе).

Пример кода

GO
package main
import (
"fmt"
"unsafe"
)
type Point struct {
X int
Y int
}
func main() {
// Безопасный путь - обычный указатель
p := &Point{X: 1, Y: 2}
fmt.Println(p.X, p.Y)
// Опасный путь - арифметика через unsafe
ptr := unsafe.Pointer(p)
// Смещаемся на 8 байт (размер int на 64-bit) - это поле Y
yPtr := (*int)(unsafe.Pointer(uintptr(ptr) + unsafe.Offsetof(p.Y)))
fmt.Println(*yPtr)
// Падение: выходим за границы структуры
badPtr := (*int)(unsafe.Pointer(uintptr(ptr) + 1000))
fmt.Println(*badPtr) // SIGSEGV
}

Как отвечать на собеседовании

Начните с того, что в Go арифметика указателей запрещена на уровне языка - это ключевое отличие от C/C++. Затем объясните, что падение возникает при использовании unsafe.Pointer и uintptr, когда вы выходите за границы аллоцированной памяти. Упомяните, что runtime не может проверить такие адреса, поэтому процесс падает сразу. Приведите пример с unsafe.Offsetof для доступа к полям - это легальный, но редкий паттерн. Подчеркните, что в обычном коде падения с указателями - это почти всегда data race или nil-указатель, а не арифметика.

Что проверяет интервьюер

Интервьюер оценивает:

  • Понимаете ли вы разницу между unsafe.Pointer и uintptr - это разные вещи, и uintptr не является указателем для GC.
  • Знаете ли вы, что Go runtime не защищает от произвольных адресов, полученных через unsafe.
  • Умеете ли вы диагностировать падение: сначала смотрите на стектрейс, ищите unsafe или cgo рядом.
  • Понимаете ли вы, когда арифметика указателей оправдана (например, в низкоуровневых сетевых протоколах или при работе с бинарными форматами) и как её безопасно ограничить.

Типичные ошибки

  • Путать unsafe.Pointer и uintptr - считать, что uintptr - это указатель. Это не так, это просто целое.
  • Думать, что Go проверяет границы при работе с unsafe - не проверяет, вы сами отвечаете за корректность.
  • Использовать unsafe для оптимизации без профилирования - в 99% случаев это не нужно и только добавляет риск падений.
  • Забывать, что moving GC может переместить объект, и uintptr станет невалидным - поэтому нельзя хранить uintptr долго.
  • Пытаться сделать ptr++ в Go - синтаксически невозможно, но через unsafe можно, и это почти всегда ошибка.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

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