Regular Expressions (regexp)

Reviewed & published by Brayan K

The regexp package matches, finds, extracts, and replaces text using patterns — you compile a pattern once with regexp.MustCompile and reuse the resulting matcher everywhere.

Part of the free Go course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ Compile, Match & Find

Start by compiling a pattern with regexp.MustCompile — written as a backtick raw string so the backslashes stay literal. The resulting *Regexp gives you MatchString (does it match at all?), FindString (the first match), and FindAllString (every match; pass -1 for no limit). Compile once and reuse — it's safe across goroutines.

package main

import (
    "fmt"
    "regexp"
)

func main() {
    // MustCompile compiles the pattern once and panics on a bad pattern.
    // Compile at package level / startup, not inside hot loops.
    re := regexp.MustCompile(`\d+`) // one or more digits

    fmt.Println(re.MatchString("abc123")) // true
    fmt.Println(re.MatchString("abc"))    // false

    // FindString returns the FIRST match (or "" if none).
    fmt.Println(re.FindString("order 42, item 7")) // 42

    // FindAllString returns ALL matches (the -1 means "no limit").
    fmt.Println(re.FindAllString("order 42, item 7", -1)) // [42 7]
}

2️⃣ Capture Groups & Named Groups

Wrap part of a pattern in parentheses to make a capture group. FindStringSubmatch then returns a slice where index 0 is the whole match and 1, 2, 3... are the groups in order. For readability, use named groups (?P<name>...) and look up a name's position with SubexpIndex — far clearer than counting parentheses.

package main

import (
    "fmt"
    "regexp"
)

func main() {
    // Capture groups in (...) let you pull out parts of a match.
    re := regexp.MustCompile(`(\w+)@(\w+)\.(\w+)`)

    // FindStringSubmatch returns [whole, group1, group2, ...].
    m := re.FindStringSubmatch("contact ada@example.com today")
    fmt.Println("whole:", m[0]) // ada@example.com
    fmt.Println("user: ", m[1]) // ada
    fmt.Println("host: ", m[2]) // example
    fmt.Println("tld:  ", m[3]) // com

    // NAMED groups (?P<name>...) are clearer for complex patterns.
    dre := regexp.MustCompile(`(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})`)
    dm := dre.FindStringSubmatch("date 2026-06-18")
    idx := dre.SubexpIndex("m")
    fmt.Println("month:", dm[idx]) // 06
}

3️⃣ Replacing & Validating

ReplaceAllString swaps every match for a replacement, and in that replacement $1, $2 (or ${name}) reference capture groups — perfect for reordering or reformatting. To validate a whole string, anchor the pattern with ^ and $ and call MatchString, so the pattern must cover the entire input.

package main

import (
    "fmt"
    "regexp"
)

func main() {
    // ReplaceAllString swaps every match for a replacement string.
    re := regexp.MustCompile(`\s+`) // runs of whitespace
    fmt.Println(re.ReplaceAllString("too   many    spaces", " "))

    // $1, $2 (or ${name}) reference capture groups in the replacement.
    swap := regexp.MustCompile(`(\w+)\s(\w+)`)
    fmt.Println(swap.ReplaceAllString("Ada Lovelace", "$2 $1"))

    // Anchors ^ and $ pin the match to start/end; MatchString validates.
    valid := regexp.MustCompile(`^[a-z]+\d+$`)
    fmt.Println(valid.MatchString("abc123"))  // true
    fmt.Println(valid.MatchString("abc123!")) // false
}

🎯 Your Turn

Find every hashtag — a # followed by one or more word characters. Fill in the blank in the pattern.

package main

import (
    "fmt"
    "regexp"
)

func main() {
    // Find every hashtag: a # followed by one or more word characters.
    re := regexp.MustCompile(`#___`) // 👉 \w+

    tags := re.FindAllString("Loving #go and #golang today!", -1)
    fmt.Println(tags)

    // ✅ Expected output:  [#go #golang]
}

Common Errors (and the fix)

🎯 Mini-Challenge: Parse Phone Numbers

Capture the area code and line number from every phone number with a pattern like (\d{3})-(\d{4}) (as a raw string). Use FindAllStringSubmatch and print each pair.

package main

import (
    "fmt"
    "regexp"
)

func main() {
    // 🎯 MINI-CHALLENGE: pull area + line from every phone number.
    // Pattern: three digits, a dash, four digits, with each part captured.
    // Use FindAllStringSubmatch(in, -1) and print "area=... line=..." per match.
    //
    // ✅ Expected output:
    // area=555 line=1234
    // area=555 line=9876

    in := "call 555-1234 or 555-9876"
    // your code here  (hint pattern: (\d{3})-(\d{4}) as a raw string)
}

🎉 Lesson Complete!

Practice quiz

Which function compiles a pattern and panics on an invalid one?

  • regexp.Compile
  • regexp.MustCompile
  • regexp.New
  • regexp.Build

Answer: regexp.MustCompile. regexp.MustCompile compiles the pattern and panics if it is invalid — ideal for package-level patterns.

Why are raw string literals (backticks) preferred for regex patterns?

  • They run faster
  • Backslashes need no escaping
  • They allow newlines only
  • They are required

Answer: Backslashes need no escaping. Raw strings avoid double-escaping backslashes, so `\d+` stays readable.

What does re.MatchString(s) return?

  • The matched text
  • A bool: whether the pattern matches somewhere in s
  • The match index
  • A slice of matches

Answer: A bool: whether the pattern matches somewhere in s. MatchString reports a bool indicating whether the regexp matches any part of s.

What does re.FindString(s) return when there is a match?

  • true
  • The leftmost matched substring
  • The whole string
  • A slice

Answer: The leftmost matched substring. FindString returns the text of the leftmost match.

What does re.FindString(s) return when there is no match?

  • nil
  • An empty string
  • A panic
  • false

Answer: An empty string. With no match, FindString returns the empty string "".

What does re.FindAllString(s, -1) return?

  • The first match
  • A slice of all non-overlapping matches
  • A count
  • A bool

Answer: A slice of all non-overlapping matches. FindAllString with n=-1 returns a []string of all matches.

What does re.FindStringSubmatch return for a pattern with groups?

  • Only group 1
  • The whole match plus each capture group
  • A bool
  • Only the whole match

Answer: The whole match plus each capture group. Index 0 is the full match; subsequent indexes are the capture groups.

How do you write a named capture group in Go's regexp?

  • (<name>...)
  • (?P<name>...)
  • (?:name...)
  • [name...]

Answer: (?P<name>...). Go uses RE2's (?P<name>...) syntax for named groups.

Which method gives the index of a named group for use after FindStringSubmatch?

  • GroupIndex
  • SubexpIndex
  • NameIndex
  • MatchIndex

Answer: SubexpIndex. re.SubexpIndex("name") returns the position of that named group in the submatch slice.

In ReplaceAllString, how do you reference the first capture group?

  • \1
  • $1
  • %1
  • {1}

Answer: $1. Use $1 (or ${1}) to reference the first numbered capture group in the replacement.

Continue this course

Frequently asked questions

Why do Go regex patterns use backtick raw strings?

Regex patterns are full of backslashes (\d, \w, \s). In a normal double-quoted Go string, the backslash is itself an escape character, so you'd have to double every one ("\\d"). A raw string literal written with backticks treats backslashes literally, so you can write the pattern exactly as a regex: `\d+`. That's why almost all Go regex code uses backtick raw strings.

What is the difference between Compile and MustCompile?

regexp.Compile returns (*Regexp, error) so you can handle an invalid pattern at run time. regexp.MustCompile returns just *Regexp and panics if the pattern is invalid. Because patterns are usually constant and known at development time, MustCompile is preferred — and you call it once at package level so a typo fails loudly at startup rather than deep in a request.

Why should I compile the regex once instead of in a loop?

Compiling a regular expression parses the pattern and builds a matching engine, which is relatively expensive. If you call MustCompile inside a hot loop you pay that cost on every iteration. Compile once — typically into a package-level var declared with regexp.MustCompile — and reuse the *Regexp, which is safe for concurrent use by multiple goroutines.

How do I extract capture groups?

Use FindStringSubmatch, which returns a slice where index 0 is the whole match and 1, 2, 3... are the parenthesized groups in order. For many matches, FindAllStringSubmatch returns a slice of those slices. Named groups written as (?P<name>...) are clearer for complex patterns; find a named group's position with re.SubexpIndex("name").

How do I reference capture groups in a replacement?

In ReplaceAllString, use $1, $2, ... for numbered groups and ${name} for named ones. For example, swapping "first last" to "last first" is ReplaceAllString(s, "$2 $1"). If a group name could run into following text, wrap it in braces (${name}) so Go knows where the reference ends.