go:linkname в Go

от автора

Привет, Хабр!

В этой статье рассмотрим //go:linkname — неофициальной, но невероятно мощной фиче Go, которая позволяет вызывать приватные функции и обращаться к закрытым переменным других пакетов.

Что делает //go:linkname

Директива //go:linkname позволяет присвоить локальной функции или переменной имя из другого пакета — даже если эта сущность не экспортирована (т.е. начинается со строчной буквы).

Формат:

//go:linkname localname importpath.name

Чтобы это работало, нужно:

  1. Импортировать unsafe (импорт сам по себе может быть неиспользуемым, _ импорт обязателен).

  2. Использовать эту директиву до объявления функции/переменной.

  3. Находиться в пределах одного модуля (go.mod).

Простой пример: доступ к приватной переменной

Допустим, в пакете internal/config есть приватная переменная:

// internal/config/config.go package config  var secretKey = "qwerty123"

Мы хотим получить доступ к ней из другого пакета:

// main.go package main  import ( _ "unsafe" "fmt" )  //go:linkname secretKey internal/config.secretKey var secretKey string  func main() { fmt.Println("Секрет:", secretKey) }

Компилятор соберёт это и мы получим значение приватной переменной без всякого рефлекта.

Вызов приватной функции

// utils/time.go package utils  func nowInNano() int64 { return 1234567890123 }

Вызовем её:

package main  import ( _ "unsafe" "fmt" )  //go:linkname nowInNano utils.nowInNano func nowInNano() int64  func main() { fmt.Println("Время:", nowInNano()) }

Работает так, как будто функция экспортирована.

Пример с runtime: прямой вызов nanotime

package main  import ( _ "unsafe" "fmt" )  //go:linkname nanotime runtime.nanotime func nanotime() int64  func main() { fmt.Println("Текущее время (ns):", nanotime()) }

Пример с timeSleep из runtime

package main  import ( _ "unsafe" "fmt" )  //go:linkname timeSleep runtime.timeSleep func timeSleep(ns int64)  func main() { fmt.Println("Ждём 1 секунду...") timeSleep(1e9) fmt.Println("Готово") }

Функция timeSleep не экспортирована. Но она вызывается из time.Sleep(). Идём напрямую.

Monkey-patch через go:linkname

Допустим, есть приватная функция логгера:

// internal/logger/logger.go package logger  func logDebug(msg string) { fmt.Println("DEBUG:", msg) }

Можно подменить реализацию из другого пакета:

package main  import ( _ "unsafe" "fmt" )  //go:linkname logDebug internal/logger.logDebug var logDebug func(string)  func main() { logDebug = func(msg string) { fmt.Println("[PATCHED]", msg) }  logDebug("оригинальный лог больше не работает") }

Доступ к map и slice по linkname

Да, можно линковать не только функции и строки. Например, глобальный map:

// internal/registry.go package internal  var registry = map[string]int{ "foo": 1, "bar": 2, }

Из другого пакета:

//go:linkname registry internal.registry var registry map[string]int  func main() { fmt.Println("bar =", registry["bar"]) registry["baz"] = 42 }

Это будет работать как обычная переменная, с полноценным доступом к содержимому.

Ограничения

  1. Не работает между модулями. Только внутри одного go.mod.

  2. Не работает, если исходник ссылается на отсутствующую функцию без тела. Решение — заглушка .s.

  3. В Go 1.22+ требуют //go:linkname в обе стороны.

  4. Нельзя использовать, если пакет собирается как go:embed или с -trimpath.

  5. Код может сломаться при обновлении Go или изменении приватных API.

Альтернатива через reflect (но дороже)

val := reflect.ValueOf(obj).Elem().FieldByName("privateField") val.SetInt(42)

Это работает, но медленно. linkname быстрее в разы.

Конечно. Вот расширенный блок, который раскрывает тему линковки не только функций, но и глобальных слайсов, мап и каналов, а также патчинга поведения стандартной библиотеки на лету:

Как линкать не только функции

Глобальный map[string]struct{}

// internal/cache.go package internal  var loadedModules = map[string]struct{}{     "core": {},     "http": {}, }

А теперь в main.go:

package main  import (     _ "unsafe"     "fmt" )  //go:linkname loadedModules internal.loadedModules var loadedModules map[string]struct{}  func main() {     fmt.Println("До:", loadedModules)     loadedModules["net"] = struct{}{}     fmt.Println("После:", loadedModules) }

Ты пишешь в этот map напрямую — без всяких публичных API.

Глобальный []string

// package config var defaultHosts = []string{"localhost", "127.0.0.1"}

В другом файле:

//go:linkname defaultHosts config.defaultHosts var defaultHosts []string  func main() {     fmt.Println(defaultHosts)     defaultHosts = append(defaultHosts, "192.168.1.1")     fmt.Println(defaultHosts) }

Даже если в оригинальном пакете defaultHosts не экспортируется — можно расширять его как хочешь.

Глобальный chan struct{}

// internal/control.go package internal  var shutdownSignal = make(chan struct{})
// main.go //go:linkname shutdownSignal internal.shutdownSignal var shutdownSignal chan struct{}  func main() {     go func() {         <-shutdownSignal         fmt.Println("Система выключается")     }()          shutdownSignal <- struct{}{} }

Можно использовать даже для синхронизации между пакетами без публичных API. Плюс — это быстрый путь к внедрению хуков в системные процессы.

Патчим поведение stdlib

Допустим, хочется переписать поведение логгера внутри стандартной библиотеки, которая логгирует через приватную logf:

// стандартная реализация, где-то внутри log/log.go func logf(format string, args ...interface{}) {     fmt.Printf(format, args...) }

В main.go:

//go:linkname logf log.logf var logf func(string, ...interface{})  func main() {     logf = func(format string, args ...interface{}) {         fmt.Println("[OVERRIDDEN LOG]:", fmt.Sprintf(format, args...))     } }

Теперь каждый вызов внутреннего логгера будет идти через твою функцию.


Больше про языки программирования эксперты OTUS рассказывают в рамках практических онлайн-курсов. С полным каталогом курсов можно ознакомиться в каталоге, а в календаре — записаться на открытые уроки.

  • 28 апреля в 20:00 пройдет открытый урок на тему «Интерфейсы в Golang на практике». На этом уроке мы на примерах разберем несколько типовых ситуаций применения интерфейсов в Go. Если интересно, записывайтесь.


ссылка на оригинал статьи https://habr.com/ru/articles/900306/