MEASUREMENT
bench/goerrors/internals.go
The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.
- Cited in
- /en/interview/golang/errors
The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.
Record of the run
This measurement has no recorded run — only the script.
Script
168 lines//go:build ignore
// Ошибки изнутри: цена обёртки, глубина цепочки и то, что делает errors.Join.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. Совет «оборачивай на каждом уровне» дают все, а цену
// его не называет никто. Цена есть, и она в двух местах сразу: выделение под
// каждую обёртку и проход по цепочке при каждой проверке. Оба измеримы.
//
// Второе, что здесь проверяется, — поведение, которое легко перепутать:
// errors.Is идёт по цепочке, %v цепочку рвёт, а errors.Join делает дерево, а
// не список. Это не вопрос скорости, и подпирать его временем было бы
// подделкой: печатаются наблюдения.
//
// ЗАПУСК:
//
// go run bench/goerrors/internals.go
package main
import (
"errors"
"fmt"
"runtime"
"testing"
)
var (
errBase = errors.New("base")
errOther = errors.New("other")
sink error
flag bool
)
// ------------------------------------------------- блок 1: цена обёртки
func wrapChain(n int) error {
err := error(errBase)
for i := 0; i < n; i++ {
err = fmt.Errorf("layer %d: %w", i, err)
}
return err
}
func cost() {
fmt.Println("ЦЕНА ОБЁРТКИ И ЦЕНА ПРОХОДА ПО ЦЕПОЧКЕ")
fmt.Println("──────────────────────────────────────")
plain := best(func(b *testing.B) {
for i := 0; i < b.N; i++ {
sink = errBase
}
})
one := best(func(b *testing.B) {
for i := 0; i < b.N; i++ {
sink = fmt.Errorf("layer: %w", errBase)
}
})
sentinel := best(func(b *testing.B) {
for i := 0; i < b.N; i++ {
sink = errors.New("static")
}
})
allocWrap := testing.AllocsPerRun(1000, func() { sink = fmt.Errorf("layer: %w", errBase) })
allocPlain := testing.AllocsPerRun(1000, func() { sink = errBase })
fmt.Printf(" вернуть готовую ошибку %9.2f нс выделений %.0f\n", plain, allocPlain)
fmt.Printf(" errors.New на каждый вызов %9.2f нс\n", sentinel)
fmt.Printf(" fmt.Errorf с %%w %9.2f нс выделений %.0f\n", one, allocWrap)
fmt.Println()
fmt.Println(" Обёртка — это форматирование строки плюс выделения под")
fmt.Println(" новую ошибку. Абсолютное число небольшое, и главное про него")
fmt.Println(" другое: платится оно только на ПУТИ ОШИБКИ. На успешном пути")
fmt.Println(" ошибок нет вовсе, а значит, нет и этой цены.")
fmt.Println()
fmt.Printf(" %-24s %12s\n", "глубина цепочки", "errors.Is")
var base float64
for _, n := range []int{1, 5, 10, 20} {
chain := wrapChain(n)
v := best(func(b *testing.B) {
for i := 0; i < b.N; i++ {
flag = errors.Is(chain, errBase)
}
})
if base == 0 {
base = v
}
fmt.Printf(" %-24d %9.2f нс ×%.1f\n", n, v, v/base)
}
fmt.Println()
fmt.Println(" Проверка растёт вместе с глубиной почти линейно: errors.Is")
fmt.Println(" снимает обёртки по одной. Отсюда практическое — оборачивать")
fmt.Println(" стоит там, где добавляется СМЫСЛ, а не на каждом уровне")
fmt.Println(" подряд: лишние звенья платятся при каждой проверке.")
fmt.Println()
}
// ------------------------------------------------- блок 2: что рвёт цепочку
type notFound struct{ key string }
func (e *notFound) Error() string { return "no key " + e.key }
func behaviour() {
fmt.Println("ЧТО СОХРАНЯЕТ ЦЕПОЧКУ, А ЧТО ЕЁ РВЁТ")
fmt.Println("────────────────────────────────────")
withW := fmt.Errorf("ctx: %w", errBase)
withV := fmt.Errorf("ctx: %v", errBase)
fmt.Printf(" %%w errors.Is находит основание %v\n", errors.Is(withW, errBase))
fmt.Printf(" %%v errors.Is находит основание %v\n", errors.Is(withV, errBase))
fmt.Printf(" %%w Unwrap снимает один слой %v\n", errors.Unwrap(withW))
fmt.Printf(" %%v Unwrap возвращает %v\n", errors.Unwrap(withV))
fmt.Println()
// errors.Join: несколько ошибок в одной, и Is находит любую.
joined := errors.Join(errBase, errOther)
fmt.Printf(" Join, находит первую %v\n", errors.Is(joined, errBase))
fmt.Printf(" Join, находит вторую %v\n", errors.Is(joined, errOther))
fmt.Printf(" Join, Unwrap одним значением %v\n", errors.Unwrap(joined))
fmt.Println()
fmt.Println(" Последняя строка — та, где ошибаются: у Join нет обёртки")
fmt.Println(" «одна над одной», и Unwrap с одним результатом для него не")
fmt.Println(" определён. Он реализует Unwrap() []error — дерево, а не")
fmt.Println(" список, — и разворачивать его надо через errors.Is/As.")
fmt.Println()
// As идёт по цепочке, утверждение типа — нет.
deep := fmt.Errorf("a: %w", fmt.Errorf("b: %w", ¬Found{key: "k"}))
var target *notFound
_, direct := deep.(*notFound)
fmt.Printf(" As находит тип в глубине %v\n", errors.As(deep, &target))
fmt.Printf(" утверждение типа на верхнем слое %v\n", direct)
fmt.Println()
fmt.Println(" Разница между этими двумя строками — самая частая ошибка в")
fmt.Println(" обработке: код с утверждением типа работает, пока никто не")
fmt.Println(" добавил обёртку уровнем выше, и молча перестаёт работать")
fmt.Println(" после первой же такой правки.")
fmt.Println()
}
// ------------------------------------------------------------ утилиты
const rounds = 7
func nsPerOp(r testing.BenchmarkResult) float64 {
return float64(r.T.Nanoseconds()) / float64(r.N)
}
func best(f func(*testing.B)) float64 {
out := 0.0
for r := 0; r < rounds; r++ {
v := nsPerOp(testing.Benchmark(f))
if out == 0 || v < out {
out = v
}
}
return out
}
func main() {
fmt.Printf("%s %s/%s | ошибки изнутри\n\n",
runtime.Version(), runtime.GOOS, runtime.GOARCH)
cost()
behaviour()
}