From 7360eea2a7d4bdf17691c92a64b2f251fdd6515e Mon Sep 17 00:00:00 2001 From: Mikhail Knyazhev Date: Sun, 27 Sep 2026 17:17:19 +0300 Subject: [PATCH 1/4] change --- .github/workflows/ci.yml | 11 +- .golangci.yml | 196 +++++++++++++++- .lic.yaml | 2 +- AGENTS.md | 25 ++ LICENSE | 2 +- Makefile | 2 +- README.md | 68 +++++- errors.go | 31 +-- errors_test.go | 217 ++++++++++-------- go.mod | 2 +- operation.go | 64 ++++-- queue.go | 4 +- queue_test.go | 2 +- skills/go-errors-usage/SKILL.md | 25 ++ skills/go-errors-usage/agents/openai.yaml | 4 + skills/go-errors-usage/examples/basic/main.go | 46 ++++ skills/go-errors-usage/examples/queue/main.go | 32 +++ skills/go-errors-usage/examples/trace/main.go | 23 ++ .../references/api_reference.md | 24 ++ .../references/wrapping-and-inspection.md | 42 ++++ trace.go | 50 ++++ types.go | 4 +- 22 files changed, 724 insertions(+), 152 deletions(-) create mode 100644 AGENTS.md create mode 100644 skills/go-errors-usage/SKILL.md create mode 100644 skills/go-errors-usage/agents/openai.yaml create mode 100644 skills/go-errors-usage/examples/basic/main.go create mode 100644 skills/go-errors-usage/examples/queue/main.go create mode 100644 skills/go-errors-usage/examples/trace/main.go create mode 100644 skills/go-errors-usage/references/api_reference.md create mode 100644 skills/go-errors-usage/references/wrapping-and-inspection.md create mode 100644 trace.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a76bd9e..075c788 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,23 +1,22 @@ - name: CI on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] jobs: build: runs-on: ubuntu-latest strategy: matrix: - go: [ '1.24' ] + go: ["1.26"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v7 with: go-version: ${{ matrix.go }} diff --git a/.golangci.yml b/.golangci.yml index 60f1183..759a38d 100755 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,9 +1,9 @@ version: "2" run: - go: "1.24" + go: "1.26" timeout: 5m - tests: false + tests: true issues-exit-code: 1 modules-download-mode: readonly allow-parallel-runners: true @@ -12,7 +12,7 @@ issues: max-issues-per-linter: 0 max-same-issues: 0 new: false - fix: false + fix: true output: formats: @@ -27,6 +27,7 @@ formatters: enable: - gofmt - goimports + - gofumpt linters: settings: @@ -46,7 +47,160 @@ linters: - G306 - G501 - G505 + tagliatelle: + case: + rules: + json: snake # JSON: snake_case (user_id) + yaml: snake # YAML: snake_case + xml: camel # XML: camelCase + mapstructure: snake # mapstructure: snake_case + env: upperSnake # ENV: UPPER_SNAKE_CASE + varnamelen: + min-name-length: 2 # Минимальная длина имени + max-distance: 5 # i, j, k допустимы в scope <= 5 строк + ignore-names: + - err # err — идиоматично + - ok # ok — идиоматично + - id # id — часто используется + - db # db — часто используется + - tx # tx — транзакция + - wg # wg — WaitGroup + - mu # mu — mutex + - rw # rw — RWMutex + - ch # ch — channel + - fn # fn — function + - sb # sb — strings.Builder + - ctx # ctx — context + - q # q — querier + - r # r — repo / reader + - s # s — strategy / service + - f # f — filter + - a # a — left operand (сравнение) + - b # b — right operand (сравнение) + ignore-type-assert-ok: true # Игнорировать v, ok := x.(T) + ignore-map-index-ok: true # Игнорировать v, ok := m[k] + ignore-chan-recv-ok: true # Игнорировать v, ok := <-ch + ignore-decls: + - i int # for i := ... + - j int # вложенные циклы + - n int # количество + - t testing.T # тесты + - b testing.B # бенчмарки + - r *http.Request # HTTP handler + - w http.ResponseWriter # HTTP handler + - c *gin.Context # Gin context + - c echo.Context # Echo context + - s *Server # конструктор Server + - m *metrics # конструктор metrics + revive: + severity: warning + rules: + # ------------------------------------------------------------------------- + # Предотвращение багов + # ------------------------------------------------------------------------- + - name: atomic # Проверяет правильное использование sync/atomic + - name: range-val-in-closure # Захват переменной цикла в замыкании + - name: range-val-address # Взятие адреса переменной цикла + - name: unreachable-code # Недостижимый код после return/panic + - name: unchecked-type-assertion # Type assertion без проверки ok + - name: datarace # Потенциальные data races + - name: identical-branches # Одинаковые ветки if/else + - name: defer # Проблемы с defer (в циклах, результат) + - name: call-to-gc # Явные вызовы runtime.GC() + - name: waitgroup-by-value # WaitGroup передан по значению + + # ------------------------------------------------------------------------- + # Обработка ошибок — Go proverb: "Don't just check errors, handle them gracefully" + # ------------------------------------------------------------------------- + - name: error-strings # Ошибки не должны начинаться с большой буквы + - name: error-return # error должен быть последним возвращаемым значением + - name: errorf # Использовать fmt.Errorf вместо errors.New + fmt.Sprintf + - name: unhandled-error # Необработанные ошибки + arguments: + - "fmt.Print" + - "fmt.Printf" + - "fmt.Println" + + # ------------------------------------------------------------------------- + # Сложность — Go proverb: "Clear is better than clever" + # ------------------------------------------------------------------------- + - name: cognitive-complexity + arguments: [15] # Cognitive complexity <= 15 + - name: cyclomatic + arguments: [10] # Cyclomatic complexity <= 10 + - name: function-result-limit + arguments: [3] # Максимум 3 возвращаемых значения + - name: argument-limit + arguments: [5] # Максимум 5 аргументов функции + + # ------------------------------------------------------------------------- + # Чистота кода — Go proverb: "A little copying is better than a little dependency" + # ------------------------------------------------------------------------- + - name: indent-error-flow # if err != nil { return } вместо else + - name: early-return # Ранний возврат вместо вложенности + - name: superfluous-else # Лишний else после return + - name: if-return # Упрощение if/return + - name: empty-block # Пустые блоки кода + - name: unnecessary-stmt # Ненужные операторы + - name: redundant-import-alias # import pkg "pkg" — лишний алиас + - name: confusing-results # Запутанные возвращаемые значения + - name: bool-literal-in-expr # if x == true → if x + - name: constant-logical-expr # Константные логические выражения + - name: modifies-parameter # Модификация параметров функции + - name: modifies-value-receiver # Модификация value receiver (бесполезно) + - name: redefines-builtin-id # Переопределение встроенных идентификаторов + - name: string-of-int # string(int) — частая ошибка + - name: time-equal # time.Time сравнение через == + - name: unconditional-recursion # Безусловная рекурсия (бесконечный цикл) + - name: useless-break # break в конце case (Go делает это автоматически) + + # ------------------------------------------------------------------------- + # Хорошие практики — Go proverbs + # ------------------------------------------------------------------------- + - name: context-as-argument # context.Context первым аргументом + - name: context-keys-type # Ключи контекста должны быть типизированы + - name: var-declaration # var x = 1 → x := 1 + - name: blank-imports # Запрет blank imports кроме main/test + - name: dot-imports # Запрет dot imports + - name: unexported-return # Публичная функция возвращает приватный тип + - name: exported # Экспортируемые идентификаторы должны быть задокументированы + arguments: + - "checkPrivateReceivers" + - "disableStutteringCheck" + + # ------------------------------------------------------------------------- + # Именование — Go proverb: "Good naming is like good coding: concise" + # ------------------------------------------------------------------------- + - name: var-naming + arguments: + - [ + "ID", + "URL", + "API", + "HTTP", + "JSON", + "XML", + "DB", + "SQL", + "UUID", + "UID", + "GUID", + "TTL", + "TCP", + "UDP", + "IP", + "RPC", + "QPS", + "EOF", + ] + - name: package-comments # Пакеты должны иметь комментарии + - name: receiver-naming # Имена receiver (r, s, c, не this/self) exclusions: + rules: + - path: pki/internal/xocsp/ocsp.go + linters: + - revive + - nestif paths: - vendors/ default: none @@ -66,3 +220,39 @@ linters: - errorlint - bodyclose - gosec + - nilerr + - nilnesserr + - nilnil + - bidichk + - contextcheck + - fatcontext + - makezero + - forcetypeassert + - unconvert + - copyloopvar + - prealloc + - perfsprint + - gocritic + - goconst + - mnd + - revive + - predeclared + - reassign + - recvcheck + - asciicheck + - importas + - durationcheck + - tparallel + - thelper + - usetesting + - musttag + - errchkjson + - tagalign + - usestdlibvars + - nestif + - mirror + - whitespace + - decorder + - nonamedreturns + - inamedparam + - testpackage diff --git a/.lic.yaml b/.lic.yaml index 2387d32..737072f 100644 --- a/.lic.yaml +++ b/.lic.yaml @@ -1,3 +1,3 @@ -author: Mikhail Knyazhev +author: Mikhail Knyazhev lic_short: "BSD 3-Clause" lic_file: LICENSE diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4c352aa --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,25 @@ +# Agent instructions + +## Project + +- This repository is the Go module `go.osspkg.com/errors`, a single package at the repository root. It targets Go 1.26. +- Keep the public error API compatible. `Trace` is exported; `Wrap` with multiple inputs must preserve each input for standard `errors.Is` and `errors.As` while retaining the colon-separated error text and legacy `Cause` behavior. +- Tests for the public API use the external package `errors_test`. + +## Commands + +Run commands from the repository root. + +- `make lint` runs `goppy lint`, which performs Go module tidy/download and formatting before `golangci-lint` and `govulncheck`. It can change files; inspect `git status` and the diff afterward. +- `make tests` runs `goppy test`. +- `make build` runs `goppy build --arch=amd64`. +- CI runs `make ci`. This target also installs `goppy@latest`, runs `goppy setup-lib` and the license target, then lint, tests, and build. Use it when the full CI sequence and local setup are intended. + +## Project memory + +For non-trivial work, use the Chroma collection `chat_go-errors_memory`: + +1. Ensure the collection exists before reading or writing. If missing, list collections and create this exact collection with the default embedding configuration. +2. Query it with a concise semantic description of the current task before making design decisions. +3. After the work, add a concise document only for a durable project decision or lesson. Query related memories first and update an existing document instead of duplicating it. +4. Never store secrets, transcripts, or temporary command output. Memory supplements the current source and tests; it does not override them. diff --git a/LICENSE b/LICENSE index 93caddc..d6c3bf3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2024-2025, Mikhail Knyazhev +Copyright (c) 2024-2026, Mikhail Knyazhev Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/Makefile b/Makefile index 03d4159..c04cf9f 100755 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ SHELL=/bin/bash .PHONY: install install: - go install go.osspkg.com/goppy/v2/cmd/goppy@latest + go install go.osspkg.com/goppy/v3/cmd/goppy@latest goppy setup-lib .PHONY: lint diff --git a/README.md b/README.md index cd79269..050b06f 100644 --- a/README.md +++ b/README.md @@ -1 +1,67 @@ -# go-errors \ No newline at end of file +# go-errors + +[![CI](https://github.com/osspkg/go-errors/actions/workflows/ci.yml/badge.svg)](https://github.com/osspkg/go-errors/actions/workflows/ci.yml) + +A small Go library for creating errors, adding context, retaining causes, and inspecting error chains. It has no third-party runtime dependencies and requires Go 1.26 or newer. + +## Installation + +```sh +go get go.osspkg.com/errors +``` + +## Quick start + +```go +package main + +import ( + "errors" + "fmt" + + errpkg "go.osspkg.com/errors" +) + +var errConnectionRefused = errors.New("connection refused") + +func main() { + err := errpkg.Wrapf(errConnectionRefused, "dial %s", "db.example:5432") + if errors.Is(err, errConnectionRefused) { + fmt.Println(err) + } +} +``` + +## API + +| API | Purpose | +| --- | --- | +| `New(message string) error` | Creates an error with a message. | +| `Wrapf(cause error, message string, args ...any) error` | Adds formatted context to one cause; returns `nil` when `cause` is `nil`. | +| `Wrap(errors ...error) error` | Combines non-nil errors, joining their messages with `: `. Every input remains discoverable through `errors.Is` and `errors.As`; returns `nil` if there are no non-nil inputs. | +| `Trace(cause error, message string, args ...any) error` | Adds formatted context and a runtime stack trace; returns `nil` when `cause` is `nil`. | +| `Queue(calls ...func() error) error` | Calls functions in order and returns the first error, or `nil` if all succeed. Callbacks must be non-nil. | +| `Unwrap(err error) error` | Returns one underlying error from an `Unwrapper`; returns `nil` for nil errors and multi-cause errors. | +| `Cause(err error) error` | Follows the legacy `Cause() error` chain and returns its terminal error. | +| `Is(err, target error) bool` | Reports whether `err` or an error in its chain matches `target`. | +| `As(err error, target any) bool` | Finds the first error in the chain assignable to `target`, following the standard `errors.As` contract. | +| `Causer` | Interface for errors exposing `Cause() error`. | +| `Unwrapper` | Interface for errors exposing `Unwrap() error`. | + +`Cause` follows legacy `Cause()` methods; it does not walk standard-library `%w` wrappers. Use `Is` or `As` to inspect those chains. For a `Wrap` call with several errors, `Cause` retains its legacy behavior and returns the final input, while `Is` and `As` inspect all inputs. + +## Development + +The repository uses Go 1.26 and `goppy` for its Makefile targets. + +```sh +make lint +make tests +make build +``` + +CI runs `make ci`, which installs `goppy@latest`, runs `goppy setup-lib` and license generation, then runs lint, tests, and build. + +## License + +BSD 3-Clause. See [LICENSE](LICENSE). diff --git a/errors.go b/errors.go index 95ced73..4453cc7 100644 --- a/errors.go +++ b/errors.go @@ -1,45 +1,34 @@ /* - * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ +// Package errors provides error construction, wrapping, and inspection helpers. package errors -import "strings" - type errorEntity struct { cause error message string + trace string } +// New returns an error with the provided message. func New(message string) error { return &errorEntity{message: message} } func (v *errorEntity) Error() string { - if v.cause == nil && len(v.message) == 0 { - return "" - } - - var b strings.Builder - - var mw bool - if len(v.message) > 0 { - b.WriteString(v.message) - mw = true - } - + message := v.message if v.cause != nil { - if mw { - b.WriteString(": ") + if message != "" { + message += ": " } - - b.WriteString(v.cause.Error()) + message += v.cause.Error() } - - return b.String() + return message + v.trace } +// Cause returns the underlying cause. func (v *errorEntity) Cause() error { return v.cause } diff --git a/errors_test.go b/errors_test.go index 0fdb8f8..1c00ec3 100644 --- a/errors_test.go +++ b/errors_test.go @@ -1,14 +1,17 @@ /* - * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ -package errors +package errors_test import ( e "errors" "fmt" + "strings" "testing" + + pkgerrors "go.osspkg.com/errors" ) func TestUnit_New(t *testing.T) { @@ -22,13 +25,13 @@ func TestUnit_New(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := New(tt.message) + err := pkgerrors.New(tt.message) if (err != nil) != tt.wantErr { - t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("pkgerrors.New() error = %v, wantErr %v", err, tt.wantErr) return } if err.Error() != tt.want { - t.Errorf("New() error = %v, want %v", err.Error(), tt.want) + t.Errorf("pkgerrors.New() error = %v, want %v", err.Error(), tt.want) return } }) @@ -42,57 +45,35 @@ func TestUnit_Wrap(t *testing.T) { want string wantErr bool }{ + {name: "no inputs"}, { - name: "Case1", - msgs: nil, - want: "", - wantErr: false, - }, - { - name: "Case2", - msgs: []error{New("hello"), e.New("world")}, + name: "two errors", + msgs: []error{pkgerrors.New("hello"), e.New("world")}, want: "hello: world", wantErr: true, }, { - name: "Case3", - msgs: []error{New("err1"), e.New("err2"), nil, e.New("err3")}, + name: "skip nil inputs", + msgs: []error{pkgerrors.New("err1"), e.New("err2"), nil, e.New("err3")}, want: "err1: err2: err3", wantErr: true, }, { - name: "Case4", - msgs: []error{Wrapf(New("err1"), "err1 message"), - Wrapf(e.New("err2"), "err2 message"), - Wrapf(e.New("err3"), "err3 message")}, + name: "wrapped errors", + msgs: []error{ + pkgerrors.Wrapf(pkgerrors.New("err1"), "err1 message"), + pkgerrors.Wrapf(e.New("err2"), "err2 message"), + pkgerrors.Wrapf(e.New("err3"), "err3 message"), + }, want: "err1 message: err1: err2 message: err2: err3 message: err3", wantErr: true, }, - { - name: "Case5", - msgs: []error{nil, nil, nil}, - want: "", - wantErr: false, - }, + {name: "all nil", msgs: []error{nil, nil, nil}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := Wrap(tt.msgs...) - - if tt.wantErr { - if err == nil { - t.Errorf("Wrap() error = %v, wantErr %v", err, tt.wantErr) - return - } - - if err.Error() != tt.want { - t.Errorf("Wrap() error = %v, want %v", err.Error(), tt.want) - } - } else { - if err != nil { - t.Errorf("Wrap() error = %v, wantErr %v", err, tt.wantErr) - } - } + err := pkgerrors.Wrap(tt.msgs...) + assertErrorResult(t, err, tt.want, tt.wantErr) }) } } @@ -142,13 +123,13 @@ func TestUnit_Wrapf(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := Wrapf(tt.args.cause, tt.args.message, tt.args.args...) + err := pkgerrors.Wrapf(tt.args.cause, tt.args.message, tt.args.args...) if (err != nil) != tt.wantErr { - t.Errorf("Wrapf() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("pkgerrors.Wrapf() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantErr && err.Error() != tt.want { - t.Errorf("Wrapf() error = %v, want %v", err.Error(), tt.want) + t.Errorf("pkgerrors.Wrapf() error = %v, want %v", err.Error(), tt.want) return } }) @@ -156,63 +137,87 @@ func TestUnit_Wrapf(t *testing.T) { } func TestUnit_CauseUnwrap(t *testing.T) { - type fields struct { - cause error - message string - args []interface{} - } tests := []struct { name string - fields fields + cause error + message string want string wantErr bool }{ { - name: "Case1", - fields: fields{ - cause: e.New("err1"), - message: "context", - }, + name: "with cause", + cause: e.New("err1"), + message: "context", want: "err1", wantErr: true, }, - { - name: "Case2", - fields: fields{ - cause: nil, - message: "context", - }, - want: "err1", - wantErr: false, - }, + {name: "nil cause", message: "context"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - v := Wrapf(tt.fields.cause, tt.fields.message, tt.fields.args...) - err := Cause(v) - if (err != nil) != tt.wantErr { - t.Errorf("Cause() error = %v, wantErr %v", err, tt.wantErr) - return - } - if tt.wantErr && err.Error() != tt.want { - t.Errorf("Cause() error = %v, want %v", err.Error(), tt.want) - return - } - err = Unwrap(v) - if (err != nil) != tt.wantErr { - t.Errorf("Unwrap() error = %v, wantErr %v", err, tt.wantErr) - return - } - if tt.wantErr && err.Error() != tt.want { - t.Errorf("Unwrap() error = %v, want %v", err.Error(), tt.want) - return - } + err := pkgerrors.Wrapf(tt.cause, "%s", tt.message) + assertErrorResult(t, pkgerrors.Cause(err), tt.want, tt.wantErr) + assertErrorResult(t, pkgerrors.Unwrap(err), tt.want, tt.wantErr) }) } } +func assertErrorResult(t *testing.T, got error, want string, wantErr bool) { + t.Helper() + if (got != nil) != wantErr { + t.Errorf("error = %v, wantErr %v", got, wantErr) + return + } + if wantErr && got.Error() != want { + t.Errorf("error = %v, want %v", got.Error(), want) + } +} + +type typedTestError struct{} + +func (*typedTestError) Error() string { return "typed" } + +func TestWrapPreservesAllCauses(t *testing.T) { + first := e.New("first") + second := e.New("second") + + err := pkgerrors.Wrap(first, second) + if got, want := err.Error(), "first: second"; got != want { + t.Fatalf("pkgerrors.Wrap() = %q, want %q", got, want) + } + if !e.Is(err, first) { + t.Error("pkgerrors.Wrap() should retain the first cause") + } + if !e.Is(err, second) { + t.Error("pkgerrors.Wrap() should retain the second cause") + } + + var got *typedTestError + if !pkgerrors.As(pkgerrors.Wrap(first, &typedTestError{}), &got) || got == nil { + t.Error("pkgerrors.As() should find a typed error in a combined error") + } +} + +func TestTrace(t *testing.T) { + if pkgerrors.Trace(nil, "context") != nil { + t.Fatal("pkgerrors.Trace(nil, ...) should return nil") + } + + cause := e.New("cause") + err := pkgerrors.Trace(cause, "context") + if err == nil { + t.Fatal("pkgerrors.Trace() = nil, want error") + } + if !strings.Contains(err.Error(), "[trace]") { + t.Fatalf("pkgerrors.Trace() error %q does not contain a trace", err) + } + if !e.Is(err, cause) { + t.Error("pkgerrors.Trace() should preserve the cause") + } +} + func TestUnit_Is(t *testing.T) { - err0 := New("test") + err0 := pkgerrors.New("test") type args struct { err error target error @@ -223,32 +228,48 @@ func TestUnit_Is(t *testing.T) { want bool }{ {name: "Case1", args: args{err: err0, target: err0}, want: true}, - {name: "Case2", args: args{err: Wrapf(err0, "ttt"), target: err0}, want: true}, - {name: "Case3", args: args{err: New("hello"), target: err0}, want: false}, + {name: "Case2", args: args{err: pkgerrors.Wrapf(err0, "ttt"), target: err0}, want: true}, + {name: "Case3", args: args{err: pkgerrors.New("hello"), target: err0}, want: false}, {name: "Case4", args: args{err: nil, target: err0}, want: false}, - {name: "Case5", args: args{err: New("hello"), target: nil}, want: false}, + {name: "Case5", args: args{err: pkgerrors.New("hello"), target: nil}, want: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := Is(tt.args.err, tt.args.target); got != tt.want { - t.Errorf("Is() = %v, want %v", got, tt.want) + if got := pkgerrors.Is(tt.args.err, tt.args.target); got != tt.want { + t.Errorf("pkgerrors.Is() = %v, want %v", got, tt.want) } }) } } func TestUnit_As(t *testing.T) { - err0 := New("err0") - err1 := fmt.Errorf("err1") + typed := &typedTestError{} + err0 := pkgerrors.Wrapf(typed, "context") + err1 := e.New("err1") - var err2 *errorEntity - if !As(err0, &err2) { - t.Errorf("As() error = %v, wantErr %v", err0, err2) - return + var got *typedTestError + if !pkgerrors.As(err0, &got) || got != typed { + t.Errorf("As() = %v, want %v", got, typed) + } + if pkgerrors.As(err1, &got) { + t.Errorf("As(%v) = true, want false", err1) } +} - if As(err1, &err2) { - t.Errorf("As() error = %v, wantErr %v", err1, err2) - return +var benchmarkErrorSink error + +func Benchmark_PkgError(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + err := pkgerrors.New("test") + benchmarkErrorSink = pkgerrors.Wrapf(err, "Hello %d", 1) + } +} + +func Benchmark_SdkError(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + err := e.New("test") + benchmarkErrorSink = fmt.Errorf("hello %d %w", 1, err) } } diff --git a/go.mod b/go.mod index 7346093..f6c6d90 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module go.osspkg.com/errors -go 1.24 +go 1.26 diff --git a/operation.go b/operation.go index 140f734..1b0074e 100644 --- a/operation.go +++ b/operation.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ @@ -10,6 +10,8 @@ import ( "fmt" ) +// Wrapf returns nil when cause is nil. Otherwise it prefixes cause with a +// formatted message and preserves cause for errors.Is and errors.As. func Wrapf(cause error, message string, args ...interface{}) error { if cause == nil { return nil @@ -27,30 +29,30 @@ func Wrapf(cause error, message string, args ...interface{}) error { return err } +// Wrap combines non-nil errors into one error. Its message joins the input +// messages with ": ", and errors.Is and errors.As can inspect every input. +// Wrap returns nil when all inputs are nil. func Wrap(messages ...error) error { - if len(messages) == 0 { - return nil - } - - var err error - + causes := make([]error, 0, len(messages)) for _, msg := range messages { if msg == nil { continue } - if err == nil { - err = &errorEntity{cause: msg} - continue - } - err = &errorEntity{ - cause: msg, - message: err.Error(), - } + causes = append(causes, msg) } - return err + switch len(causes) { + case 0: + return nil + case 1: + return &errorEntity{cause: causes[0]} + default: + return &joinedError{causes: causes} + } } +// Unwrap returns the single wrapped error when err implements Unwrapper. +// It returns nil for nil errors and errors with multiple causes. func Unwrap(err error) error { if err == nil { return nil @@ -63,6 +65,9 @@ func Unwrap(err error) error { return nil } +// Cause follows legacy Cause() methods and returns the first error that does +// not implement Causer. For errors wrapped with %w, use Is or As to inspect +// the chain. func Cause(err error) error { if err == nil { return nil @@ -78,10 +83,37 @@ func Cause(err error) error { } } +// Is reports whether err or any error in its chain matches target. func Is(err, target error) bool { return e.Is(err, target) } +// As finds the first error in err's chain assignable to target and stores it +// there, following the standard errors.As contract. func As(err error, target any) bool { return e.As(err, target) } + +type joinedError struct { + causes []error +} + +func (v *joinedError) Error() string { + message := "" + for _, cause := range v.causes { + if message != "" { + message += ": " + } + message += cause.Error() + } + return message +} + +func (v *joinedError) Unwrap() []error { + return append([]error(nil), v.causes...) +} + +// Cause returns the final error passed to Wrap. +func (v *joinedError) Cause() error { + return v.causes[len(v.causes)-1] +} diff --git a/queue.go b/queue.go index cac3b69..21f054a 100644 --- a/queue.go +++ b/queue.go @@ -1,10 +1,12 @@ /* - * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ package errors +// Queue calls each function in order and returns the first non-nil error. +// It returns nil when every function succeeds or calls is empty. func Queue(calls ...func() error) error { for _, call := range calls { if err := call(); err != nil { diff --git a/queue_test.go b/queue_test.go index 81cb766..3875ce3 100644 --- a/queue_test.go +++ b/queue_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/skills/go-errors-usage/SKILL.md b/skills/go-errors-usage/SKILL.md new file mode 100644 index 0000000..2bcfcf9 --- /dev/null +++ b/skills/go-errors-usage/SKILL.md @@ -0,0 +1,25 @@ +--- +name: go-errors-usage +description: Use the go.osspkg.com/errors library when creating, wrapping, combining, tracing, or inspecting errors in Go code. Apply when this repository's error API or its compatibility behavior matters; use general Go error guidance for code that does not depend on this package. +--- + +# go-errors usage + +Use this skill when implementing or reviewing call sites of `go.osspkg.com/errors`. +Start with the closest API reference, then use a runnable example for the relevant pattern. + +- Read [API reference](references/api-reference.md) for signatures, nil behavior, and compatibility details. +- Read [wrapping and inspection](references/wrapping-and-inspection.md) when choosing how to preserve causes or inspect an error chain. +- Browse [`examples/`](examples/) for complete Go programs. Run one from the repository root with `go run ./skills/go-errors-usage/examples/`. + +Keep standard error inspection semantics intact. Prefer `errors.Is` and `errors.As` (standard library or this package's forwarding helpers) over comparing error strings. Add context with `Wrapf` only when a non-nil cause exists; it returns nil for a nil cause. Use `Wrap` to combine independent errors when every cause must remain discoverable. + +Preserve these API contracts: + +- `Trace` is public and captures a runtime stack trace when called. +- Multi-error `Wrap` keeps the existing colon-separated message and exposes each input to `errors.Is` and `errors.As`; legacy `Cause` returns its final input. +- The package-level `Unwrap` handles a single `Unwrap() error`; it returns nil for a multi-error `Unwrap() []error`. +- `Cause` follows legacy `Cause() error` methods. It does not walk standard `%w` wrappers. +- `Queue` calls non-nil callbacks in order and stops at the first error. + +The package name conflicts with the standard library package name. In examples that import both, alias the project package (for example, `pkgerrors`) and leave the standard package as `errors`. diff --git a/skills/go-errors-usage/agents/openai.yaml b/skills/go-errors-usage/agents/openai.yaml new file mode 100644 index 0000000..6e53db4 --- /dev/null +++ b/skills/go-errors-usage/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "go-errors usage" + short_description: "Use go.osspkg.com/errors safely and idiomatically" + default_prompt: "Use the go-errors library APIs according to repository contracts and examples." diff --git a/skills/go-errors-usage/examples/basic/main.go b/skills/go-errors-usage/examples/basic/main.go new file mode 100644 index 0000000..0f1df9d --- /dev/null +++ b/skills/go-errors-usage/examples/basic/main.go @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +// Package main demonstrates wrapping, combining, and inspecting errors. +package main + +import ( + "errors" + "fmt" + + pkgerrors "go.osspkg.com/errors" +) + +const ( + accountID = 42 + statusBadGateway = 502 +) + +var errUnavailable = errors.New("service unavailable") + +type responseError struct { + status int +} + +func (e *responseError) Error() string { + return fmt.Sprintf("unexpected status %d", e.status) +} + +func fetch() error { + return pkgerrors.Wrapf(errUnavailable, "fetch account %d", accountID) +} + +func main() { + err := fetch() + if errors.Is(err, errUnavailable) { + fmt.Println("fetch failed:", err) + } + + statusErr := &responseError{status: statusBadGateway} + combined := pkgerrors.Wrap(err, statusErr) + var target *responseError + fmt.Println("contains status error:", errors.As(combined, &target)) + fmt.Println("contains unavailable error:", errors.Is(combined, errUnavailable)) +} diff --git a/skills/go-errors-usage/examples/queue/main.go b/skills/go-errors-usage/examples/queue/main.go new file mode 100644 index 0000000..7916cfe --- /dev/null +++ b/skills/go-errors-usage/examples/queue/main.go @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +// Package main demonstrates sequential callbacks with Queue. +package main + +import ( + "fmt" + + pkgerrors "go.osspkg.com/errors" +) + +func main() { + if err := pkgerrors.Queue( + func() error { + fmt.Println("validate configuration") + return nil + }, + func() error { + fmt.Println("connect to database") + return pkgerrors.New("connection refused") + }, + func() error { + fmt.Println("this callback is not called") + return nil + }, + ); err != nil { + fmt.Println("startup stopped:", err) + } +} diff --git a/skills/go-errors-usage/examples/trace/main.go b/skills/go-errors-usage/examples/trace/main.go new file mode 100644 index 0000000..6f20c83 --- /dev/null +++ b/skills/go-errors-usage/examples/trace/main.go @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +// Package main demonstrates capturing a runtime error trace. +package main + +import ( + "fmt" + + pkgerrors "go.osspkg.com/errors" +) + +func loadConfig() error { + return pkgerrors.Trace(pkgerrors.New("file not found"), "load application config") +} + +func main() { + if err := loadConfig(); err != nil { + fmt.Println(err) + } +} diff --git a/skills/go-errors-usage/references/api_reference.md b/skills/go-errors-usage/references/api_reference.md new file mode 100644 index 0000000..46f8eea --- /dev/null +++ b/skills/go-errors-usage/references/api_reference.md @@ -0,0 +1,24 @@ +# API reference + +The import path is `go.osspkg.com/errors`; the package name is `errors`. The module targets Go 1.26 and has no third-party runtime dependencies. + +| API | Behavior | +| --- | --- | +| `New(message string) error` | Creates an error containing `message`. The returned error is non-nil even if the message is empty. | +| `Wrapf(cause error, message string, args ...any) error` | Returns nil for a nil cause. Otherwise stores the literal message when there are no args, or formats it with `fmt.Sprintf` when args are present, then wraps the cause. | +| `Wrap(causes ...error) error` | Skips nil interface values, returns nil if none remain, and joins their messages with `: `. With multiple causes, each remains visible to standard `errors.Is` and `errors.As`. | +| `Trace(cause error, message string, args ...any) error` | Like `Wrapf`, and appends captured runtime frames in `[trace] function:line` form. Returns nil for a nil cause. | +| `Queue(calls ...func() error) error` | Executes callbacks sequentially, returning the first non-nil error. Empty input returns nil. A nil callback panics when reached. | +| `Unwrap(err error) error` | Calls `Unwrap() error` once when implemented. Returns nil for nil input, unsupported errors, and multi-cause errors. | +| `Cause(err error) error` | Follows the legacy `Cause() error` interface until reaching an error without it. For a multi-error returned by `Wrap`, this remains the last non-nil input. | +| `Is(err, target error) bool` | Forwards to the standard `errors.Is`. | +| `As(err error, target any) bool` | Forwards to the standard `errors.As`. | +| `Causer` | Interface containing `Cause() error`. | +| `Unwrapper` | Interface containing `Unwrap() error` for single-cause errors. | + +## Interoperation notes + +- `Wrapf` and `Trace` create errors that implement both `Cause() error` and `Unwrap() error`. +- Multi-error `Wrap` implements `Unwrap() []error`; standard `errors.Is` and `errors.As` traverse all causes. The package helper `Unwrap` intentionally supports only the single-error interface, like the standard `errors.Unwrap` helper. +- `Cause` does not follow `Unwrap() error`. For standard `%w` wrappers and joined errors, inspect with `Is` or `As`. +- `Trace` captures frames during the call, so invoke it at the point where the trace should begin. diff --git a/skills/go-errors-usage/references/wrapping-and-inspection.md b/skills/go-errors-usage/references/wrapping-and-inspection.md new file mode 100644 index 0000000..5a361b7 --- /dev/null +++ b/skills/go-errors-usage/references/wrapping-and-inspection.md @@ -0,0 +1,42 @@ +# Wrapping and inspection patterns + +## Add operation context + +Use `Wrapf` when an underlying error exists and callers may need to inspect it: + +```go +return pkgerrors.Wrapf(err, "read account %q", accountID) +``` + +A nil cause produces nil, even if a context message was supplied. Do not use `Wrapf` to create a standalone error; use `New` or `fmt.Errorf` for that case. + +## Inspect by identity or type + +Use `errors.Is` for sentinels and `errors.As` for typed errors. The package also exports `Is` and `As` as direct forwards to the standard library. + +```go +if errors.Is(err, ErrNotFound) { + // Handle absence. +} + +var parseErr *ParseError +if errors.As(err, &parseErr) { + // Use fields from ParseError. +} +``` + +Do not compare `Error()` strings to identify error kinds. + +## Combine independent failures + +Use `pkgerrors.Wrap(errA, errB)` when both errors must be retained. Its string is the colon-separated input text; `errors.Is` and `errors.As` inspect every cause. `pkgerrors.Cause` preserves the legacy contract and returns the last non-nil input, so it is not a substitute for inspecting all causes. + +For cleanup that should stop at the first failure, use `Queue` instead of collecting every error. `Queue` is sequential and stops immediately after a callback returns an error. + +## Legacy Cause versus modern wrapping + +`Cause` follows only `Cause() error`. It does not traverse standard `fmt.Errorf("context: %w", err)` wrappers. Prefer `%w`, `errors.Is`, and `errors.As` for new APIs; use `Cause` when maintaining compatibility with errors that implement the legacy interface. + +## Capture a trace + +`Trace` adds context and a runtime stack trace to a non-nil cause. The trace is captured at the call site. Its output includes multiple `[trace]` entries, so callers should avoid adding it where a large error string is undesirable. diff --git a/trace.go b/trace.go new file mode 100644 index 0000000..3a53177 --- /dev/null +++ b/trace.go @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package errors + +import ( + "errors" + "fmt" + "runtime" +) + +const ( + traceDepth = 10 + traceCallersSkip = 4 +) + +// Trace wraps cause with a formatted message and appends a stack trace. +// It returns nil when cause is nil. +func Trace(cause error, message string, args ...interface{}) error { + err := Wrapf(cause, message, args...) + if err == nil { + return nil + } + + wrapped := func() *errorEntity { + target := &errorEntity{} + _ = errors.As(err, &target) + return target + }() + wrapped.trace = runtimeTrace(traceDepth) + return wrapped +} + +func runtimeTrace(depth int) string { + pcs := make([]uintptr, depth) + n := runtime.Callers(traceCallersSkip, pcs) + frames := runtime.CallersFrames(pcs[:n]) + + var result string + for { + frame, more := frames.Next() + if !more { + break + } + result += fmt.Sprintf("\n\t[trace] %s:%d", frame.Function, frame.Line) + } + return result +} diff --git a/types.go b/types.go index 83c8307..af9b9eb 100644 --- a/types.go +++ b/types.go @@ -1,14 +1,16 @@ /* - * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2026 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ package errors +// Causer is implemented by errors that expose a legacy cause. type Causer interface { Cause() error } +// Unwrapper is implemented by errors that expose one underlying error. type Unwrapper interface { Unwrap() error } From 635cbd951b704e7a8453c036b28ddd39700518ae Mon Sep 17 00:00:00 2001 From: Mikhail Knyazhev Date: Sun, 27 Sep 2026 17:29:38 +0300 Subject: [PATCH 2/4] change --- errors_test.go | 4 ++-- operation.go | 2 +- trace.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/errors_test.go b/errors_test.go index 1c00ec3..cf4ff62 100644 --- a/errors_test.go +++ b/errors_test.go @@ -82,7 +82,7 @@ func TestUnit_Wrapf(t *testing.T) { type args struct { cause error message string - args []interface{} + args []any } tests := []struct { name string @@ -115,7 +115,7 @@ func TestUnit_Wrapf(t *testing.T) { args: args{ cause: e.New("err1"), message: "bad ip %s", - args: []interface{}{"127.0.0.1"}, + args: []any{"127.0.0.1"}, }, want: "bad ip 127.0.0.1: err1", wantErr: true, diff --git a/operation.go b/operation.go index 1b0074e..d5c1a98 100644 --- a/operation.go +++ b/operation.go @@ -12,7 +12,7 @@ import ( // Wrapf returns nil when cause is nil. Otherwise it prefixes cause with a // formatted message and preserves cause for errors.Is and errors.As. -func Wrapf(cause error, message string, args ...interface{}) error { +func Wrapf(cause error, message string, args ...any) error { if cause == nil { return nil } diff --git a/trace.go b/trace.go index 3a53177..5bbcb4e 100644 --- a/trace.go +++ b/trace.go @@ -18,7 +18,7 @@ const ( // Trace wraps cause with a formatted message and appends a stack trace. // It returns nil when cause is nil. -func Trace(cause error, message string, args ...interface{}) error { +func Trace(cause error, message string, args ...any) error { err := Wrapf(cause, message, args...) if err == nil { return nil From 7b56237d6a9ebb8f58e9244db4bb91d590435387 Mon Sep 17 00:00:00 2001 From: Mikhail Knyazhev Date: Sun, 27 Sep 2026 17:44:25 +0300 Subject: [PATCH 3/4] change --- errors.go | 22 +++++++++++++++------- operation.go | 11 ++++++----- trace.go | 20 ++++++++++---------- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/errors.go b/errors.go index 4453cc7..60513c1 100644 --- a/errors.go +++ b/errors.go @@ -6,6 +6,8 @@ // Package errors provides error construction, wrapping, and inspection helpers. package errors +import "strings" + type errorEntity struct { cause error message string @@ -18,14 +20,20 @@ func New(message string) error { } func (v *errorEntity) Error() string { - message := v.message - if v.cause != nil { - if message != "" { - message += ": " - } - message += v.cause.Error() + if v.cause == nil { + return v.message + v.trace + } + + cause := v.cause.Error() + var message strings.Builder + message.Grow(len(v.message) + len(cause) + len(v.trace) + 2) + message.WriteString(v.message) + if v.message != "" { + message.WriteString(": ") } - return message + v.trace + message.WriteString(cause) + message.WriteString(v.trace) + return message.String() } // Cause returns the underlying cause. diff --git a/operation.go b/operation.go index d5c1a98..1df22c1 100644 --- a/operation.go +++ b/operation.go @@ -8,6 +8,7 @@ package errors import ( e "errors" "fmt" + "strings" ) // Wrapf returns nil when cause is nil. Otherwise it prefixes cause with a @@ -99,14 +100,14 @@ type joinedError struct { } func (v *joinedError) Error() string { - message := "" + var message strings.Builder for _, cause := range v.causes { - if message != "" { - message += ": " + if message.Len() > 0 { + message.WriteString(": ") } - message += cause.Error() + message.WriteString(cause.Error()) } - return message + return message.String() } func (v *joinedError) Unwrap() []error { diff --git a/trace.go b/trace.go index 5bbcb4e..6665152 100644 --- a/trace.go +++ b/trace.go @@ -6,9 +6,9 @@ package errors import ( - "errors" - "fmt" "runtime" + "strconv" + "strings" ) const ( @@ -24,11 +24,7 @@ func Trace(cause error, message string, args ...any) error { return nil } - wrapped := func() *errorEntity { - target := &errorEntity{} - _ = errors.As(err, &target) - return target - }() + wrapped := err.(*errorEntity) wrapped.trace = runtimeTrace(traceDepth) return wrapped } @@ -38,13 +34,17 @@ func runtimeTrace(depth int) string { n := runtime.Callers(traceCallersSkip, pcs) frames := runtime.CallersFrames(pcs[:n]) - var result string + var result strings.Builder + var lineBuffer [20]byte for { frame, more := frames.Next() if !more { break } - result += fmt.Sprintf("\n\t[trace] %s:%d", frame.Function, frame.Line) + result.WriteString("\n\t[trace] ") + result.WriteString(frame.Function) + result.WriteByte(':') + result.Write(strconv.AppendInt(lineBuffer[:0], int64(frame.Line), 10)) } - return result + return result.String() } From 1097a6ab57696ad6772b1e9a4f6d20a2b94f8b72 Mon Sep 17 00:00:00 2001 From: Mikhail Knyazhev Date: Sun, 27 Sep 2026 17:53:59 +0300 Subject: [PATCH 4/4] change --- errors.go | 12 +++++++----- operation.go | 4 ++-- trace.go | 16 +++++++++++----- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/errors.go b/errors.go index 60513c1..dbdb94a 100644 --- a/errors.go +++ b/errors.go @@ -8,6 +8,8 @@ package errors import "strings" +const causeSeparator = ": " + type errorEntity struct { cause error message string @@ -26,13 +28,13 @@ func (v *errorEntity) Error() string { cause := v.cause.Error() var message strings.Builder - message.Grow(len(v.message) + len(cause) + len(v.trace) + 2) - message.WriteString(v.message) + message.Grow(len(v.message) + len(cause) + len(v.trace) + len(causeSeparator)) + _, _ = message.WriteString(v.message) if v.message != "" { - message.WriteString(": ") + _, _ = message.WriteString(causeSeparator) } - message.WriteString(cause) - message.WriteString(v.trace) + _, _ = message.WriteString(cause) + _, _ = message.WriteString(v.trace) return message.String() } diff --git a/operation.go b/operation.go index 1df22c1..a87e23b 100644 --- a/operation.go +++ b/operation.go @@ -103,9 +103,9 @@ func (v *joinedError) Error() string { var message strings.Builder for _, cause := range v.causes { if message.Len() > 0 { - message.WriteString(": ") + _, _ = message.WriteString(causeSeparator) } - message.WriteString(cause.Error()) + _, _ = message.WriteString(cause.Error()) } return message.String() } diff --git a/trace.go b/trace.go index 6665152..306ee06 100644 --- a/trace.go +++ b/trace.go @@ -6,6 +6,7 @@ package errors import ( + "errors" "runtime" "strconv" "strings" @@ -14,6 +15,7 @@ import ( const ( traceDepth = 10 traceCallersSkip = 4 + decimalBase = 10 ) // Trace wraps cause with a formatted message and appends a stack trace. @@ -24,7 +26,11 @@ func Trace(cause error, message string, args ...any) error { return nil } - wrapped := err.(*errorEntity) + wrapped := &errorEntity{} + if !errors.As(err, &wrapped) { + wrapped.cause = err + } + wrapped.trace = runtimeTrace(traceDepth) return wrapped } @@ -41,10 +47,10 @@ func runtimeTrace(depth int) string { if !more { break } - result.WriteString("\n\t[trace] ") - result.WriteString(frame.Function) - result.WriteByte(':') - result.Write(strconv.AppendInt(lineBuffer[:0], int64(frame.Line), 10)) + _, _ = result.WriteString("\n\t[trace] ") + _, _ = result.WriteString(frame.Function) + _ = result.WriteByte(':') + _, _ = result.Write(strconv.AppendInt(lineBuffer[:0], int64(frame.Line), decimalBase)) } return result.String() }