Черная магия unsafe в Go: практические примеры и ошибки использования. Часть 2
- пятница, 20 марта 2026 г. в 00:00:12
Привет, Хабр! Я — Владимир Балун, и это — вторая часть материала о пакете с отпугивающим названием «unsafe» в Go и том, чем он может быть реально полезен. В первой части мы рассмотрели его содержимое, особенности и нюансы, оптимизации кода с использованием unsafe — все это вы можете освежить в памяти по ссылке.
Сегодня перейдем непосредственно к той самой «магии»: трюки, хаки, советы и лучшие практики с моей стороны.

Важно понять: я не говорю, что вы должны делать именно так на работе. Это просто некоторые возможности, которые вы можете осуществлять с использованием пакета unsafe, например в личных проектах или из целей подробного изучения языка.
Пакет unsafe помогает обходить систему безопасности типов — следовательно, можно тип данных float скастить, например, в целочисленный тип данных.

Я понимаю — это как открывать консервную банку ложкой, но так или иначе, работать это будет.
Возникает вопрос — зачем такое может понадобиться? В Go нет unions, и, если вы захотите сделать union, например, вы можете здесь использовать пакет unsafe.

Вы представляете некоторый участок памяти, когда вам нужен, например, float’ом. А когда потом потребуется другой тип данных, вы представляете этот участок памяти, например, int’ом либо другим типом данных. Но когда вы так делаете, преобразуя один тип в другой, нужно быть очень осторожным.

Если вы создали маленький тип данных, и потом относительно него хотите скаститься к большому, вы захватываете больший участок памяти, а какая-то часть памяти уже вам не принадлежит. Там могут находиться другие данные. Это привет к undefined behavior, известному многим C и C++ разработчикам — ваш код ведет себя абсолютно не так, как вы предполагали.
Пакет называется unsafe — кажется, анализировать структуры данных нужно небезопасно. Но давайте предметнее.

Когда у меня есть какая-то реализация, например, map (здесь, правда, старая реализация), я хочу понять, как она работает. Мне просто интересно, как что-то устроено внутри. Мы можем представить структуру данных map или другого типа данных, и просто с использованием пакета unsafe обойти систему безопасности типов.
В продакшене так точно не стоит делать. Это нужно, когда вы хотите понять, что происходит с map при вставке или при удалении. Вы пишете такой код, потом используете map, вставляете, удаляете элементы и смотрите, что происходит с вашей структурой данных.
Это простой способ реверсинга для вашего хобби, когда вы хотите разобраться с тем, как что-то внутри устроено.
Это уже абсолютно небезопасные вещи в безопасном языке программирования. Но все-таки мы живем в рамках реального мира с некоторыми границами.
Представим, что мы живем в рамках ограничения — временного, legacy, ресурсов или чего-то еще. Если нам нужно раз на миллион получить доступ к приватным полям, что мы делаем?

При помощи рефлексии получаем unsafe.Pointer по некоторому имени поля. Дальше мы уже знаем, что с ним нужно делать. У нас есть unsafe.Pointer, некоторый участок памяти. То, что мы делаем с участком памяти, зависит полностью от нас.
Строки в Go неизменяемые, и менять их, конечно же, не стоит. Но если вдруг захочется, есть способы это сделать.

Например, у меня есть строковый литерал. Я его преобразовываю в срез байтов, затем с использованием известной нам функции получаю адрес начала среза и конструирую строку, которой передаю указатель на начало массива и длину. Выходит, что строка и срез разделяют общий участок памяти, и при изменении значения внутри среза по какому-то индексу оно будет изменено у строки.
Здесь многие Go-разработчики могут обрадоваться — я научился менять строки, побегу и в сложных своих алгоритмах буду менять строки in place! Все будет хорошо, возможно, у меня будет эффективнее код.
Но есть нюансы. Предположим, я напишу такой код:

Здесь есть строковый литерал. Я получаю адрес начала массива, который стоит за ним, делаю преобразование в срез, когда строка и срез шарят один и тот же участок памяти, и с использованием среза пытаюсь изменить какой-то символ в строке, я получаю ошибку.
В разных операционных системах вы будете получать абсолютно разное поведение. Одни операционные системы вам скажут: «Undefined behavior», и может случиться что-то плохое. А некоторые ОС бьют по рукам — как здесь я получаю критическую ошибку.
Разница между первым и вторым кодом заключается в том, что я использую строковый литерал. Посмотрим, что получится с ним.

Я создал две строки Hello World. Они одинаковые, только первая строка представлена как строковый литерал, а вторая — как константная строка. Если посмотреть их адреса, они идентичные. В Go используется интернирование константных строк. То есть по факту получается, когда я пишу строковый литерал, он у меня уже является константной строкой. Как правило, в большинстве языков программирования константные строки хранятся в read-only text segment.
Как подтвердить это? Возьмем функцию. С ней сопряжен адрес первой инструкции, относительно которой она начнет исполняться. Адреса у них очень близки, поэтому велика вероятность, что эта строка была проаллоцирована в read-only text segment.
Повторюсь, модификация read-only text segment в некоторых ОС — это undefined behavior, а в некоторых операционных системах просто убивает ваш процесс, который исполнялся.
Итак, с трюками и хаками закончили. И конечно, я не могу оставить этот материал без советов и лучших практик — все-таки, не зря я много писал на C/C++.
Как лучше использовать unsafe в ваших проектах?
Изолируйте код, использующий пакет unsafe
Выделите для него отдельное место, скройте его за понятными абстракциями в виде понятных функций. Кто-то может возразить: «Я хочу оптимизировать код, какая речь может идти о вызове какой-то функции, это же дополнительный расход?». Но в Go есть inlining функции. Там, где inlining не работает, используйте пакет unsafe явно.
Используйте инструмент
Я показывал инструменты, которые позволяют избежать некоторых проблем, которые вы как программист можете допустить. Все мы периодически садим баги, это нормально.
Разберитесь с пакетом unsafe перед использованием
Советую понять его нюансы и тонкости, хорошо разобраться с арифметикой указателей — это тонкий лед. Шаг влево, шаг вправо — можно очень сильно провалиться, и непонятно, что будет на глубине.
Следите за изменениями в новых версиях Go
Пакет unsafe тоже развивается. С версий Go 1.17, Go 1.20 появлялись новые функции, которые постепенно упрощали взаимодействие с пакетом unsafe. Более того, если вы закладываетесь на внутреннее устройство той или иной структуры данных рантайма, как я показывал выше, вы обязательно должны следить за изменениями в версиях Go. Если что-то изменится в следующей, вы обязаны это изменить у себя, иначе при обновлении вашего проекта до новой версии все будет работать не так, как вы ожидали.
Используйте пакет unsafe только при крайней необходимости
Это самый главный совет. Не старайтесь писать код с использованием пакета unsafe ровно до тех пор, пока у вас не возникнет четкая необходимость — когда вы понимаете, что в рамках какого-то участка кода вам в действительности нужно что-то оптимизировать и выжать там соки. Потому что в большинстве случаев читаемый и понятный код будет лучшим решением, чем оптимизированный, но более трудный для понимания.
Недавно я провел опрос у себя в Telegram-канале, в котором участвовали 600-700 Go-разработчиков. Я спросил: часто ли они используют пакет unsafe на практике?

По результатам опроса, 79% никогда не использовали. Но при этом не факт, что вам не придется это делать завтра. Еще 18% редко использовали, а 3% постоянно взаимодействуют с unsafe.
Переходите в мой ТГ-канал и на YouTube — там тоже много всего интересного!
А чтобы получить еще больше знаний, которые можно применить на практике уже сегодня, приходите на конференцию развития GolangConf 20 апреля! Принять участие можно как очно, так и в онлайн-формате. Если сейчас пройти квиз, вы можете получить доступ к топ-видеозаписям GolangConf 2025.