Релиз Go 1.24 запланирован на февраль, поэтому сейчас самое время узнать, что нового появилось в языке. Официальные release notes (примечания к релизу) довольно сухие, поэтому я подготовил интерактивную версию с множеством примеров, которые покажут, что именно изменилось и как теперь работает код.
Также я добавил ссылки на соответствующие предложения (?) и коммиты (??) для описанных фич. Рекомендую их изучить, чтобы лучше понять детали реализации.
Эта статья основана на официальных примечаниях к релизу от The Go Authors, лицензированных по BSD-3-Clause. Это не полный список изменений; для полного перечня см. официальные примечания к релизу.
Содержание
Обобщённые псевдонимы типов
Короткое напоминание: псевдоним типа в Go создаёт синоним для существующего типа, но при этом не создаёт новый тип.
Когда тип определяется на основе другого типа, эти типы считаются разными:
type ID int
var n int = 10
var id ID = 10
// id = n
// Ошибка на этапе компиляции:
// нельзя использовать n (переменную типа int) в качестве значения типа ID при присваивании
id = ID(n)
fmt.Printf("id is %T\n", id)
Когда тип объявляется псевдонимом другого типа, оба типа остаются идентичными:
type ID = int
var n int = 10
var id ID = 10
id = n // работает без ошибок
fmt.Printf("id is %T\n", id)
В Go 1.24 теперь поддерживаются обобщённые (generic) псевдонимы типов: псевдонимы могут быть параметризованы, как и обычные определённые типы. Например, можно определить Set
как обобщённый псевдоним для map
, где значения имеют тип bool (хотя это не даёт особых преимуществ):
type Set[T comparable] = map[T]bool
set := Set[string]{"one": true, "two": true}
fmt.Println("'one' in set:", set["one"])
fmt.Println("'six' in set:", set["six"])
fmt.Printf("set is %T\n", set)
Соответствующие изменения внесены в спецификацию языка. На данный момент функцию можно отключить, установив переменную окружения GOEXPERIMENT=noaliastypeparams
, но этот флаг будет удалён в Go 1.25.
? 46477
Слабые указатели
Слабый указатель (weak pointer) ссылается на объект так же, как и обычный указатель. Но в отличие от обычного указателя, слабый указатель не удерживает объект в памяти. Если на объект ссылаются только слабые указатели, сборщик мусора может освободить занимаемую им память.
Допустим, у нас есть тип blob:
// Blob — это большой срез байтов.
type Blob []byte
func (b Blob) String() string {
return fmt.Sprintf("Blob(%d KB)", len(b)/1024)
}
// newBlob возвращает новый Blob указанного размера в КБ.
func newBlob(size int) *Blob {
b := make([]byte, size*1024)
for i := range size {
b[i] = byte(i) % 255
}
return (*Blob)(&b)
}
И указатель на объект размером 1000 КБ:
func main() {
b := newBlob(1000) // 1000 KB
fmt.Println(b)
}
Можно создать слабый указатель (weak.Pointer
) из обычного с помощью weak.Make
, а затем получить оригинальный указатель через метод Pointer.Value
:
func main() {
wb := weak.Make(newBlob(1000)) // 1000 KB
fmt.Println(wb.Value())
}
Обычный указатель предотвращает удаление объекта сборщиком мусора:
func main() {
heapSize := getAlloc()
b := newBlob(1000)
fmt.Println("value before GC =", b)
runtime.GC()
fmt.Println("value after GC =", b)
fmt.Printf("heap size delta = %d KB\n", heapDelta(heapSize))
}
Что такое getAlloc и heapDelta?
// heapDelta возвращает разницу в КБ между
// текущим размером кучи и предыдущим размером кучи.
func heapDelta(prev uint64) uint64 {
cur := getAlloc()
if cur < prev {
return 0
}
return cur - prev
}
// getAlloc возвращает текущий размер кучи в КБ.
func getAlloc() uint64 {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return m.Alloc / 1024
}
Слабый указатель позволяет сборщику мусора освободить память:
func main() {
heapSize := getAlloc()
wb := weak.Make(newBlob(1000))
fmt.Println("value before GC =", wb.Value())
runtime.GC()
fmt.Println("value after GC =", wb.Value())
fmt.Printf("heap size delta = %d KB\n", heapDelta(heapSize))
}
Как видно, Pointer.Value
возвращает nil
, если оригинальный объект был собран сборщиком мусора. Обратите внимание: нет гарантии, что nil
вернётся сразу после того, как объект перестанет использоваться (или в любой последующий момент); решение о его удалении принимает runtime.
Слабые указатели особенно полезны для реализации кешей крупных объектов, так как они позволяют избежать ситуации, когда объект продолжает существовать только потому, что на него есть ссылка в кеше.
Улучшенные финализаторы
Вернёмся к blob:
func main() {
b := newBlob(1000)
fmt.Printf("b=%v, type=%T\n", b, b)
}
Что, если нужно запустить функцию очистки, когда blob будет удалён сборщиком мусора?
Ранее для этого использовался runtime.SetFinalizer
, но этот механизм был известен своей сложностью в использовании. Теперь появилась более удобная альтернатива — runtime.AddCleanup
:
func main() {
b := newBlob(1000)
now := time.Now()
// Регистрируем функцию очистки, которая будет выполняться,
// когда объект станет недоступным.
runtime.AddCleanup(b, cleanup, now)
time.Sleep(10 * time.Millisecond)
b = nil
runtime.GC()
time.Sleep(10 * time.Millisecond)
}
func cleanup(created time.Time) {
fmt.Printf(
"object is cleaned up! lifetime = %dms\n",
time.Since(created)/time.Millisecond,
)
}
AddCleanup
привязывает функцию очистки к объекту, которая выполняется, когда объект становится недоступным. Функция очистки запускается в отдельной горутине, которая последовательно обрабатывает все вызовы очистки в программе. К одному указателю можно привязать несколько функций очистки.
Обратите внимание на аргумент функции очистки:
// AddCleanup привязывает функцию очистки к ptr.
// Через некоторое время после того, как ptr станет недоступным,
// рантайм вызовет cleanup(arg) в отдельной горутине.
func AddCleanup[T, S any](ptr *T, cleanup func(S), arg S) Cleanup
В приведённом выше примере мы передали время создания в качестве аргумента, но обычно это был бы ресурс, который необходимо освободить при сборке указателя сборщиком мусора.
Допустим, мы хотим реализовать WeakMap
, в которой элементы автоматически удаляются, если на них больше никто не ссылается. Используем map со значениями типа weak.Pointer
:
// WeakMap — это карта (map) со слабо ссылочными значениями.
type WeakMap[K comparable, V any] struct {
store map[K]weak.Pointer[V]
mu sync.Mutex
}
// NewWeakMap создаёт новый WeakMap.
func NewWeakMap[K comparable, V any]() *WeakMap[K, V] {
return &WeakMap[K, V]{
store: make(map[K]weak.Pointer[V]),
}
}
// Len возвращает количество элементов в карте.
func (wm *WeakMap[K, V]) Len() int {
wm.mu.Lock()
defer wm.mu.Unlock()
return len(wm.store)
}
Получить значение просто:
// Get возвращает значение, хранящееся в карте для указанного ключа,
// или nil, если значение отсутствует.
func (wm *WeakMap[K, V]) Get(key K) *V {
wm.mu.Lock()
defer wm.mu.Unlock()
if wp, found := wm.store[key]; found {
return wp.Value()
}
return nil
}
Как сделать так, чтобы элемент удалялся из map
, когда runtime освобождает память, занимаемую значением? С runtime.AddCleanup
это просто:
// Set устанавливает значение для указанного ключа.
func (wm *WeakMap[K, V]) Set(key K, value *V) {
wm.mu.Lock()
defer wm.mu.Unlock()
// Создаёт слабую ссылку на значение.
wp := weak.Make(value)
// Удаляет элемент, когда значение будет собрано сборщиком мусора.
runtime.AddCleanup(value, wm.Delete, key)
// Сохраняет слабую ссылку в карте.
wm.store[key] = wp
}
// Delete удаляет элемент по ключу.
func (wm *WeakMap[K, V]) Delete(key K) {
wm.mu.Lock()
defer wm.mu.Unlock()
delete(wm.store, key)
}
Мы передаём текущий ключ в функцию очистки (wm.Delete
), чтобы она знала, какой элемент удалить из map
.
var sink *Blob
func main() {
wm := NewWeakMap[string, Blob]()
wm.Set("one", newBlob(10))
wm.Set("two", newBlob(20))
fmt.Println("Before GC:")
fmt.Println("len(map) =", wm.Len())
fmt.Println("map[one] =", wm.Get("one"))
fmt.Println("map[two] =", wm.Get("two"))
// Позволяем сборщику мусора освободить
// второй элемент, но не первый.
sink = wm.Get("one")
runtime.GC()
fmt.Println("After GC:")
fmt.Println("len(map) =", wm.Len())
fmt.Println("map[one] =", wm.Get("one"))
fmt.Println("map[two] =", wm.Get("two"))
}
Работает как надо!
Обратите внимание, что функция очистки не гарантированно выполняется сразу после того, как объект перестал быть доступен. Она может запуститься в произвольный момент в будущем.
С появлением AddCleanup
использование SetFinalizer
не рекомендуется. В новом коде следует отдавать предпочтение AddCleanup
.
Swiss Tables
Спустя многие годы команда Go изменила базовую реализацию map
. Теперь она основана на SwissTable, что привело к ряду оптимизаций:
Операции чтения и записи в
map
с более чем 1024 элементами стали быстрее примерно на 30%.Запись в заранее выделенные
map
стала быстрее примерно на 35%.Итерация ускорилась в среднем на 10%, а для
map
с небольшим числом записей относительно размера — до 60%.
Бенчмарки
Хотя результаты не учитывают всех оптимизаций, они дают общее представление об изменениях.
│ /tmp/noswiss.lu.txt │ /tmp/swiss.lu.txt │
│ sec/op │ sec/op vs base │
MapIter/impl=runtimeMap/t=Int64/len=64-12 642.0n ± 3% 603.8n ± 6% -5.95% (p=0.004 n=6)
MapIter/impl=runtimeMap/t=Int64/len=8192-12 87.98µ ± 1% 78.80µ ± 1% -10.43% (p=0.002 n=6)
MapIter/impl=runtimeMap/t=Int64/len=4194304-12 47.40m ± 2% 44.41m ± 2% -6.30% (p=0.002 n=6)
MapIterLowLoad/impl=runtimeMap/t=Int64/len=64-12 145.85n ± 3% 92.85n ± 2% -36.34% (p=0.002 n=6)
MapIterLowLoad/impl=runtimeMap/t=Int64/len=8192-12 13.205µ ± 0% 6.078µ ± 1% -53.97% (p=0.002 n=6)
MapIterLowLoad/impl=runtimeMap/t=Int64/len=4194304-12 15.20m ± 1% 18.22m ± 1% +19.87% (p=0.002 n=6)
MapIterGrow/impl=runtimeMap/t=Int64/len=64-12 10.196µ ± 2% 8.092µ ± 8% -20.63% (p=0.002 n=6)
MapIterGrow/impl=runtimeMap/t=Int64/len=8192-12 1.259m ± 2% 1.008m ± 4% -19.97% (p=0.002 n=6)
MapIterGrow/impl=runtimeMap/t=Int64/len=4194304-12 1.424 ± 5% 1.275 ± 0% -10.47% (p=0.002 n=6)
MapGetHit/impl=runtimeMap/t=Int64/len=64-12 14.08n ± 4% 15.28n ± 3% +8.45% (p=0.002 n=6)
MapGetHit/impl=runtimeMap/t=Int64/len=8192-12 27.61n ± 1% 18.80n ± 1% -31.89% (p=0.002 n=6)
MapGetHit/impl=runtimeMap/t=Int64/len=4194304-12 82.94n ± 1% 102.20n ± 0% +23.22% (p=0.002 n=6)
MapGetHit/impl=runtimeMap/t=Int32/len=64-12 13.84n ± 5% 15.56n ± 2% +12.39% (p=0.002 n=6)
MapGetHit/impl=runtimeMap/t=Int32/len=8192-12 26.90n ± 2% 18.47n ± 2% -31.34% (p=0.002 n=6)
MapGetHit/impl=runtimeMap/t=Int32/len=4194304-12 79.60n ± 0% 93.00n ± 0% +16.83% (p=0.002 n=6)
MapGetHit/impl=runtimeMap/t=String/len=64-12 16.36n ± 6% 18.69n ± 1% +14.24% (p=0.002 n=6)
MapGetHit/impl=runtimeMap/t=String/len=8192-12 38.39n ± 1% 25.67n ± 1% -33.13% (p=0.002 n=6)
MapGetHit/impl=runtimeMap/t=String/len=4194304-12 146.0n ± 1% 172.2n ± 1% +17.95% (p=0.002 n=6)
MapGetMiss/impl=runtimeMap/t=Int64/len=64-12 15.63n ± 8% 15.08n ± 8% ~ (p=0.240 n=6)
MapGetMiss/impl=runtimeMap/t=Int64/len=8192-12 17.55n ± 1% 17.59n ± 4% ~ (p=0.909 n=6)
MapGetMiss/impl=runtimeMap/t=Int64/len=4194304-12 106.40n ± 1% 72.99n ± 2% -31.40% (p=0.002 n=6)
MapGetMiss/impl=runtimeMap/t=Int32/len=64-12 15.63n ± 7% 15.27n ± 8% ~ (p=0.132 n=6)
MapGetMiss/impl=runtimeMap/t=Int32/len=8192-12 17.18n ± 3% 17.25n ± 1% ~ (p=0.729 n=6)
MapGetMiss/impl=runtimeMap/t=Int32/len=4194304-12 100.15n ± 1% 74.71n ± 1% -25.40% (p=0.002 n=6)
MapGetMiss/impl=runtimeMap/t=String/len=64-12 18.96n ± 3% 18.19n ± 11% ~ (p=0.132 n=6)
MapGetMiss/impl=runtimeMap/t=String/len=8192-12 23.79n ± 3% 20.98n ± 2% -11.79% (p=0.002 n=6)
MapGetMiss/impl=runtimeMap/t=String/len=4194304-12 134.85n ± 1% 84.82n ± 1% -37.10% (p=0.002 n=6)
MapPutGrow/impl=runtimeMap/t=Int64/len=64-12 5.886µ ± 3% 5.699µ ± 3% -3.18% (p=0.015 n=6)
MapPutGrow/impl=runtimeMap/t=Int64/len=8192-12 739.1µ ± 2% 816.0µ ± 4% +10.41% (p=0.002 n=6)
MapPutGrow/impl=runtimeMap/t=Int64/len=4194304-12 929.3m ± 1% 894.2m ± 5% ~ (p=0.065 n=6)
MapPutGrow/impl=runtimeMap/t=Int32/len=64-12 5.487µ ± 4% 5.326µ ± 2% -2.93% (p=0.028 n=6)
MapPutGrow/impl=runtimeMap/t=Int32/len=8192-12 681.6µ ± 2% 767.3µ ± 2% +12.58% (p=0.002 n=6)
MapPutGrow/impl=runtimeMap/t=Int32/len=4194304-12 831.9m ± 2% 802.9m ± 1% -3.49% (p=0.002 n=6)
MapPutGrow/impl=runtimeMap/t=String/len=64-12 7.607µ ± 2% 7.379µ ± 2% -2.99% (p=0.002 n=6)
MapPutGrow/impl=runtimeMap/t=String/len=8192-12 1.204m ± 4% 1.212m ± 4% ~ (p=0.310 n=6)
MapPutGrow/impl=runtimeMap/t=String/len=4194304-12 1.699 ± 2% 1.876 ± 1% +10.37% (p=0.002 n=6)
MapPutPreAllocate/impl=runtimeMap/t=Int64/len=64-12 2.179µ ± 1% 1.428µ ± 5% -34.47% (p=0.002 n=6)
MapPutPreAllocate/impl=runtimeMap/t=Int64/len=8192-12 277.6µ ± 2% 198.6µ ± 1% -28.45% (p=0.002 n=6)
MapPutPreAllocate/impl=runtimeMap/t=Int64/len=4194304-12 389.7m ± 1% 518.2m ± 1% +32.97% (p=0.002 n=6)
MapPutPreAllocate/impl=runtimeMap/t=Int32/len=64-12 1.784µ ± 2% 1.110µ ± 3% -37.78% (p=0.002 n=6)
MapPutPreAllocate/impl=runtimeMap/t=Int32/len=8192-12 228.1µ ± 5% 151.4µ ± 4% -33.62% (p=0.002 n=6)
MapPutPreAllocate/impl=runtimeMap/t=Int32/len=4194304-12 361.5m ± 1% 481.2m ± 1% +33.10% (p=0.002 n=6)
MapPutPreAllocate/impl=runtimeMap/t=String/len=64-12 2.670µ ± 3% 2.167µ ± 3% -18.81% (p=0.002 n=6)
MapPutPreAllocate/impl=runtimeMap/t=String/len=8192-12 380.1µ ± 2% 417.2µ ± 9% +9.77% (p=0.015 n=6)
MapPutPreAllocate/impl=runtimeMap/t=String/len=4194304-12 493.1m ± 4% 718.1m ± 7% +45.62% (p=0.002 n=6)
MapPutReuse/impl=runtimeMap/t=Int32/len=64-12 1421.0n ± 3% 804.0n ± 5% -43.42% (p=0.002 n=6)
MapPutReuse/impl=runtimeMap/t=Int32/len=8192-12 192.4µ ± 1% 120.6µ ± 1% -37.30% (p=0.002 n=6)
MapPutReuse/impl=runtimeMap/t=Int32/len=4194304-12 364.0m ± 2% 473.0m ± 2% +29.95% (p=0.002 n=6)
MapPutReuse/impl=runtimeMap/t=String/len=64-12 1.602µ ± 4% 1.083µ ± 14% -32.41% (p=0.002 n=6)
MapPutReuse/impl=runtimeMap/t=String/len=8192-12 232.4µ ± 1% 165.7µ ± 2% -28.68% (p=0.002 n=6)
MapPutReuse/impl=runtimeMap/t=String/len=4194304-12 440.4m ± 2% 672.5m ± 1% +52.72% (p=0.002 n=6)
MapPutDelete/impl=runtimeMap/t=Int64/len=64-12 34.25n ± 3% 37.76n ± 5% +10.23% (p=0.002 n=6)
MapPutDelete/impl=runtimeMap/t=Int64/len=8192-12 57.91n ± 2% 45.24n ± 2% -21.89% (p=0.002 n=6)
MapPutDelete/impl=runtimeMap/t=Int64/len=4194304-12 170.5n ± 0% 222.0n ± 1% +30.20% (p=0.002 n=6)
MapPutDelete/impl=runtimeMap/t=Int32/len=64-12 34.06n ± 4% 37.87n ± 6% +11.16% (p=0.002 n=6)
MapPutDelete/impl=runtimeMap/t=Int32/len=8192-12 54.92n ± 1% 43.41n ± 2% -20.96% (p=0.002 n=6)
MapPutDelete/impl=runtimeMap/t=Int32/len=4194304-12 153.4n ± 1% 178.3n ± 2% +16.26% (p=0.002 n=6)
MapPutDelete/impl=runtimeMap/t=String/len=64-12 42.11n ± 8% 48.48n ± 7% +15.12% (p=0.002 n=6)
MapPutDelete/impl=runtimeMap/t=String/len=8192-12 78.46n ± 1% 56.10n ± 2% -28.50% (p=0.002 n=6)
MapPutDelete/impl=runtimeMap/t=String/len=4194304-12 204.6n ± 1% 261.4n ± 1% +27.76% (p=0.002 n=6)
Вы можете отключить новую реализацию, установив GOEXPERIMENT=noswissmap
во время сборки.
? 54766
Concurrent hash-trie map
Реализация sync.Map была изменена на конкурентный hash-trie, что повысило производительность, особенно при изменении map
. Изменения непересекающихся наборов ключей реже приводят к конфликтам при работе с крупными map
, и больше не требуется время на «прогрев» для достижения низкого уровня конфликтов.
Новая реализация превосходит старую почти во всех бенчмарках:
│ before │ after │
│ sec/op │ sec/op vs base │
MapLoadMostlyHits 7.870n ± 1% 8.415n ± 3% +6.93% (p=0.002 n=6)
MapLoadMostlyMisses 7.210n ± 1% 5.314n ± 2% -26.28% (p=0.002 n=6)
MapLoadOrStoreBalanced 360.10n ± 18% 71.78n ± 2% -80.07% (p=0.002 n=6)
MapLoadOrStoreUnique 707.2n ± 18% 135.2n ± 4% -80.88% (p=0.002 n=6)
MapLoadOrStoreCollision 5.089n ± 201% 3.963n ± 1% -22.11% (p=0.002 n=6)
MapLoadAndDeleteBalanced 17.045n ± 64% 5.280n ± 1% -69.02% (p=0.002 n=6)
MapLoadAndDeleteUnique 14.250n ± 57% 6.452n ± 1% ~ (p=0.368 n=6)
MapLoadAndDeleteCollision 19.34n ± 39% 23.31n ± 27% ~ (p=0.180 n=6)
MapRange 3.055µ ± 3% 1.918µ ± 2% -37.23% (p=0.002 n=6)
MapAdversarialAlloc 245.30n ± 6% 14.90n ± 23% -93.92% (p=0.002 n=6)
MapAdversarialDelete 143.550n ± 2% 8.184n ± 1% -94.30% (p=0.002 n=6)
MapDeleteCollision 9.199n ± 65% 3.165n ± 1% -65.59% (p=0.002 n=6)
MapSwapCollision 164.7n ± 7% 108.7n ± 36% -34.01% (p=0.002 n=6)
MapSwapMostlyHits 33.12n ± 15% 35.79n ± 9% ~ (p=0.180 n=6)
MapSwapMostlyMisses 604.5n ± 5% 280.2n ± 7% -53.64% (p=0.002 n=6)
MapCompareAndSwapCollision 96.02n ± 40% 69.93n ± 24% -27.17% (p=0.041 n=6)
MapCompareAndSwapNoExistingKey 6.345n ± 1% 6.202n ± 1% -2.24% (p=0.002 n=6)
MapCompareAndSwapValueNotEqual 6.121n ± 3% 5.564n ± 4% -9.09% (p=0.002 n=6)
MapCompareAndSwapMostlyHits 44.21n ± 13% 43.46n ± 11% ~ (p=0.485 n=6)
MapCompareAndSwapMostlyMisses 33.51n ± 6% 13.51n ± 5% -59.70% (p=0.002 n=6)
MapCompareAndDeleteCollision 27.85n ± 104% 31.02n ± 26% ~ (p=0.180 n=6)
MapCompareAndDeleteMostlyHits 50.43n ± 33% 109.45n ± 8% +117.03% (p=0.002 n=6)
MapCompareAndDeleteMostlyMisses 27.17n ± 7% 11.37n ± 3% -58.14% (p=0.002 n=6)
MapClear 300.2n ± 5% 124.2n ± 8% -58.64% (p=0.002 n=6)
geomean 50.38n 25.79n -48.81%
Сценарий частых успешных чтений (MapLoadMostlyHits
) стал немного медленнее из-за того, что Swiss Tables улучшили производительность старой версии sync.Map. Некоторые бенчмарки показывают значительное замедление, но это в основном связано с тем, что новая реализация сразу уменьшает размер map
при удалении элементов, тогда как старая версия делала это поэтапно — сначала перемещая удалённые элементы в dirty map
, прежде чем окончательно их удалять.
Конкурентный hash-trie (HashTrieMap
) впервые появился в Go 1.23 в составе пакета unique
. Он оказался быстрее оригинального sync.Map в большинстве случаев, поэтому команда Go переработала sync.Map как обёртку над HashTrieMap
.
Чтобы отключить новую реализацию, установите GOEXPERIMENT=nosynchashtriemap
во время сборки.
Ограниченный каталогом доступ к файловой системе
Новый тип os.Root ограничивает файловые операции определённым каталогом.
Функция OpenRoot
открывает каталог и возвращает Root
:
dir, err := os.OpenRoot("data")
fmt.Printf("opened root=%s, err=%v\n", dir.Name(), err)
Методы Root
ограничивают файловые операции пределами этого каталога, запрещая доступ к внешним путям:
file, err := dir.Open("01.txt")
fmt.Printf("opened file=%s, err=%v\n", file.Name(), err)
file, err = dir.Open("../main.txt")
fmt.Printf("opened file=%v, err=%v\n", file, err)
Методы Root
дублируют большинство файловых операций, доступных в пакете os
file, err := dir.Create("new.txt")
fmt.Printf("created file=%s, err=%v\n", file.Name(), err)
stat, err := dir.Stat("02.txt")
fmt.Printf(
"file info: name=%s, size=%dB, mode=%v, err=%v\n",
stat.Name(), stat.Size(), stat.Mode(), err,
)
err = dir.Remove("03.txt")
fmt.Printf("deleted 03.txt, err=%v\n", err)
Завершив работу с Root
, его следует закрыть:
func process(dir string) error {
r, err := os.OpenRoot(dir)
if err != nil {
return err
}
defer r.Close()
// выполняем операции
return nil
}
После закрытия Root
вызовы его методов будут возвращать ошибки:
err = dir.Close()
fmt.Printf("closed root, err=%v\n", err)
file, err := dir.Open("01.txt")
fmt.Printf("opened file=%v, err=%v\n", file, err)
Методы Root
следуют символическим ссылкам, но такие ссылки не могут указывать за пределы корневого каталога. Символические ссылки должны быть относительными. Методы Root
не ограничивают:
пересечение границ файловых систем,
монтирование с привязкой (bind mount) в Linux,
специальные файлы /proc,
доступ к файловым устройствам Unix.
На большинстве платформ создание Root открывает файловый дескриптор или хендл для каталога. Если каталог был перемещён, методы Root продолжают ссылаться на этот каталог в его новом местоположении.
? 67002 • ?? 612136, 627076, 627475, 629518, 629555
Benchmark loop
Вы, вероятно, знакомы с традиционным циклом бенчмарка (for range b.N):
var sink int
func BenchmarkSlicesMax(b *testing.B) {
// Подготовка бенчмарка.
s := randomSlice(10_000)
b.ResetTimer()
// Запуск бенчмарка.
for range b.N {
sink = slices.Max(s)
}
}
Go автоматически управляет выполнением бенчмарков, определяет подходящее b.N
и выводит конечные результаты в наносекундах на операцию.
Однако есть несколько нюансов:
Функция бенчмарка (
BenchmarkSlicesMax
) выполняется многократно, этап подготовки запускается каждый раз (избежать этого нельзя).Нужно сбрасывать таймер (
b.ResetTimer
), чтобы исключить время подготовки из времени бенчмарка.Чтобы убедиться, что компилятор не оптимизирует вызов тестируемой функции, используется переменная-поглотитель (sink variable).
В Go 1.24 появился новый, более быстрый и надёжный механизм — b.Loop, который заменяет традиционный цикл for range b.N
:
func BenchmarkSlicesMax(b *testing.B) {
// Подготовка бенчмарка.
s := randomSlice(10_000)
// Запуск бенчмарка.
for b.Loop() {
slices.Max(s)
}
}
b.Loop
устраняет недостатки подхода с b.N
:
Функция бенчмарка выполняется один раз на каждые
-count
, поэтому инициализация и очистка выполняются только один раз.Всё, что находится за пределами
b.Loop
, не влияет на время выполнения бенчмарка, поэтомуb.ResetTimer
не нужен.Компилятор никогда не оптимизирует вызовы функций внутри
b.Loop
.
Бенчмарки должны использовать либо b.Loop
, либо b.N
, но не оба метода одновременно.
? 61515 • ?? 608798, 612043, 612835, 627755, 635898
Синтетическое (эмулируемое) время для тестирования
Предположим, у нас есть функция, которая ждёт значение из канала в течение одной минуты, после чего завершает выполнение по таймауту:
// Read считывает значение из входного канала и возвращает его.
// В случае таймаута через 60 секунд возвращает ошибку.
func Read(in chan int) (int, error) {
select {
case v := <-in:
return v, nil
case <-time.After(60 * time.Second):
return 0, errors.New("timeout")
}
}
Она используется так:
func main() {
ch := make(chan int)
go func() {
ch <- 42
}()
val, err := Read(ch)
fmt.Printf("val=%v, err=%v\n", val, err)
}
Как протестировать ситуацию с таймаутом? Очевидно, что нам не хочется ждать минуту во время теста. Можно было бы сделать таймаут параметром (и, вероятно, стоит), но допустим, что мы предпочитаем оставить его жёстко заданным.
На помощь приходит новый пакет testing/synctest
Функция synctest.Run() создаёт изолированную среду в новой горутине, где функции пакета time
работают с эмулированным временем. Это позволяет тесту проходить моментально:
func TestReadTimeout(t *testing.T) {
synctest.Run(func() {
ch := make(chan int)
_, err := Read(ch)
if err == nil {
t.Fatal("ожидалась ошибка таймаута, но получили nil")
}
})
}
Горутины внутри bubble используют эмулируемое время. Изначальное время — полночь UTC, 1 января 2000 года. Время автоматически продвигается вперёд, когда все горутины внутри bubble блокируются.
В нашем примере единственная горутина заблокирована на select
внутри Read, поэтому виртуальные часы мгновенно продвигаются на 60 секунд, вызывая таймаут.
Дополнительная функция — synctest.Wait
. Она ожидает, пока все горутины в текущей изолированной среде (bubble) заблокируются, а затем возобновляет выполнение:
synctest.Run(func() {
const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// Ждём чуть меньше таймаута.
time.Sleep(timeout - time.Nanосекунда)
synctest.Wait()
fmt.Printf("до таймаута: ctx.Err() = %v\n", ctx.Err())
// Дожидаемся оставшегося времени до таймаута.
time.Sleep(time.Наносекунда)
synctest.Wait()
fmt.Printf("после таймаута: ctx.Err() = %v\n", ctx.Err())
})
before timeout: ctx.Err() = <nil>
after timeout: ctx.Err() = context deadline exceeded
Пакет synctest
является экспериментальным и должен быть включён вручную путём установки GOEXPERIMENT=synctest
при сборке. API пакета может измениться в будущих версиях.
Контекст теста и рабочая директория
Допустим, мы хотим протестировать этот полезный сервер:
// Server предоставляет ответы на все вопросы.
type Server struct{}
// Get возвращает ответ от сервера.
func (s *Server) Get(query string) int {
return 42
}
// startServer запускает сервер, который можно
// остановить путем отмены контекста.
func startServer(ctx context.Context) *Server {
go func() {
select {
case <-ctx.Done():
// Освобождение ресурсов.
}
}()
return &Server{}
}
И вот тест, который я написал:
func Test(t *testing.T) {
srv := startServer(context.Background())
if srv.Get("how much?") != 42 {
t.Fatal("unexpected value")
}
}
Ура, тест прошёл! Однако есть проблема: я использовал пустой контекст, поэтому сервер не остановился. Такая утечка ресурсов может стать проблемой, особенно если тестов много.
Чтобы это исправить, я создам отменяемый контекст и отменю его по завершении теста:
func Test(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
srv := startServer(ctx)
if srv.Get("how much?") != 42 {
t.Fatal("unexpected value")
}
}
Ещё лучше: теперь я могу использовать новый метод T.Context. Он возвращает контекст, который автоматически отменяется после завершения теста:
func Test(t *testing.T) {
srv := startServer(t.Context())
if srv.Get("how much?") != 42 {
t.Fatal("unexpected value")
}
}
Но есть нюанс. Что, если освобождение ресурсов сервера занимает время? Горутина startServer
начнёт очистку при закрытии контекста, но успеет ли она завершиться, прежде чем основная горутина завершит тест? Не обязательно.
Есть полезное свойство тестового контекста: он отменяется прямо перед вызовом функций, зарегистрированных через T.Cleanup
. Поэтому можно использовать T.Cleanup
, чтобы зарегистрировать функцию, которая дождётся завершения сервера:
func Test(t *testing.T) {
srv := startServer(t.Context())
t.Cleanup(func() {
<-srv.Done()
})
if srv.Get("how much?") != 42 {
t.Fatal("unexpected value")
}
}
Изменения в коде сервера (спойлер)
Код сервера также изменился.
// Server предоставляет ответы на все вопросы.
type Server struct {
done chan struct{}
}
// Get возвращает ответ от сервера.
func (s *Server) Get(query string) int {
return 42
}
// Stop останавливает сервер.
func (s *Server) Stop() {
// Симуляция долгой операции.
time.Sleep(10 * time.Millisecond)
fmt.Println("сервер остановлен")
close(s.done)
}
// Done возвращает канал, который закрывается при остановке сервера.
func (s *Server) Done() <-chan struct{} {
return s.done
}
// startServer запускает сервер, который можно
// остановить путем отмены контекста.
func startServer(ctx context.Context) *Server {
srv := &Server{done: make(chan struct{})}
go func() {
select {
case <-ctx.Done():
srv.Stop()
}
}()
return srv
}
Как и тесты, бенчмарки теперь имеют свой собственный B.Context.
Кстати, о тестах: новые методы T.Chdir и B.Chdir изменяют рабочую директорию на время выполнения теста или бенчмарка:
func Test(t *testing.T) {
t.Run("test1", func(t *testing.T) {
// Изменяем рабочую директорию для текущего теста.
t.Chdir("/tmp")
cwd, _ := os.Getwd()
if cwd != "/tmp" {
t.Fatalf("неожиданная рабочая директория: %s", cwd)
}
})
t.Run("test2", func(t *testing.T) {
// Этот тест использует исходную рабочую директорию.
cwd, _ := os.Getwd()
if cwd == "/tmp" {
t.Fatalf("неожиданная рабочая директория: %s", cwd)
}
})
}
Методы Chdir
используют Cleanup
, чтобы автоматически восстановить рабочую директорию после завершения теста или бенчмарка.
Отключение вывода логов
Простой способ создать логгер без вывода сообщений (например, для тестирования или бенчмаркинга) — использовать slog.TextHandler
с io.Discard
:
log := slog.New(
slog.NewTextHandler(io.Discard, nil),
)
log.Info("Prints nothing")
Теперь можно просто использовать slog.DiscardHandler:
log := slog.New(slog.DiscardHandler)
log.Info("Ничего не выводится")
Интерфейсы добавления данных
Новые интерфейсы encoding.TextAppender и encoding.BinaryAppender позволяют добавлять текстовое или бинарное представление объекта в существующий срез байтов:
type TextAppender interface {
// AppendText добавляет текстовое представление объекта в конец `b`
// (выделяя дополнительную память при необходимости) и возвращает обновленный срез.
//
// Реализации не должны сохранять `b` или изменять любые байты в `b[:len(b)]`.
AppendText(b []byte) ([]byte, error)
}
type BinaryAppender interface {
// AppendBinary добавляет бинарное представление объекта в конец `b`
// (выделяя дополнительную память при необходимости) и возвращает обновленный срез.
//
// Реализации не должны сохранять `b` или изменять любые байты в `b[:len(b)]`.
AppendBinary(b []byte) ([]byte, error)
}
Эти интерфейсы предоставляют тот же функционал, что и TextMarshaler
и BinaryMarshaler
, но вместо создания нового среза при каждом вызове добавляют данные напрямую в существующий.
Теперь эти интерфейсы реализованы стандартными типами библиотеки, которые уже поддерживали TextMarshaler
или BinaryMarshaler
:
math/big.Float
net.IP
regexp.Regexp
time.Time
и другие
// 2021-02-03T04:05:06Z
t := time.Date(2021, 2, 3, 4, 5, 6, 0, time.UTC)
var b []byte
b, err := t.AppendText(b)
fmt.Printf("b=%s, err=%v", b, err)
? 62384 • ?? 601595, 601776, 603255, 603815, 605056, 605758, 606655, 607079, 607520, 634515
Больше итераторов для строк и байтов
В Go 1.23 была введена поддержка итераторов, и теперь в стандартной библиотеке появилось ещё больше таких функций.
Новые функции в пакете strings:
Lines — возвращает итератор по строке s
, разбивая её на строки, каждая из которых включает завершающий символ новой строки, за исключением последней, если s
не оканчивается \n
:
s := "one\ntwo\nsix"
for line := range strings.Lines(s) {
fmt.Print(line)
}
SplitSeq — возвращает итератор по всем подстрокам строки s
, разделённым разделителем sep
:
s := "one-two-six"
for part := range strings.SplitSeq(s, "-") {
fmt.Println(part)
}
SplitAfterSeq — возвращает итератор по подстрокам строки s
, разделённым после каждого вхождения sep
:
s := "one-two-six"
for part := range strings.SplitAfterSeq(s, "-") {
fmt.Println(part)
}
FieldsSeq — возвращает итератор по подстрокам строки s
, разделённым в местах последовательностей пробельных символов, как определено в unicode.IsSpace
:
s := "one two\nsix"
for part := range strings.FieldsSeq(s) {
fmt.Println(part)
}
FieldsFuncSeq — возвращает итератор по подстрокам строки s
, разделённым в местах последовательностей символов Unicode, удовлетворяющих функции f(c)
:
f := func(c rune) bool {
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
}
s := "one,two;six..."
for part := range strings.FieldsFuncSeq(s, f) {
fmt.Println(part)
}
Те же функции-итераторы были добавлены в пакет bytes.
SHA-3 и связанные функции
Новый пакет crypto/sha3 реализует хеш-функцию SHA-3, а также функции с расширяемым выводом SHAKE и cSHAKE, как определено в стандарте FIPS 202:
s := []byte("go is awesome")
fmt.Printf("Source: %s\n", s)
fmt.Printf("SHA3-224: %x\n", sha3.Sum224(s))
fmt.Printf("SHA3-256: %x\n", sha3.Sum256(s))
fmt.Printf("SHA3-384: %x\n", sha3.Sum384(s))
fmt.Printf("SHA3-512: %x\n", sha3.Sum512(s))
Добавлены два новых криптографических пакета:
crypto/hkdf — реализует функцию извлечения и расширения ключей HKDF на основе HMAC, как определено в RFC 5869.
crypto/pbkdf2 — реализует функцию извлечения ключей на основе пароля PBKDF2, как определено в RFC 8018.
HTTP-протоколы
В пакете net/http
появились новые поля Server.Protocols и Transport.Protocols, которые позволяют простым способом настроить используемые HTTP-протоколы на сервере или клиенте:
t := http.DefaultTransport.(*http.Transport).Clone()
// Используем либо HTTP/1, либо HTTP/2.
t.Protocols = new(http.Protocols)
t.Protocols.SetHTTP1(true)
t.Protocols.SetHTTP2(true)
cli := &http.Client{Transport: t}
res, err := cli.Get("http://httpbingo.org/status/200")
if err != nil {
panic(err)
}
res.Body.Close()
Поддерживаются следующие протоколы:
HTTP1 — HTTP/1.0 и HTTP/1.1. Поддерживается как по незащищённому TCP, так и через защищённое TLS-соединение.
HTTP2 — протокол HTTP/2 поверх TLS-соединения.
UnencryptedHTTP2 — протокол HTTP/2 поверх незащищённого TCP-соединения.
Пропуск нулевых значений в JSON
В JSON-маршалере появилась новая опция omitzero
, которая указывает пропускать нулевые значения. Это более читаемая и надёжная альтернатива omitempty
, если цель — пропускать только нулевые значения. В отличие от omitempty
, omitzero
также исключает нулевые значения time.Time
, что часто является проблемным местом.
Сравните omitempty
:
type Person struct {
Name string `json:"name"`
BirthDate time.Time `json:"birth_date,omitempty"`
}
alice := Person{Name: "Alice"}
b, err := json.Marshal(alice)
fmt.Println(string(b), err)
С omitzero
:
type Person struct {
Name string `json:"name"`
BirthDate time.Time `json:"birth_date,omitzero"`
}
alice := Person{Name: "Alice"}
b, err := json.Marshal(alice)
fmt.Println(string(b), err)
Если у типа поля есть метод IsZero() bool
, он используется для проверки, является ли значение нулевым. В противном случае значение считается нулевым, если оно равно нулевому значению для своего типа.
Случайный текст
Функция crypto/rand.Text возвращает криптографически случайную строку, используя стандартный алфавит Base32:
text := rand.Text()
fmt.Println(text)
Результат содержит не менее 128 бит случайности, что делает подбор значений крайне сложным и минимизирует вероятность коллизий.
Зависимости от инструментов
Теперь Go-модули могут отслеживать зависимости исполняемых файлов, используя директиву tool
в go.mod
.
Чтобы добавить зависимость, используйте go get -tool
:
go mod init sandbox
go get -tool golang.org/x/tools/cmd/stringer
Это добавляет зависимость с директивой require
в go.mod
:
module sandbox
go 1.24rc1
tool golang.org/x/tools/cmd/stringer
require (
golang.org/x/mod v0.22.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/tools v0.29.0 // indirect
)
Ранее приходилось использовать обходное решение — добавлять инструменты в виде пустых импортов в файл, который традиционно назывался "tools.go". Теперь в этом нет необходимости.
Команда go tool
теперь может запускать эти инструменты, помимо тех, что поставляются с Go:
go tool stringer
Подробности — в документации.
? 48429
Поддержка JSON-вывода для команд build, install и test
Команды go build
, go install
и go test
теперь поддерживают флаг -json
, который выводит результаты и ошибки в формате JSON в стандартный поток вывода.
Пример вывода go test
в обычном режиме:
go test -v
=== RUN TestSet_Add
--- PASS: TestSet_Add (0.00s)
=== RUN TestSet_Contains
--- PASS: TestSet_Contains (0.00s)
PASS
ok sandbox 0.934s
Вывод go test
в JSON-режиме:
go test -json
{"Time":"2025-01-11T19:22:29.280091+05:00","Action":"start","Package":"sandbox"}
{"Time":"2025-01-11T19:22:29.671331+05:00","Action":"run","Package":"sandbox","Test":"TestSet_Add"}
{"Time":"2025-01-11T19:22:29.671418+05:00","Action":"output","Package":"sandbox","Test":"TestSet_Add","Output":"=== RUN TestSet_Add\n"}
{"Time":"2025-01-11T19:22:29.67156+05:00","Action":"output","Package":"sandbox","Test":"TestSet_Add","Output":"--- PASS: TestSet_Add (0.00s)\n"}
{"Time":"2025-01-11T19:22:29.671579+05:00","Action":"pass","Package":"sandbox","Test":"TestSet_Add","Elapsed":0}
{"Time":"2025-01-11T19:22:29.671601+05:00","Action":"run","Package":"sandbox","Test":"TestSet_Contains"}
{"Time":"2025-01-11T19:22:29.671608+05:00","Action":"output","Package":"sandbox","Test":"TestSet_Contains","Output":"=== RUN TestSet_Contains\n"}
{"Time":"2025-01-11T19:22:29.67163+05:00","Action":"output","Package":"sandbox","Test":"TestSet_Contains","Output":"--- PASS: TestSet_Contains (0.00s)\n"}
{"Time":"2025-01-11T19:22:29.671638+05:00","Action":"pass","Package":"sandbox","Test":"TestSet_Contains","Elapsed":0}
{"Time":"2025-01-11T19:22:29.671645+05:00","Action":"output","Package":"sandbox","Output":"PASS\n"}
{"Time":"2025-01-11T19:22:29.672058+05:00","Action":"output","Package":"sandbox","Output":"ok \tsandbox\t0.392s\n"}
{"Time":"2025-01-11T19:22:29.6721+05:00","Action":"pass","Package":"sandbox","Elapsed":0.392}
При этом в стандартный поток ошибок (stderr) могут выводиться сообщения об ошибках, не относящиеся к JSON, даже при использовании флага -json
. Обычно это указывает на критические ошибки, возникшие на ранних этапах выполнения.
Формат JSON описан в go help buildjson
.
? 62067
Версия главного модуля
Команда go build
теперь устанавливает версию главного модуля (BuildInfo.Main.Version
) в скомпилированном бинарном файле на основе версии в системе контроля версий (VCS). Если есть неотправленные изменения, к версии добавляется суффикс +dirty
.
Программа для вывода версии:
// Получаем информацию о сборке, встроенную в выполняемый бинарный файл.
info, _ := debug.ReadBuildInfo()
fmt.Println("Версия Go:", info.GoVersion)
fmt.Println("Версия приложения:", info.Main.Version)
Пример вывода для Go 1.23:
Go version: go1.23.4
App version: (devel)
Пример вывода для Go 1.24:
Go version: go1.24rc1
App version: v0.0.0-20250111143208-a7857c757b85+dirty
Если текущий коммит соответствует тегированной версии, используется v<тег>[+dirty]
:
v1.2.4
v1.2.4+dirty
Если текущий коммит не соответствует тегированной версии, значение устанавливается в <pseudo>[+dirty]
, где pseudo
включает последний тег, текущую дату и хеш коммита:
v1.2.3-0.20240620130020-daa7c0413123
v1.2.3-0.20240620130020-daa7c0413123+dirty
Если информации о VCS нет, отображается (devel), как в Go 1.23.
Флаг -buildvcs=false
предотвращает добавление информации о версии в бинарный файл.
? 50603
Заключение
Go 1.24 включает множество новых возможностей, среди которых слабые указатели, финализаторы и файловый доступ с областью видимости каталога. Значительные усилия были направлены на ускорение работы map
, что является долгожданным улучшением.
Команда Go также явно уделяет внимание опыту разработчиков, предлагая более удобные и безопасные способы написания бенчмарков, тестирования конкурентного кода и работы с пользовательскими инструментами.
Кроме того, приятным дополнением стали криптографические улучшения, такие как SHA-3 и генерация случайного текста.
В целом, отличный релиз!
В заключение рекомендуем Go-разработчикам посетить открытые уроки февраля в Otus:
13 февраля: «Паттерны параллельного программирования».
Узнаете, как эффективно использовать горутины и каналы для написания производительного кода. Покажем, в каких именно сценариях Go оказывается особенно мощным инструментом. Записаться27 февраля: «Управление зависимостями в Go: продвинутые техники и корпоративные паттерны».
Разберём систему модулей в Go: управление зависимостями, прокси, локальный кэш, контроль сумм для безопасности. Также затронем приватные репозитории, Semantic Import Versioning и оптимизацию сборок в компании. Записаться
Весь список бесплатных онлайн-уроков февраля по разработке и не только можно посмотреть в календаре.