Library / SDK
dlclark/regexp2 avatar
dlclark/regexp2

regexp2 is a backtracking regex engine for Go, and its README tells you when to skip it

A full-featured regex engine in pure Go based on the .NET engine

1,192 stars105 forksGoMIT

At a glance

What is it?
regexp2 is a pure Go regular expression engine ported from the .NET framework, offering backtracking and Perl5 and .NET compatibility where Go's standard RE2 based regexp package refuses to follow. Its own README says you will probably be better off with the standard library, and that is the most useful thing in it.
Who is it for?
regexp2 suits a Go developer who has a pattern that RE2 rejects and that genuinely requires backtracking, or who has to match .NET or Perl5 behaviour that existing tooling depends on, and who can put a timeout on it. It does not suit anyone who reaches for it by default, since the standard regexp package is faster, safer against pathological input and already in the toolchain.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The README argues against its own use

The opening paragraph of the README is unusual for a library and worth quoting in full, because it sets the terms for the whole project. Regexp2 is a feature-rich RegExp engine for Go. It does not have constant time guarantees like the built-in regexp package, but it allows backtracking and is compatible with Perl5 and .NET. You will likely be better off with the RE2 engine from the regexp package and should only use this if you need to write very complex patterns or require compatibility with .NET.

That is the honest position, and it is the difference between this and most alternative regex libraries. The standard library's engine is RE2, which guarantees linear time in the size of the input, which is why Go's regexp deliberately refuses backreferences and lookaround. regexp2 gives those features back, and the price is that a pattern can be slow on a hostile input, which is why the library ships explicit guards.

The audience is narrow and identifiable. It is a developer with an existing .NET or Perl5 codebase whose pattern uses a construct RE2 will not compile, or with a pattern too complex to express in RE2 syntax, and who is migrating that logic to Go without wanting to rewrite the regular expression.

The engine is a port of the .NET framework's System.Text.RegularExpressions.Regex engine, which was open sourced in 2015 under the MIT licence. Some fundamental differences between .NET strings and Go strings required borrowing from the Go framework regex engine as well, and the author says he cleaned up a couple of the dirtier bits during the port, with the parse tree, the code emitted and therefore the patterns matched supposed to be identical. The repository also carries an ATTRIB file, which is where an attribution note for the original .NET code belongs.

The guards are the API, not an afterthought

Because the engine backtracks, the ways to stop it are part of the public contract, and they are visible in the method signatures.

The Match methods can return a timeout error if the re.MatchTimeout field is set, and they can return ErrBacktrackingStackLimit if a match exceeds its configured backtracking stack size. The README is explicit that any other error is a bug in the regexp2 package, which is a useful contract, since it tells you that a timeout is an expected condition to handle rather than an exceptional one.

Usage otherwise resembles the standard package closely. You start by converting a regex into a state machine through Compile or MustCompile, with MustCompile panicking if the regex is invalid, and then use the resulting Regexp struct to find matches repeatedly. The struct is safe to use across goroutines.

go
re := regexp2.MustCompile(`Your pattern`)
if isMatch, _ := re.MatchString(`Something to match`); isMatch {
    //do something
}

The consequence for a service is direct. A pattern that can backtrack needs either a timeout, a stack limit, or both, and the timeout has to be a value the service can afford, which means the pattern has to be chosen with the worst case in mind rather than the average case. This is exactly the failure mode RE2's design removes, and it is the price of the compatibility.

Offsets are runes, and byte offsets cost you a second call

The internals of regexp2 always operate on a slice of runes, so the RuneIndex and RuneLength data in a Match always reference a position in runes rather than bytes, even if the input was given as a string. The consequence is that a substring index taken from a match does not line up with a byte slice of the original string, and the library says so directly.

The escape hatch is Capture.ByteRange(), which returns the byte offset index and length of the captured text. It matches the original string input for the string APIs, and the README recommends using the provided String methods when explicit offsets are not needed.

There is a cost to it, and the README spells it out. The conversion from native rune offsets to byte offsets requires additional processing, so ByteRange does the work lazily and caches the byte offsets on the shared match text. That cache means the first call on captures from the same match is not safe to run concurrently with other ByteRange calls on that match. In practice, that is a rule for a hot path rather than for ordinary use, but it is the kind of detail that turns into a race report if nobody wrote it down, which is why it is written down here.

The v2 release renamed Capture.Index and Capture.Length to Capture.RuneIndex and Capture.RuneLength to be clearer about what they refer to, and added the ByteRange method. If you are porting from v1, that rename is a compile error rather than a silent behaviour change, which is the better kind.

Groups, repeated captures and the ECMAScript differences

The group model has a few sharp edges that the README documents precisely.

Group 0 is embedded in the Match and is an automatically assigned group encompassing the whole pattern, so m.String() is the same as m.Group.String() and as m.Groups()[0].String(). The last capture is embedded in each group, so g.String() equals g.Capture.String() and g.Captures[len(g.Captures)-1].String(). A group can be captured more than once, and each capture is separately addressable through the Captures slice, which is how you get at the individual repetitions inside a repeated group:

go
if m, _ := re.FindStringMatch(`Something to match`); m != nil {
    fmt.Printf("Group 0: %v\n", m.String())
    gps := m.Groups()
    fmt.Printf("Group 1, first capture", gps[1].Captures[0].String())
}

The ECMAScript mode has two behaviours that differ from the other modes and are worth knowing before you port a pattern. Duplicate group names are allowed in separate alternatives and keep distinct group numbers, with GroupByName(name) selecting the participating group, or the first if none participated, while GroupNumberFromName(name) always returns the first declaration's number, which may differ from the selected group. And captures inside a quantified group are cleared before each repetition in ECMAScript mode, so only the final successful repetition's captures remain, a group that does not participate in that repetition has no captures, and its backreference matches the empty string. Other modes retain capture history across repetitions.

For multiple matches from one input, the API is FindNextMatch, which is optimised so that it re-uses the underlying string and rune slice, and the README shows how to build a FindAllString equivalent out of it.

The compat package is the escape hatch for a gradual migration

The new compat sub-package provides a regexp compatibility adapter with the same Find* and Match* method signatures as regexp.Regexp, plus a compat.Matcher interface that is implemented by both *regexp.Regexp and the adapter.

That interface is the interesting part, not the adapter. A function that takes a compat.Matcher can be handed either the standard library engine or the backtracking one, which means a codebase can move pattern by pattern: the patterns that RE2 handles stay on the standard package, and the handful that need backtracking go through regexp2, with the call sites unchanged.

The v2 release also brought Regexp.Split, for splitting strings on regexp matches, so the gap between the two packages is narrower than it was in v1.

The other v2 changes are worth a skim because they are the kind that break a build. The module path is now github.com/dlclark/regexp2/v2, so imports need the /v2 suffix, and the minimum supported Go version is Go 1.25. The parser, optimizer and runner internals changed significantly to support generated regexes and additional matching optimisations, and Compile and MustCompile now take variadic compile options for regex behaviour and memory and performance tuning, with regexp2.Debug and regexp2.Compile moved to the new regexp2.OptionDebug() and regexp2.OptionIsCodeGen() options. The goal of the ECMAScript option was changed to be closer to the ECMAScript standard rather than C#'s ECMAScript behaviour, and some types and constants in the syntax package were exported or changed to support code generation.

Code generation is the performance answer, with a caveat

For extra performance the README points at regexp2cg, a code generation utility for regexp2, and says you can likely improve your regexp runtime performance by three to ten times in hot code paths, immediately adding the obvious qualification: as always you should benchmark your specifics to confirm the results.

That is the project's own claim, with the project's own caveat attached, and it is worth reading in that form rather than as a headline. The mechanism is in the v2 changelog: changes to support regexp2cg were merged in, the internals were reworked to support generated regexes, and the compile options gained a way to mark a regular expression as generated, with the runner having a separate codegen path.

The trade is portability. A generated engine is built for one pattern and one set of options, and the generated code has to be regenerated when the pattern changes, which is a different maintenance model from compiling a pattern at runtime. The repository layout shows the machinery: regexp_codegen.go, runner_codegen_test.go, regexp_codegen_test.go and the syntax/ directory whose exported types exist to support code generation.

For most uses, the standard library's regexp package remains the default answer, and this project's own README says so. The case for regexp2 is compatibility, and the case for regexp2cg is that you have a pattern hot enough to matter and have already accepted the backtracking model.

The test corpus compares against other engines

The repository layout says more about how this library is validated than the README does. Beyond the unit tests there are corpus tests that compare behaviour against other engines, with regexp_corpus_pcre_test.go, regexp_corpus_re2_test.go and regexp_corpus_rust_test.go, along with helpers in regexp_corpus_helpers_test.go.

Comparing against PCRE, RE2 and Rust's engine is the right way to test a port of this kind, because the interesting failures are not crashes but divergences: a pattern that matches a different span, a capture that is reported differently, a case where the .NET semantics leak into a place they should not. A corpus test suite is the mechanism for catching exactly that class of bug.

There are also performance and benchmark files, with regexp_performance_test.go, string_processing_benchmark_test.go and regexcmp_benchmark_test.go, and the .circleci/ directory and Taskfile.yml for the build.

The licence is MIT, the repository is not archived, and the last push was on 2026-10-15. There are no GitHub releases, so version tracking runs through the module path and the tags rather than a release page, which for a library at version two is unremarkable, but it does mean the v2 changelog in the README is the primary document for anyone upgrading from v1.

Editorial conclusion

regexp2 suits a Go developer who has a pattern that RE2 rejects and that genuinely requires backtracking, or who has to match .NET or Perl5 behaviour that existing tooling depends on, and who can put a timeout on it. It does not suit anyone who reaches for it by default, since the standard regexp package is faster, safer against pathological input and already in the toolchain. Verify first that your pattern really cannot be expressed in RE2 syntax, decide whether you will set MatchTimeout and a backtracking stack limit rather than relying on the engine to stop, and read the v2 changes because the module path, the minimum Go version and the capture field names all moved.

Frequently asked questions

What is regexp2 and why would I use it instead of Go's regexp package?

It is a Go regular expression engine ported from the .NET framework's engine, which allows backtracking and is compatible with Perl5 and .NET. The README says you will likely be better off with the built-in RE2 based regexp package unless you need very complex patterns or .NET compatibility.

How do I install regexp2?

With go get github.com/dlclark/regexp2/v2@latest. The module path carries the /v2 suffix, and version 2 requires Go 1.25 or newer.

How do I stop a backtracking regexp from hanging?

Set the re.MatchTimeout field, and configure the backtracking stack size. The Match methods then return a timeout error or ErrBacktrackingStackLimit rather than running indefinitely, and the README notes that any other error is a bug in the package.

Are the match offsets in bytes or runes?

Runes. The internals always operate on a slice of runes, so RuneIndex and RuneLength refer to rune positions even when the input was a string. Use Capture.ByteRange() for UTF-8 byte offsets, and note that it caches lazily, so the first call on captures from the same match is not safe to run concurrently with other ByteRange calls.

What does the compat sub-package do?

It provides a regexp compatibility adapter with the same Find* and Match* method signatures as regexp.Regexp, plus a compat.Matcher interface implemented by both *regexp.Regexp and the adapter, so code can be moved between the standard engine and regexp2 without changing call sites.

Official sources

  1. dlclark/regexp2 on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/dlclark-regexp2.svg)](https://hysenlabs.com/projects/dlclark-regexp2)

Was this useful?