Привет, Хабр!
В этой статье рассмотрим //go:linkname — неофициальной, но невероятно мощной фиче Go, которая позволяет вызывать приватные функции и обращаться к закрытым переменным других пакетов.
Что делает //go:linkname
Директива //go:linkname позволяет присвоить локальной функции или переменной имя из другого пакета — даже если эта сущность не экспортирована (т.е. начинается со строчной буквы).
Формат:
//go:linkname localname importpath.name
Чтобы это работало, нужно:
-
Импортировать
unsafe(импорт сам по себе может быть неиспользуемым,_импорт обязателен). -
Использовать эту директиву до объявления функции/переменной.
-
Находиться в пределах одного модуля (
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 }
Это будет работать как обычная переменная, с полноценным доступом к содержимому.
Ограничения
-
Не работает между модулями. Только внутри одного
go.mod. -
Не работает, если исходник ссылается на отсутствующую функцию без тела. Решение — заглушка
.s. -
В Go 1.22+ требуют
//go:linknameв обе стороны. -
Нельзя использовать, если пакет собирается как
go:embedили с-trimpath. -
Код может сломаться при обновлении 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/
Добавить комментарий