Как стать автором
Обновить
1080.1
OTUS
Цифровые навыки от ведущих экспертов

Обзор пакета unsafe: как обходить ограничения Go (но лучше этого не делать)

Уровень сложностиПростой
Время на прочтение4 мин
Количество просмотров2.1K

Когда вы впервые открываете Go, вас встречает строгая и безопасная среда: никаких сюрпризов, сегфолтов, фишек с указателями. Всё строго, как в хорошо организованной организации. Но есть в этом языке лазейка, которая ломает весь этот порядок и это — пакет unsafe.

Что такое пакет unsafe

С его помощью можно делать вещи, которые язык обычно запрещает.

Вот что он позволяет:

  1. Конвертировать типы указателей.

  2. Достучаться до приватных полей структур.

  3. Лезть напрямую в память и изменять данные.

  4. Работать с выравниванием данных.

Но за всё это вы платите. И не маленькую цену. Этот код может стать крайне нестабильным, а в некоторых случаях — просто некорректным. К тому же сам по себе unsafe ломает философию языка Golang как безопасного языка.

Синтаксис

unsafe.Pointer

unsafe.Pointer — это такая черная дыра для указателей. Он не знает, что за тип данных находится по адресу, и ему, честно говоря, всё равно. Его можно использовать для конвертации указателей туда-сюда.

var p unsafe.Pointer

Но и трогать его напрямую нельзя — только через преобразование.

uintptr

uintptr — это целочисленное представление адреса в памяти. Зачем? Например, чтобы выполнять арифметические операции с указателями.

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	var x int = 42

	// Получаем указатель на x
	ptr := unsafe.Pointer(&x)

	// Преобразуем в uintptr
	address := uintptr(ptr) + 4

	// Обратно в unsafe.Pointer
	ptr2 := unsafe.Pointer(address)

	fmt.Println(ptr2)
}

Важно: после преобразования uintptr в указатель у вас очень мало гарантий, что это всё ещё валидный адрес.

Функции unsafe

Вот основные функции пакета:

  • unsafe.Sizeof: возвращает размер объекта в байтах.

  • unsafe.Alignof: возвращает выравнивание объекта.

  • unsafe.Offsetof: возвращает смещение поля структуры.

Пример с Sizeof:

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	var x int64
	fmt.Println(unsafe.Sizeof(x)) // 8
}

C Alignof:

package main

import (
	"fmt"
	"unsafe"
)

type MyStruct struct {
	A int8
	B int64
}

func main() {
	var s MyStruct

	fmt.Println("Выравнивание поля A:", unsafe.Alignof(s.A)) // 1
	fmt.Println("Выравнивание поля B:", unsafe.Alignof(s.B)) // 8
}

Примеры применения

Конвертация указателей

Классика жанра. Представим, что есть указатель на один тип, а нужно его превратить в указатель на другой.

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	var x int = 42

	// Преобразуем указатель на x в unsafe.Pointer
	ptr := unsafe.Pointer(&x)

	// А затем в указатель на float32
	floatPtr := (*float32)(ptr)

	// Внимание: здесь вас ждёт undefined behavior!
	fmt.Println(*floatPtr)
}

Правда, это опасно, так как данные в памяти хранятся по-разному для каждого типа. Прочитать int как float32 — это всё равно что попытаться открыть банку консервов ложкой.

Доступ к приватным полям

В Go приватные поля начинаются с маленькой буквы. Официально доступ к ним из другого пакета невозможен. Но с unsafe можно поступить так:

package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

type SecretStruct struct {
	privateField string
}

func main() {
	secret := SecretStruct{privateField: "Это секрет"}

	// Получаем доступ через reflect
	field := reflect.ValueOf(&secret).Elem().FieldByName("privateField")

	ptr := unsafe.Pointer(field.UnsafeAddr())
	realPtr := (*string)(ptr)

	// Меняем значение
	*realPtr = "Теперь не секрет"

	fmt.Println(secret.privateField) // "Теперь не секрет"
}

Этот код на первый взгляд невинен, но он ломает защиту компилятора.

Оптимизация копирования

Допустим, есть массив байтов, и хочется превратить его в строку. Обычно Go делает это с копированием данных. А если копировать не хочется?

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	b := []byte("Привет")

	// Преобразуем без копирования
	s := *(*string)(unsafe.Pointer(&b))

	fmt.Println(s) // "Привет"
}

Главное помнить: изменения массива b затронут строку s. Это нарушает привычные принципы работы со строками в Go.

Выравнивание памяти

Go сам заботится о выравнивании данных для оптимизации доступа к ним. Но иногда может захотеться сделать это вручную:

package main

import (
	"fmt"
	"unsafe"
)

type Unaligned struct {
	A int32 // Поле A занимает 4 байта
	B int16 // Поле B занимает 2 байта, но для выравнивания Go добавляет "пустое место"
}

func main() {
	u := Unaligned{A: 10, B: 20}

	// Получаем смещение поля B в структуре
	offset := unsafe.Offsetof(u.B)

	// Преобразуем адрес структуры в uintptr, добавляем смещение
	ptr := unsafe.Pointer(uintptr(unsafe.Pointer(&u)) + offset)

	// Изменяем значение поля B напрямую через указатель
	*(*int16)(ptr) = 42

	// Проверяем результат
	fmt.Println(u.B) // 42
}

Такие приемчики оправданы лишь в редких случаях, например, при работе с низкоуровневыми API или оптимизации критически важного кода. Однако важно учитывать риски: нестабильность кода при изменении компилятора, сложность отладки и возможное нарушение безопасности. Если подобных задач нет, полагайтесь на автоматическое выравнивание, которое Go обеспечивает самостоятельно.

Риски использования unsafe

Всё, что вы прочитали выше, звучит круто. Но это только на первый взгляд. Вот с какими проблемами вы можете столкнуться:

  1. Код становится хрупким. Любое изменение структуры данных может всё сломать.

  2. Отсутствие гарантий от Golang.

  3. Сложная отладка. Ошибки при работе с unsafe не всегда легко обнаружить.

Когда использовать unsafe

Где без unsafe не обойтись:

  • Вы пишете библиотеку низкого уровня.

  • Вам нужна максимальная производительность.

  • Нужно взаимодействовать с C или другой низкоуровневой системой.

Во всех остальных случаях лучше искать более безопасные решения.

Если вы используете unsafe, обязательно пишите тесты, документируйте причины использования и убедитесь, что нет других решений.

В заключение небольшое объявление: 11 декабря в Otus пройдет открытый урок, посвященный созданию чат-бота для генерации мемов с использованием Go.

На уроке вы пройдете все этапы создания, от проектирования структуры до реализации функционала, под руководством опытного преподавателя. Записаться можно по ссылке.

Теги:
Хабы:
+17
Комментарии4

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS