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

go:linkname в Go

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

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

В этой статье рассмотрим //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. Если интересно, записывайтесь.

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

Публикации

Информация

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