Deep Engineering

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", &notFound{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()
}