Deep Engineering

MEASUREMENT

bench/goerrors/practice.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

145 lines
//go:build ignore

// Две задачи урока про ошибки: обе отвечают прогоном.
//
// ЗАДАЧА 1 — ПРЕДСКАЗАТЬ ВЫВОД. Разница между %w и %v, между Is и ==, и то,
// что errors.As ищет по ЦЕПОЧКЕ, а утверждение типа — только на верхнем
// уровне. Все три различия невидимы в исходнике: обёртка выглядит одинаково.
//
// ЗАДАЧА 2 — ОЦЕНИТЬ КРАТНОСТЬ. Во сколько раз дороже обернуть ошибку через
// fmt.Errorf с %w, чем вернуть её как есть. Сравнивается одна и та же
// неудачная операция.
//
// ЗАПУСК:
//
//	go run bench/goerrors/practice.go
package main

import (
	"errors"
	"fmt"
	"runtime"
	"testing"
)

// ---------------------------------------------------------------- задача 1

var errNotFound = errors.New("not found")

// NotFoundError — конкретный тип ошибки, чтобы было что искать через As.
type NotFoundError struct{ Key string }

func (e *NotFoundError) Error() string { return "no key " + e.Key }

func wrapped() error   { return fmt.Errorf("layer: %w", errNotFound) }
func formatted() error { return fmt.Errorf("layer: %v", errNotFound) }

func typedWrapped() error {
	return fmt.Errorf("outer: %w", fmt.Errorf("inner: %w", &NotFoundError{Key: "k"}))
}

func task1() {
	// %w сохраняет цепочку, %v — нет.
	fmt.Println(errors.Is(wrapped(), errNotFound))
	fmt.Println(errors.Is(formatted(), errNotFound))

	// Сравнение через == не разворачивает обёртку.
	fmt.Println(wrapped() == errNotFound)

	// As идёт по всей цепочке, утверждение типа — только по верхнему уровню.
	var target *NotFoundError
	fmt.Println(errors.As(typedWrapped(), &target))
	_, ok := typedWrapped().(*NotFoundError)
	fmt.Println(ok)

	// Unwrap снимает ровно один слой.
	fmt.Println(errors.Unwrap(typedWrapped()))
}

// ---------------------------------------------------------------- задача 2

// ПОЧЕМУ ЗДЕСЬ ГЛУБИНА, А НЕ ЦЕНА ОБЁРТКИ.
//
// Первая редакция этой задачи сравнивала «вернуть ошибку как есть» с
// «обернуть через %w» и получила кратность 249. Число верное и негодное:
// база в ней — возврат готовой ошибки, то есть 0,64 нс, почти ноль. Кратность
// от почти нуля не говорит ни о чём, и оценивать её читателю бессмысленно.
// (Сама цена обёртки осталась, но переехала в internals.go, где рядом стоят
// выделения памяти и абсолютные числа.)
//
// Здесь сравнивается то, что действительно решает при выборе глубины
// оборачивания: errors.Is идёт по цепочке ЗВЕНО ЗА ЗВЕНОМ, и цена проверки
// растёт вместе с числом обёрток. Работа при этом одинаковая — найти одну и ту
// же ошибку в основании.

const deep = 10

var (
	sink    error
	found   bool
	shallow = fmt.Errorf("layer: %w", errNotFound)
	nested  = buildChain(deep)
)

func buildChain(n int) error {
	err := error(errNotFound)
	for i := 0; i < n; i++ {
		err = fmt.Errorf("layer %d: %w", i, err)
	}
	return err
}

func isShallow(b *testing.B) {
	for i := 0; i < b.N; i++ {
		found = errors.Is(shallow, errNotFound)
	}
}

func isDeep(b *testing.B) {
	for i := 0; i < b.N; i++ {
		found = errors.Is(nested, errNotFound)
	}
}

const rounds = 7

func nsPerOp(r testing.BenchmarkResult) float64 {
	return float64(r.T.Nanoseconds()) / float64(r.N)
}

func main() {
	fmt.Println("ЗАДАЧА 1 — предсказать вывод")
	fmt.Println()
	task1()
	fmt.Println()

	one, ten := 0.0, 0.0
	for r := 0; r < rounds; r++ {
		// Круг: оба варианта подряд, чтобы просадка машины досталась обоим.
		oo := nsPerOp(testing.Benchmark(isShallow))
		tt := nsPerOp(testing.Benchmark(isDeep))
		if one == 0 || oo < one {
			one = oo
		}
		if ten == 0 || tt < ten {
			ten = tt
		}
	}

	fmt.Println("ЗАДАЧА 2 — во сколько раз дороже errors.Is на цепочке из десяти")
	fmt.Println()
	fmt.Printf("  errors.Is, одна обёртка     %9.2f нс\n", one)
	fmt.Printf("  errors.Is, десять обёрток   %9.2f нс\n", ten)
	fmt.Printf("  кратность                   %9.2f\n", ten/one)
	fmt.Printf("  она же округлённо           %9.1f\n", ten/one)
	fmt.Println()
	fmt.Printf("  Лучший из %d чередующихся кругов. %s %s/%s\n",
		rounds, runtime.Version(), runtime.GOOS, runtime.GOARCH)
	fmt.Println()
	fmt.Println("  Работа одинаковая: найти одну и ту же ошибку в основании.")
	fmt.Println("  Отличается длина цепочки, и цена растёт вместе с ней —")
	fmt.Println("  errors.Is разворачивает обёртки по одной. Это и есть ответ")
	fmt.Println("  на вопрос «оборачивать ли на каждом уровне»: цена не в")
	fmt.Println("  самой обёртке, а в том, что по ней потом ходят.")
}