Expressões regulares (regex) são uma ferramenta poderosa para busca, validação e manipulação de texto. Em Go, o pacote regexp fornece uma implementação robusta baseada na sintaxe RE2, que garante execução linear e segura (sem backtracking catastrófico). Nesta aula, vamos explorar os conceitos essenciais: compilar padrões, realizar operações de match, find e replace, trabalhar com grupos de captura e entender as implicações de performance.

Antes de mergulhar nos exemplos, é importante destacar que a sintaxe de regex em Go segue o padrão RE2, que é um subconjunto do PCRE (Perl Compatible Regular Expressions). Isso significa que alguns recursos avançados como lookahead e lookbehind não são suportados. Mas para a maioria dos casos de uso, a sintaxe básica e os grupos são suficientes.

Compilando

O primeiro passo para usar regex em Go é compilar o padrão. A função regexp.Compile recebe uma string com o padrão e retorna um ponteiro para Regexp e um erro. Se o padrão for inválido, o erro será retornado. Também existe a função regexp.MustCompile, que entra em pânico (panic) em caso de erro, útil para padrões fixos definidos em tempo de compilação.

É fundamental compilar uma vez e reutilizar o objeto Regexp em vez de compilar a cada operação, pois a compilação é relativamente cara. Vamos ver um exemplo simples:

package main

import (
    "fmt"
    "regexp"
)

func main() {
    // Compilação com tratamento de erro
    re, err := regexp.Compile(`[0-9]+`)
    if err != nil {
        fmt.Println("Erro ao compilar:", err)
        return
    }
    fmt.Println("Regex compilada com sucesso:", re)

    // Usando MustCompile para padrões fixos
    re2 := regexp.MustCompile(`^[a-z]+$`)
    fmt.Println("MustCompile:", re2)
}

Note o uso de crases (backticks) para definir a string do padrão, evitando escapes desnecessários. Em strings com aspas duplas, precisaríamos escapar barras invertidas.

Match, Find, Replace

O pacote regexp oferece diversos métodos para trabalhar com texto. Os principais são:

  • MatchString: verifica se a string contém alguma correspondência.
  • FindString: retorna a primeira correspondência.
  • FindAllString: retorna todas as correspondências.
  • ReplaceAllString: substitui todas as correspondências por uma string de substituição.

Vamos ver um exemplo prático que demonstra essas operações:

package main

import (
    "fmt"
    "regexp"
)

func main() {
    texto := "O gato subiu no telhado. O cachorro latiu."
    re := regexp.MustCompile(`\b(gato|cachorro)\b`)

    // MatchString
    fmt.Println("Tem animal?", re.MatchString(texto)) // true

    // FindString
    fmt.Println("Primeiro animal:", re.FindString(texto)) // gato

    // FindAllString
    animais := re.FindAllString(texto, -1)
    fmt.Println("Todos os animais:", animais) // [gato cachorro]

    // ReplaceAllString
    novoTexto := re.ReplaceAllString(texto, "animal")
    fmt.Println("Texto substituído:", novoTexto)
}

Além desses, existem variantes como FindStringIndex, FindAllStringSubmatch, etc., que permitem obter posições e subcorrespondências.

Grupos

Grupos de captura são usados para extrair partes específicas de uma correspondência. Em Go, os grupos são definidos com parênteses (). O método FindStringSubmatch retorna um slice de strings onde o primeiro elemento é a correspondência completa e os seguintes são os grupos.

Vamos extrair data no formato dd/mm/aaaa:

package main

import (
    "fmt"
    "regexp"
)

func main() {
    re := regexp.MustCompile(`(\d{2})/(\d{2})/(\d{4})`)
    texto := "Hoje é 15/04/2025."
    submatch := re.FindStringSubmatch(texto)
    if submatch != nil {
        fmt.Println("Data completa:", submatch[0]) // 15/04/2025
        fmt.Println("Dia:", submatch[1])           // 15
        fmt.Println("Mês:", submatch[2])           // 04
        fmt.Println("Ano:", submatch[3])           // 2025
    }
}

Também é possível usar grupos nomeados para tornar o código mais legível. A sintaxe é (?P<nome>padrão):

re := regexp.MustCompile(`(?P\d{2})/(?P\d{2})/(?P\d{4})`)
submatch := re.FindStringSubmatch(texto)
for i, nome := range re.SubexpNames() {
    if i != 0 && nome != "" {
        fmt.Printf("%s: %s\n", nome, submatch[i])
    }
}

Essa abordagem facilita a manutenção quando o padrão é complexo.

Performance

Embora o RE2 seja eficiente, ainda é importante usar regex com cuidado para evitar gargalos. Algumas boas práticas:

  • Compile uma vez: guarde o objeto Regexp em uma variável global ou em uma estrutura, em vez de compilar a cada chamada.
  • Evite padrões muito complexos: mesmo com RE2, padrões com muitos grupos aninhados podem ser lentos. Prefira padrões simples.
  • Use métodos específicos: se você só precisa verificar se existe correspondência, use MatchString em vez de FindString.
  • Considere alternativas: para validações simples, funções da biblioteca padrão como strings.Contains ou strconv podem ser mais rápidas.

Vamos testar a performance de uma busca com e sem compilação prévia:

package main

import (
    "fmt"
    "regexp"
    "time"
)

func main() {
    texto := "abc123def456"
    padrao := `\d+`

    // Compilando uma vez
    re := regexp.MustCompile(padrao)
    start := time.Now()
    for i := 0; i < 100000; i++ {
        re.MatchString(texto)
    }
    fmt.Println("Com compilação prévia:", time.Since(start))

    // Compilando a cada iteração
    start = time.Now()
    for i := 0; i < 100000; i++ {
        regexp.MustCompile(padrao).MatchString(texto)
    }
    fmt.Println("Compilando a cada iteração:", time.Since(start))
}

A diferença é significativa, então sempre compile uma vez.

Boas Práticas e Observações Finais

Além da performance, lembre-se de que regex pode ser difícil de ler. Comente seus padrões ou use constantes com nomes descritivos. Para padrões complexos, considere escrever testes específicos para validar os casos de borda. E, principalmente, use o pacote regexp sempre que precisar de manipulação avançada de texto, mas evite usá-lo para tarefas simples que podem ser resolvidas com funções da biblioteca padrão.

Referências

Exercícios

  1. Escreva uma função que valide se uma string é um email válido (formato simples: algo@algo.algo). Use regex e retorne booleano.

✓ Resposta:
func isValidEmail(email string) bool {
    re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
    return re.MatchString(email)
}
  • Dada a string "O preço é R$ 123,45", extraia o valor numérico (123.45) usando grupos.
  • ✓ Resposta:
    re := regexp.MustCompile(`R\$\s*(\d+),(\d+)`)
    submatch := re.FindStringSubmatch(texto)
    valor := submatch[1] + "." + submatch[2]
    fmt.Println(valor) // 123.45
    
  • Substitua todas as ocorrências de palavras de 3 letras por "###" em uma frase.
  • ✓ Resposta:
    frase := "O sol é uma estrela."
    re := regexp.MustCompile(`\b[a-zA-Z]{3}\b`)
    resultado := re.ReplaceAllString(frase, "###")
    fmt.Println(resultado) // "### sol é uma estrela." (O é 1 letra, não substitui)
    
  • Escreva um programa que use grupos nomeados para extrair nome e idade de uma string como "João: 30 anos".
  • ✓ Resposta:
    re := regexp.MustCompile(`(?P[A-Za-z]+):\s*(?P\d+)\s*anos`)
    submatch := re.FindStringSubmatch("João: 30 anos")
    for i, nome := range re.SubexpNames() {
        if i != 0 && nome != "" {
            fmt.Printf("%s: %s\n", nome, submatch[i])
        }
    }
    
  • Explique por que é importante compilar a regex apenas uma vez e dê um exemplo de como fazer isso em um programa com múltiplas chamadas.
  • ✓ Resposta: Compilar uma regex é um processo relativamente caro, pois envolve análise e construção da máquina de estados. Se você compilar a cada chamada, o custo é pago repetidamente, degradando a performance. A prática recomendada é compilar uma vez e reutilizar o objeto Regexp. Por exemplo, em um servidor HTTP, você pode criar a regex como variável global ou em uma estrutura, e usá-la em todos os handlers.
    var emailRegex = regexp.MustCompile(`^[^@]+@[^@]+\.[^@]+$`)
    
    func handler(w http.ResponseWriter, r *http.Request) {
        email := r.URL.Query().Get("email")
        if !emailRegex.MatchString(email) {
            http.Error(w, "Email inválido", http.StatusBadRequest)
            return
        }
        // ...
    }