golang

Черная магия unsafe в Go: практические примеры и ошибки использования. Часть 2

  • пятница, 20 марта 2026 г. в 00:00:12
https://habr.com/ru/companies/oleg-bunin/articles/1006052/

Привет, Хабр! Я — Владимир Балун, и это — вторая часть материала о пакете с отпугивающим названием «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.