Advertisement
Scroll to top
This post is part of a series called Regular Expressions With Go.
Regular Expressions With Go: Part 1

Overview

This is part two of a two-part series of tutorials about regular expressions in Go. In part one we learned what regular expressions are, how to express them in Go, and the basics of using the Go regexp library to match text against regular expression patterns. 

In part two, we will focus on using the regexp library to its full extent, including compiling regular expressions, finding one or more matches in the text, replacing regular expressions, grouping submatches, and dealing with new lines.

Using the Regexp Library

The regexp library provides full-fledged support for regular expressions as well as the ability to compile your patterns for more efficient execution when using the same pattern to match against multiple texts. You can also find indices of matches, replace matches, and use groups. Let's dive in.

Compiling Your Regex

There are two methods for compiling regexes: Compile() and MustCompile(). Compile() will return an error if the provided pattern is invalid. MustCompile() will panic. Compilation is recommended if you care about performance and plan to use the same regex multiple times. Let's change our match() helper function to take a compiled regex. Note that there is no need to check for errors because the compiled regex must be valid.

1
func match(r *regexp.Regexp, text string) {
2
    matched := r.MatchString(text)
3
	if matched {
4
		fmt.Println("√", r.String(), ":", text)
5
	} else {
6
		fmt.Println("X", r.String(), ":", text)
7
	}
8
}

Here is how to compile and use the same compiled regex multiple times:

1
func main() {
2
    es :=  `(\bcats?\b)|(\bdogs?\b)|(\brats?\b)`
3
    e := regexp.MustCompile(es)
4
	match(e, "It's raining dogs and cats")
5
	match(e, "The catalog is ready. It's hotdog time!")
6
	match(e, "It's a dog eat dog world.")
7
}
8
9
Output:
10
11
 (\bcats?\b)|(\bdogs?\b)|(\brats?\b) : 
12
  It's raining dogs and cats
13
X (\bcats?\b)|(\bdogs?\b)|(\brats?\b) : 
14
  The catalog is ready. It's hotdog time!
15
 (\bcats?\b)|(\bdogs?\b)|(\brats?\b) : 
16
  It's a dog eat dog world.

Finding

The Regexp object has a lot of FindXXX() methods. Some of them return the first match, others return all matches, and yet others return an index or indexes. Interestingly enough, the names of all 16 methods of functions match the following regex: Find(All)?(String)?(Submatch)?(Index)?

If 'All' is present then all matches are returned vs. the leftmost one. If 'String' is present then the target text and the return values are strings vs. byte arrays. If 'Submatch' is present then submatches (groups) are returned vs. just simple matches. If 'Index' is present then indexes within the target text are returned vs. the actual matches.

Let's take one of the more complex functions to task and use the FindAllStringSubmatch() method. It takes a string and a number n. If n is -1, it will return all matching indices. If n is a non-negative integer then it will return the n leftmost matches. The result is a slice of string slices. 

The result of each submatch is the full match followed by the captured group. For example, consider a list of names where some of them have titles such "Mr.", "Mrs.", or "Dr.". Here is a regex that captures the title as a submatch and then the rest of the name after a space: \b(Mr\.|Mrs\.|Dr\.) .*.

1
func main() {
2
    re := regexp.MustCompile(`\b(Mr\.|Mrs\.|Dr\.) .*`)
3
    fmt.Println(re.FindAllStringSubmatch("Dr. Dolittle", -1))
4
    fmt.Println(re.FindAllStringSubmatch(`Mrs. Doubtfire
5
                                          Mr. Anderson`, -1))
6
}
7
8
Output:
9
10
[[Dr. Dolittle Dr.]]
11
[[Mrs. Doubtfire Mrs.] [Mr. Anderson Mr.]]
12

As you can see in the output, the full match is captured first and then just the title. For each line, the search resets.

Replacing

Finding matches is great, but often you may need to replace the match with something else. The Regexp object has several ReplaceXXX() methods as usual for dealing with strings vs. byte arrays and literal replacements vs. expansions. In the great book 1984 by George Orwell, the slogans of the party are inscribed on the white pyramid of the ministry of truth: 

  • War is Peace 
  • Freedom is Slavery 
  • Ignorance is Strength 

I found a little essay on The Price of Freedom that uses some of these terms. Let's correct a snippet of it according to the party doublespeak using Go regexes. Note that some of the target words for replacement use different capitalization. The solution is to add the case-insensitive flag (i?) at the beginning of the regex. 

Since the translation is different depending on the case, we need a more sophisticated approach then literal replacement. Luckily (or by design), the Regexp object has a replace method that accepts a function it uses to perform the actual replacement. Let's define our replacer function that returns the translation with the correct case.

1
func replacer(s string) string {
2
    d := map[string]string{
3
		"war":       "peace",
4
		"WAR":       "PEACE",
5
		"War":       "Peace",
6
		"freedom":   "slavery",
7
		"FREEDOM":   "SLAVERY",
8
		"Freedom":   "Slavery",
9
		"ignorance": "strength",
10
		"IGNORANCE": "STRENGTH",
11
		"Ignorance": "Strength",
12
	}
13
14
	r, ok := d[s]
15
	if ok {
16
		return r
17
	} else {
18
		return s
19
	}
20
}

Now, we can perform the actual replacement:

1
func main() {
2
    text := `THE PRICE OF FREEDOM: Americans at War
3
             Americans have gone to war to win their
4
             independence, expand their national
5
             boundaries, define their freedoms, and defend
6
             their interests around the globe.`
7
8
    expr := `(?i)(war|freedom|ignorance)`
9
    r := regexp.MustCompile(expr)
10
11
    result := r.ReplaceAllStringFunc(text, replacer)
12
    fmt.Println(result)
13
}
14
15
Output:
16
17
THE PRICE OF SLAVERY: Americans at Peace
18
    Americans have gone to peace to win their
19
    independence, expand their national
20
    boundaries, define their slaverys, and defend
21
    their interests around the globe.

The output is somewhat incoherent, which is the hallmark of good propaganda.

Grouping

We saw how to use grouping with submatches earlier. But it is sometimes difficult to handle multiple submatches. Named groups can help a lot here. Here is how to name your submatch groups and populate a dictionary for easy access by name:

1
func main() {
2
    e := `(?P<first>\w+) (?P<middle>.+ )?(?P<last>\w+)`
3
    r := regexp.MustCompile(e)
4
    names := r.SubexpNames()
5
    fullNames := []string{
6
        `John F. Kennedy`,
7
        `Michael Jordan`}
8
    for _, fullName := range fullNames {
9
        result := r.FindAllStringSubmatch(fullName, -1)
10
        m := map[string]string{}
11
        for i, n := range result[0] {
12
            m[names[i]] = n
13
        }
14
        fmt.Println("first name:", m["first"])
15
        fmt.Println("middle_name:", m["middle"])
16
        fmt.Println("last name:", m["last"])
17
        fmt.Println()
18
    }
19
}
20
21
Output:
22
23
first name: John
24
middle_name: F. 
25
last name: Kennedy
26
27
first name: Michael
28
middle_name: 
29
last name: Jordan

Dealing With New Lines

If you remember, I said that the dot special character matches any character. Well, I lied. It doesn't match the newline (\n) character by default. That means that your matches will not cross lines unless you specify it explicitly with the special flag (?s) that you can add to the beginning of your regex. Here is an example with and without the flag.

1
func main() {
2
    text := "1111\n2222"
3
4
	expr := []string{".*", "(?s).*"}
5
	for _, e := range expr {
6
		r := regexp.MustCompile(e)
7
		result := r.FindString(text)
8
		result = strings.Replace(result, "\n", `\n`, -1)
9
		fmt.Println(e, ":", result)
10
		fmt.Println()
11
	}
12
}
13
14
Output:
15
16
.* : 1111
17
18
(?s).* : 1111\n2222

Another consideration is whether to treat the ^ and $ special characters as the beginning and end of the whole text (the default) or as the beginning and end of each line with the (?m) flag.  

Conclusion

Regular expressions are a powerful tool when working with semi-structured text. You can use them to validate textual input, clean it up, transform it, normalize it, and in general deal with a lot of diversity using concise syntax. 

Go provides a library with an easy-to-use interface that consists of a Regexp object with many methods. Give it a try, but beware of the pitfalls.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.