Skip to content

Getting started

This tutorial walks you through compiling a regular expression that came from a config file — the canonical untrusted source — using CompileBounded, and handling each way it can fail. By the end you will know how to branch on the three sentinel errors and, just as importantly, when not to use this package at all.

You will build one small, self-contained program so you can see every moving part.

Prerequisites

  • Go 1.26 or newer.
  • A new module to experiment in:
mkdir regexutil-tutorial && cd regexutil-tutorial
go mod init example.test/regexutil-tutorial
go get gitlab.com/phpboyscout/go/regexutil

Step 1 — Compile a config-supplied pattern

Imagine your tool lets an operator define which branches to act on via a regex in a config file. That value is untrusted: it can be arbitrarily long, or pathological, or simply mistyped. Route it through CompileBounded instead of regexp.Compile.

CompileBounded takes a context.Context and the pattern, and returns the usual *regexp.Regexp plus an error:

ctx := context.Background()

re, err := regexutil.CompileBounded(ctx, patternFromConfig)
if err != nil {
    // handled in the next step
}

Under the hood it applies two bounds before you get a compiled regex back: it rejects the pattern outright if it exceeds MaxPatternLength (1024 bytes), and it aborts the compile if it runs longer than DefaultCompileTimeout (100 ms). On success you get an ordinary *regexp.Regexp you can use exactly as if you had called regexp.Compile.

Step 2 — Handle each failure mode

The error returned on failure wraps one of three sentinels, so you can tell the failure modes apart with errors.Is. Use cockroachdb/errors (the same package regexutil wraps with); the standard library's errors.Is works too, since the sentinels are ordinary error values.

switch {
case errors.Is(err, regexutil.ErrPatternTooLong):
    // The pattern exceeded MaxPatternLength and was rejected without being
    // compiled. Ask the operator to shorten it.
case errors.Is(err, regexutil.ErrPatternCompileTimeout):
    // Compilation did not finish within the timeout — a sign of a
    // pathological pattern. Ask for a simpler expression.
case errors.Is(err, regexutil.ErrPatternInvalid):
    // The pattern is not valid regex syntax (regexp.Compile rejected it).
case err != nil:
    // Defensive: no other error kind is expected, but never assume.
default:
    // err == nil — re is ready to use.
}

Why errors.Is, not ==. The returned error is a wrapped value with an attached hint, not the bare sentinel. Comparing with == would miss it; errors.Is unwraps the chain and matches the sentinel underneath.

Note the distinction between the first two branches. ErrPatternTooLong is cheap — the length is checked and the pattern is rejected before any compile work starts. ErrPatternCompileTimeout means a short-enough pattern still spent too long compiling, which is the pathological case the timeout exists to catch.

Step 3 — The complete program

package main

import (
    "context"
    "fmt"

    "github.com/cockroachdb/errors"
    "gitlab.com/phpboyscout/go/regexutil"
)

func main() {
    // Pretend this came from a config file.
    patternFromConfig := `^release/v\d+\.\d+\.\d+$`

    re, err := regexutil.CompileBounded(context.Background(), patternFromConfig)
    switch {
    case errors.Is(err, regexutil.ErrPatternTooLong):
        fmt.Println("pattern rejected: too long — shorten it")
        return
    case errors.Is(err, regexutil.ErrPatternCompileTimeout):
        fmt.Println("pattern rejected: too complex to compile safely")
        return
    case errors.Is(err, regexutil.ErrPatternInvalid):
        fmt.Println("pattern rejected: invalid regex syntax")
        return
    case err != nil:
        fmt.Println("unexpected error:", err)
        return
    }

    fmt.Println("release/v1.2.3 matches:", re.MatchString("release/v1.2.3"))
    fmt.Println("main matches:", re.MatchString("main"))
}

Run it:

go run .

You should see release/v1.2.3 matches: true and main matches: false. Try editing patternFromConfig to broken syntax such as (unclosed and rerun — the program prints the invalid-syntax branch.

When not to use regexutil

regexutil exists to bound patterns you do not control. If the pattern is a literal known at build time, there is nothing to defend against — you wrote it — and the bounds are pure overhead. Keep using regexp.MustCompile for those:

// Build-time literal: you own it, so this is correct and idiomatic.
var semverRE = regexp.MustCompile(`^v\d+\.\d+\.\d+$`)

Reach for regexutil only when the pattern crosses a trust boundary into your process — config file, CLI flag, TUI input, HTTP payload, message queue.

Next steps