go:linkname в Go
- пятница, 18 апреля 2025 г. в 00:00:11
Привет, Хабр!
В этой статье рассмотрим //go:linkname
— неофициальной, но невероятно мощной фиче Go, которая позволяет вызывать приватные функции и обращаться к закрытым переменным других пакетов.
Директива //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())
}
Работает так, как будто функция экспортирована.
package main
import (
_ "unsafe"
"fmt"
)
//go:linkname nanotime runtime.nanotime
func nanotime() int64
func main() {
fmt.Println("Текущее время (ns):", nanotime())
}
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()
. Идём напрямую.
Допустим, есть приватная функция логгера:
// 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:
// 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.
val := reflect.ValueOf(obj).Elem().FieldByName("privateField")
val.SetInt(42)
Это работает, но медленно. linkname
быстрее в разы.
Конечно. Вот расширенный блок, который раскрывает тему линковки не только функций, но и глобальных слайсов, мап и каналов, а также патчинга поведения стандартной библиотеки на лету:
// 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.
// 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
не экспортируется — можно расширять его как хочешь.
// 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. Плюс — это быстрый путь к внедрению хуков в системные процессы.
Допустим, хочется переписать поведение логгера внутри стандартной библиотеки, которая логгирует через приватную 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. Если интересно, записывайтесь.