Expressões regulares são ferramentas poderosas para busca, validação e manipulação de texto baseadas em padrões. Em Go, o pacote regexp fornece uma implementação eficiente e segura, baseada na sintaxe RE2 do Google, que garante tempo linear e evita problemas de backtracking catastrófico. Nesta aula, você aprenderá a compilar padrões, realizar buscas e substituições, extrair grupos e otimizar o uso de regex em aplicações de alto desempenho.

Dominar regex é essencial para tarefas como validação de entrada, parsing de logs, extração de dados e sanitização de texto. O pacote regexp oferece uma API limpa e consistente, e entender suas nuances — como a diferença entre funções que aceitam strings e as que operam em slices de bytes — é crucial para escrever código eficiente e correto.

Compilando

Antes de usar uma expressão regular, é necessário compilá-la em um objeto *regexp.Regexp. A compilação analisa o padrão e o transforma em uma representação interna otimizada. Existem duas funções principais: regexp.Compile e regexp.MustCompile. A primeira retorna um erro se o padrão for inválido, enquanto a segunda entra em pânico (panic) — útil para padrões definidos em tempo de compilação, onde um erro é considerado fatal.

Ao compilar, você pode usar a sintaxe RE2, que suporta a maioria das construções clássicas, mas não inclui lookahead ou lookbehind (por exemplo, (?=...) e (?<=...)). Essa limitação é intencional para garantir desempenho previsível. Se você precisar de funcionalidades avançadas, talvez seja necessário usar outra biblioteca ou reestruturar a lógica.

import (
    "fmt"
    "regexp"
)

func main() {
    // Compile retorna um erro se o padrão for inválido
    re, err := regexp.Compile(`\d+`)
    if err != nil {
        fmt.Println("Erro:", err)
        return
    }
    fmt.Println(re) // imprime a representação compilada

    // MustCompile entra em pânico se o padrão for inválido
    re2 := regexp.MustCompile(`^[a-zA-Z]+$`)
    fmt.Println(re2)
}

É importante notar que a compilação é relativamente cara. Para padrões usados repetidamente, é recomendável compilar uma vez e reutilizar o objeto *regexp.Regexp, em vez de compilar a cada chamada. Isso é especialmente crítico em loops ou em servidores que processam muitas requisições.

Match, Find, Replace

O pacote regexp oferece métodos para verificar correspondências (MatchString, Match), encontrar correspondências (FindString, FindAllString, FindStringSubmatch, etc.) e substituir (ReplaceAllString, ReplaceAllFunc). Cada método tem variantes que operam em []byte e em string; as versões em []byte são mais eficientes quando se trabalha com dados binários ou grandes volumes, pois evitam conversões desnecessárias.

Os métodos Find retornam a primeira correspondência ou todas as correspondências, dependendo da variante. Para extrair informações detalhadas, como os grupos capturados, use FindStringSubmatch ou FindAllStringSubmatch. Já as substituições permitem usar referências a grupos no padrão de substituição, como $1 para o primeiro grupo.

func main() {
    texto := "O gato preto subiu no telhado. O rato branco correu."
    re := regexp.MustCompile(`(gato|rato) (preto|branco)`)

    // MatchString verifica se há correspondência
    fmt.Println(re.MatchString(texto)) // true

    // FindString retorna a primeira correspondência
    fmt.Println(re.FindString(texto)) // "gato preto"

    // FindAllString retorna todas, com limite opcional (-1 para todas)
    fmt.Println(re.FindAllString(texto, -1)) // ["gato preto" "rato branco"]

    // ReplaceAllString substitui todas as ocorrências
    substituido := re.ReplaceAllString(texto, "animal $2")
    fmt.Println(substituido) // "O animal preto subiu no telhado. O animal branco correu."
}

Além disso, ReplaceAllFunc permite uma função de substituição que recebe a correspondência e retorna o texto de reposição, útil para transformações dinâmicas. Por exemplo, você pode converter todas as ocorrências de números em seu equivalente em hexadecimal.

Grupos

Grupos de captura são partes da expressão entre parênteses. Eles permitem extrair subpartes da correspondência, além de possibilitar referências a esses grupos na substituição. Em Go, os grupos são numerados a partir de 1, e o grupo 0 é a correspondência completa. A sintaxe (?P<nome>...) permite nomear grupos, facilitando o acesso.

Para obter os grupos de uma correspondência, use FindStringSubmatch, que retorna um slice de strings: o primeiro elemento é a correspondência total, e os seguintes são os grupos, na ordem de abertura dos parênteses. Com grupos nomeados, você pode usar SubexpIndex para encontrar o índice pelo nome.

func main() {
    re := regexp.MustCompile(`(?P<animal>gato|rato) (?P<cor>preto|branco)`)
    texto := "O gato preto subiu."

    match := re.FindStringSubmatch(texto)
    fmt.Println(match) // ["gato preto" "gato" "preto"]

    // Acessando por nome
    animalIndex := re.SubexpIndex("animal")
    corIndex := re.SubexpIndex("cor")
    fmt.Println("Animal:", match[animalIndex]) // "gato"
    fmt.Println("Cor:", match[corIndex])      // "preto"

    // Substituição usando grupos nomeados
    substituido := re.ReplaceAllString(texto, "$cor $animal")
    fmt.Println(substituido) // "preto gato"
}

Grupos também podem ser não capturantes usando (?:...), o que é útil para agrupar sem capturar, economizando memória e evitando confusão na indexação. Isso é especialmente recomendado quando você precisa de agrupamento apenas para aplicar quantificadores ou alternância, sem extrair o conteúdo.

Performance

A performance de expressões regulares em Go é geralmente excelente devido à implementação RE2, que garante complexidade linear no tamanho da entrada, independente do padrão. Isso elimina o risco de ReDoS (Denial of Service via regex) comum em outras linguagens. No entanto, ainda há boas práticas para otimizar o uso.

Primeiro, evite compilar o mesmo padrão repetidamente; compile uma vez no início e reutilize. Segundo, prefira os métodos que operam em []byte quando seus dados já estão em bytes, como ao processar arquivos ou respostas HTTP. Terceiro, use padrões mais específicos: por exemplo, em vez de .*, use [^\n]* se souber que não há quebras de linha. Isso reduz o trabalho do motor.

Além disso, considere usar regexp.MatchString para verificações simples sem precisar compilar, embora internamente ele compile e descarte o padrão a cada chamada — para uso repetido, é melhor compilar. Para substituições complexas, ReplaceAllFunc pode ser mais eficiente que várias chamadas separadas.

// Exemplo de reutilização de regex e uso de bytes
func processarDados(dados []byte) {
    re := regexp.MustCompile(`\b\w+@\w+\.\w+\b`)
    emails := re.FindAll(dados, -1)
    for _, email := range emails {
        fmt.Println(string(email))
    }
}

Por fim, lembre-se de que expressões regulares não são a solução para tudo. Para parsing de linguagens complexas, como HTML ou JSON, é melhor usar parsers dedicados. Regex é ideal para padrões simples e bem definidos.

Boas Práticas e Observações Finais

  • Use MustCompile para padrões constantes definidos em nível de pacote, para falhar rapidamente em caso de erro.
  • Documente seus padrões com comentários, pois regex pode ser difícil de ler.
  • Teste seus padrões com casos extremos, como strings vazias, caracteres especiais e grandes entradas.
  • Considere usar o pacote regexp/syntax para inspecionar a árvore sintática do padrão, se necessário.

Referências

Exercícios

  1. Escreva uma expressão regular que valide um endereço de e-mail simples (ex: nome@dominio.com) e use MatchString para testar alguns exemplos.

✓ Resposta: Um padrão simples: ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$. Exemplo:
re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
fmt.Println(re.MatchString("teste@exemplo.com")) // true
fmt.Println(re.MatchString("invalido@"))        // false
  • Dada a string "João tem 25 anos e Maria tem 30", use FindAllString para extrair todos os números.
  • ✓ Resposta: Use o padrão \d+ e FindAllString com limite -1:
    re := regexp.MustCompile(`\d+`)
    resultados := re.FindAllString("João tem 25 anos e Maria tem 30", -1)
    fmt.Println(resultados) // ["25" "30"]
  • Crie um padrão com grupos nomeados para capturar o dia, mês e ano de uma data no formato DD/MM/AAAA e use FindStringSubmatch para extraí-los.
  • ✓ Resposta: Padrão: (?P<dia>\d{2})/(?P<mes>\d{2})/(?P<ano>\d{4}). Exemplo:
    re := regexp.MustCompile(`(?P\d{2})/(?P\d{2})/(?P\d{4})`)
    match := re.FindStringSubmatch("Hoje é 25/12/2023")
    fmt.Println("Dia:", match[re.SubexpIndex("dia")])
    fmt.Println("Mês:", match[re.SubexpIndex("mes")])
    fmt.Println("Ano:", match[re.SubexpIndex("ano")])
  • Use ReplaceAllString para substituir todas as ocorrências de "foo" por "bar" em uma string.
  • ✓ Resposta:
    re := regexp.MustCompile(`foo`)
    resultado := re.ReplaceAllString("foo bar foo", "bar")
    fmt.Println(resultado) // "bar bar bar"
  • Explique por que é importante reutilizar um objeto *regexp.Regexp em vez de compilar o padrão a cada chamada, e dê um exemplo de código que demonstre a diferença de performance (pode ser conceitual).
  • ✓ Resposta: Compilar um regex é caro porque envolve análise sintática e construção de autômatos. Reutilizar o objeto compilado evita esse custo. Exemplo: em um loop de 1 milhão de iterações, compilar dentro do loop torna o código muito mais lento. Veja o contraste:
    // Lento: compila a cada iteração
    for _, s := range lista {
        re := regexp.MustCompile(`\d+`)
        re.MatchString(s)
    }
    
    // Rápido: compila uma vez
    re := regexp.MustCompile(`\d+`)
    for _, s := range lista {
        re.MatchString(s)
    }