Expressões Regulares e Data

Lição 24: Expressões Regulares e Data

Analogia da Vida

Imagine que você é um trabalhador de classificação de pacotes:

Em programação, regex ajuda você a localizar e extrair com precisão informações de texto, enquanto o pacote time ajuda você a medir, calcular e exibir tempo.


Conceitos Centrais

Expressões Regulares (Pacote regexp)

O pacote regexp do Go é baseado no mecanismo de sintaxe RE2, suportando a maioria das sintaxes de regex comuns, mas não suporta backtracking e backreferências (uma escolha de design priorizando desempenho).

Interfaces centrais:
- Compile: regexp.Compile(pattern) → (*Regexp, error)
- Match: MatchString(s) → bool
- Find: FindString / FindAllString / FindStringSubmatch
- Replace: ReplaceAllString / ReplaceAllStringFunc

Data e Hora (Pacote time)

O tratamento de tempo do Go gira em torno da struct time.Time, usando um tempo de referência fixo como template de formatação:

Tempo de referência: Mon Jan 2 15:04:05 MST 2006
Mnemônico numérico: 01/02 03:04:05PM 2006 -0700
💡 Por que esses números estranhos? Go escolheu a sequência 1 2 3 4 5 6 7 (Mês 1, Dia 2, Hora 3, Minuto 4, Segundo 5, Ano 6, Fuso Horário 7) para facilitar a memorização.


Sintaxe Básica e Uso

1. Fundamentos de Regex

GO
package main

import (
    "fmt"
    "regexp"
)

func main() {
    // Compilar expressão regular
    re, err := regexp.Compile(`\d{3}-\d{4}-\d{4}`)
    if err != nil {
        fmt.Println("Falha na compilação da regex:", err)
        return
    }

    // Verificar se corresponde
    phone := "11-9876-5432"
    fmt.Println("Corresponde:", re.MatchString(phone)) // true

    // Encontrar primeira correspondência
    text := "Contato: 11-9876-5432 ou 21-1234-5678"
    fmt.Println("Primeira:", re.FindString(text)) // 11-9876-5432

    // Encontrar todas as correspondências
    all := re.FindAllString(text, -1)
    fmt.Println("Todas:", all) // [11-9876-5432 21-1234-5678]
}
💡 MustCompile vs Compile: MustCompile entra em pânico diretamente na falha de compilação, adequado para regexes de constantes globais; Compile retorna um erro, adequado para regexes construídas dinamicamente em tempo de execução.

2. Grupos de Captura Nomeados

GO
package main

import (
    "fmt"
    "regexp"
)

func main() {
    re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)

    email := "joao@example.com"

    match := re.FindStringSubmatch(email)
    names := re.SubexpNames()

    for i, name := range names {
        if i > 0 && name != "" {
            fmt.Printf("%s = %s\n", name, match[i])
        }
    }
    // user = joao
    // domain = example.com
}

3. Operações de Substituição

GO
package main

import (
    "fmt"
    "regexp"
    "strings"
)

func main() {
    re := regexp.MustCompile(`\s+`)

    text := "  hello   world   go  "
    result := re.ReplaceAllString(text, " ")
    fmt.Printf("[%s]\n", result) // [hello world go]

    re2 := regexp.MustCompile(`\b\w`)
    title := re2.ReplaceAllStringFunc("hello world go", func(s string) string {
        return strings.ToUpper(s)
    })
    fmt.Println(title) // Hello World Go
}

4. Operações Básicas com Tempo

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    now := time.Now()
    fmt.Println("Tempo atual:", now)

    fmt.Println("Ano:", now.Year())
    fmt.Println("Mês:", now.Month())
    fmt.Println("Dia:", now.Day())
    fmt.Println("Hora:", now.Hour())
    fmt.Println("Minuto:", now.Minute())
    fmt.Println("Segundo:", now.Second())
    fmt.Println("Dia da semana:", now.Weekday())
}
💡 time.Now() retorna hora no fuso horário local. Para hora UTC, use time.Now().UTC().

5. Formatação e Análise de Tempo

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    now := time.Now()

    // Formatação: usar tempo de referência como template
    fmt.Println(now.Format("2006-01-02"))
    fmt.Println(now.Format("2006/01/02 15:04:05"))
    fmt.Println(now.Format("03:04PM"))

    // Formatos predefinidos comuns
    fmt.Println(now.Format(time.RFC3339))

    // Analisar string para tempo
    t, err := time.Parse("2006-01-02", "2025-12-25")
    if err != nil {
        fmt.Println("Falha na análise:", err)
        return
    }
    fmt.Println("Resultado da análise:", t)

    // Analisar com fuso horário
    t2, _ := time.ParseInLocation("2006-01-02 15:04:05",
        "2025-12-25 08:00:00", time.Local)
    fmt.Println("Com fuso horário:", t2)
}
⚠️ Formatação e análise usam a mesma string de tempo de referência — este é o design exclusivo do Go. Basta lembrar 2006-01-02 15:04:05.

6. Duration e Cálculos com Tempo

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    now := time.Now()

    tomorrow := now.Add(24 * time.Hour)
    fmt.Println("Amanhã:", tomorrow.Format("2006-01-02"))

    twoHoursLater := now.Add(2 * time.Hour)
    fmt.Println("Duas horas depois:", twoHoursLater.Format("15:04:05"))

    diff := tomorrow.Sub(now)
    fmt.Println("Diferença de tempo:", diff)
    fmt.Println("Horas:", diff.Hours())
    fmt.Println("Minutos:", diff.Minutes())

    fmt.Println("Amanhã é depois:", tomorrow.After(now))
    fmt.Println("Hoje é antes:", now.Before(tomorrow))

    floored := now.Truncate(time.Hour)
    fmt.Println("Truncado para hora:", floored.Format("15:04:05"))
}

7. Temporizadores

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    // Temporizador de disparo único
    timer := time.NewTimer(2 * time.Second)
    fmt.Println("Aguardando 2 segundos...")
    <-timer.C
    fmt.Println("Tempo esgotado!")

    // Temporizador periódico (Ticker)
    ticker := time.NewTicker(500 * time.Millisecond)
    defer ticker.Stop()

    count := 0
    for t := range ticker.C {
        count++
        fmt.Println("Tick:", t.Format("15:04:05.000"))
        if count >= 3 {
            break
        }
    }

    <-time.After(1 * time.Second)
    fmt.Println("Executado após 1 segundo")
}
💡 Tenha cuidado ao usar time.After em loops — cada iteração cria um novo canal, e temporizadores antigos não serão coletados pelo garbage collector. Em loops, use time.NewTimer e manualmente Reset.


Código de Exemplo

Exemplo: Validação e Extração — Análise de Log (Dificuldade ⭐)

GO
package main

import (
    "fmt"
    "regexp"
)

func main() {
    logs := []string{
        "[2025-06-27 14:30:00] ERROR Falha na conexão com banco de dados",
        "[2025-06-27 14:30:01] INFO  Serviço iniciado com sucesso",
        "[2025-06-27 14:30:05] WARN  Espaço em disco baixo",
        "[2025-06-27 14:31:00] ERROR Timeout de requisição",
    }

    re := regexp.MustCompile(`\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (\w+)\s+(.+)`)

    levelCount := make(map[string]int)

    for _, log := range logs {
        match := re.FindStringSubmatch(log)
        if match == nil {
            continue
        }

        timestamp := match[1]
        level := match[2]
        message := match[3]

        fmt.Printf("Hora: %s | Nível: %-5s | Mensagem: %s\n",
            timestamp, level, message)

        levelCount[level]++
    }

    fmt.Println("\nEstatísticas por nível:")
    for level, count := range levelCount {
        fmt.Printf("  %s: %d entradas\n", level, count)
    }
}
▶ Experimente

Saída:

TEXT
Hora: 2025-06-27 14:30:00 | Nível: ERROR | Mensagem: Falha na conexão com banco de dados
Hora: 2025-06-27 14:30:01 | Nível: INFO  | Mensagem: Serviço iniciado com sucesso
Hora: 2025-06-27 14:30:05 | Nível: WARN  | Mensagem: Espaço em disco baixo
Hora: 2025-06-27 14:31:00 | Nível: ERROR | Mensagem: Timeout de requisição

Estatísticas por nível:
  ERROR: 2 entradas
  INFO: 1 entradas
  WARN: 1 entradas

Exemplo: Motor de Templates — Sistema de Substituição de Texto (Dificuldade ⭐⭐)

GO
package main

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

func main() {
    template := `Prezado {{name}}:
    Seu pedido {{order}} foi enviado em {{date}}.
    Previsão de entrega em {{days}} dias.
    Hora atual: {{now}}`

    vars := map[string]string{
        "name":  "Alice",
        "order": "PED-20250627-001",
        "date":  "2025-06-27",
        "days":  "3",
    }

    re := regexp.MustCompile(`\{\{(\w+)\}\}`)

    result := re.ReplaceAllStringFunc(template, func(match string) string {
        key := match[2 : len(match)-2]
        if key == "now" {
            return time.Now().Format("2006-01-02 15:04:05")
        }
        if val, ok := vars[key]; ok {
            return val
        }
        return match
    })

    fmt.Println(result)

    allVars := re.FindAllString(template, -1)
    varNames := make([]string, 0, len(allVars))
    for _, v := range allVars {
        varNames = append(varNames, v[2:len(v)-2])
    }
    fmt.Printf("\nVariáveis do template: %s\n", strings.Join(varNames, ", "))
}
▶ Experimente

Saída:

TEXT
Prezado Alice:
    Seu pedido PED-20250627-001 foi enviado em 2025-06-27.
    Previsão de entrega em 3 dias.
    Hora atual: 2025-06-27 14:30:00

Variáveis do template: name, order, date, days, now

Exemplo: Temporizador de Contagem Regressiva (Dificuldade ⭐⭐⭐)

GO
package main

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

func formatDuration(d time.Duration) string {
    if d <= 0 {
        return "Expirado"
    }
    days := int(d.Hours()) / 24
    hours := int(d.Hours()) % 24
    minutes := int(d.Minutes()) % 60
    seconds := int(d.Seconds()) % 60

    parts := []string{}
    if days > 0 {
        parts = append(parts, fmt.Sprintf("%dd", days))
    }
    if hours > 0 {
        parts = append(parts, fmt.Sprintf("%dh", hours))
    }
    if minutes > 0 {
        parts = append(parts, fmt.Sprintf("%dm", minutes))
    }
    parts = append(parts, fmt.Sprintf("%02ds", seconds))
    return strings.Join(parts, " ")
}

func parseDeadline(s string) (time.Time, error) {
    formats := []string{
        "2006-01-02 15:04:05",
        "2006-01-02",
        "2006/01/02 15:04",
        "01-02 15:04",
    }

    re := regexp.MustCompile(`^\+(\d+)([hms])`)
    if match := re.FindStringSubmatch(s); match != nil {
        return time.Now().Add(2 * time.Hour), nil
    }

    for _, format := range formats {
        if t, err := time.ParseInLocation(format, s, time.Local); err == nil {
            return t, nil
        }
    }

    return time.Time{}, fmt.Errorf("não foi possível analisar o horário: %s", s)
}

func main() {
    deadline, err := parseDeadline("2025-12-31 23:59:59")
    if err != nil {
        fmt.Println("Erro:", err)
        return
    }

    fmt.Printf("Prazo: %s\n", deadline.Format("2006-01-02 15:04:05"))
    fmt.Println(strings.Repeat("─", 40))

    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()

    count := 0
    for now := range ticker.C {
        remaining := deadline.Sub(now)
        fmt.Printf("\rRestante: %-30s", formatDuration(remaining))
        count++
        if count >= 5 || remaining <= 0 {
            break
        }
    }

    fmt.Println("\n\nDemonstração de contagem regressiva concluída")

    re := regexp.MustCompile(`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$`)
    formatted := deadline.Format("2006-01-02 15:04:05")
    fmt.Printf("Verificação de formato: %v\n", re.MatchString(formatted))
}
▶ Experimente

Cenários de Aplicação Prática

Cenário 1: Validação de Dados de Formulário

GO
package main

import (
    "fmt"
    "regexp"
)

type Validator struct {
    rules map[string]*regexp.Regexp
}

func NewValidator() *Validator {
    rules := map[string]*regexp.Regexp{
        "phone":    regexp.MustCompile(`^1[3-9]\d{9}$`),
        "email":    regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`),
        "id_card":  regexp.MustCompile(`^\d{17}[\dXx]$`),
        "username": regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]{3,15}$`),
        "password": regexp.MustCompile(`^.{8,}$`),
        "ip":       regexp.MustCompile(`^(\d{1,3}\.){3}\d{1,3}$`),
        "date":     regexp.MustCompile(`^\d{4}[-/](0[1-9]|1[0-2])[-/](0[1-9]|[12]\d|3[01])$`),
    }
    return &Validator{rules: rules}
}

func (v *Validator) Validate(field, value string) bool {
    re, ok := v.rules[field]
    if !ok {
        return true
    }
    return re.MatchString(value)
}

func (v *Validator) ValidateAll(data map[string]string) []string {
    var errors []string
    for field, value := range data {
        if !v.Validate(field, value) {
            errors = append(errors, fmt.Sprintf("formato de %s incorreto: %s", field, value))
        }
    }
    return errors
}

func main() {
    v := NewValidator()

    data := map[string]string{
        "phone":    "11987654321",
        "email":    "test@example.com",
        "username": "go_dev",
        "date":     "2025-06-27",
    }

    errors := v.ValidateAll(data)
    if len(errors) == 0 {
        fmt.Println("✓ Todos os campos validados com sucesso")
    } else {
        fmt.Println("Falha na validação:")
        for _, err := range errors {
            fmt.Printf("  ✗ %s\n", err)
        }
    }

    invalidData := map[string]string{
        "phone": "12345678901",
        "email": "not-an-email",
    }

    fmt.Println("\nTeste de dados inválidos:")
    for field, value := range invalidData {
        result := "✓"
        if !v.Validate(field, value) {
            result = "✗"
        }
        fmt.Printf("  %s %s: %s\n", result, field, value)
    }
}

Cenário 2: Consulta por Intervalo de Tempo — Gerenciamento de Status de Atividades

GO
package main

import (
    "fmt"
    "strings"
    "time"
)

type Activity struct {
    Name      string
    StartTime time.Time
    EndTime   time.Time
}

func (a Activity) Status(now time.Time) string {
    switch {
    case now.Before(a.StartTime):
        return "Não iniciada"
    case now.After(a.EndTime):
        return "Encerrada"
    default:
        return "Em andamento"
    }
}

func (a Activity) Remaining(now time.Time) string {
    if now.Before(a.StartTime) {
        d := a.StartTime.Sub(now)
        return fmt.Sprintf("Inicia em: %s", formatDur(d))
    }
    if now.Before(a.EndTime) {
        d := a.EndTime.Sub(now)
        return fmt.Sprintf("Restante: %s", formatDur(d))
    }
    return "Expirado"
}

func formatDur(d time.Duration) string {
    hours := int(d.Hours())
    minutes := int(d.Minutes()) % 60
    if hours > 24 {
        days := hours / 24
        hours = hours % 24
        return fmt.Sprintf("%dd %dh %dm", days, hours, minutes)
    }
    return fmt.Sprintf("%dh %dm", hours, minutes)
}

func main() {
    now := time.Now()

    activities := []Activity{
        {
            Name:      "Promoção de Verão",
            StartTime: mustParse("2025-06-01 00:00:00"),
            EndTime:   mustParse("2025-06-18 23:59:59"),
        },
        {
            Name:      "Volta às Aulas",
            StartTime: mustParse("2025-07-01 00:00:00"),
            EndTime:   mustParse("2025-08-31 23:59:59"),
        },
        {
            Name:      "Black Friday Preview",
            StartTime: mustParse("2025-11-01 00:00:00"),
            EndTime:   mustParse("2025-11-11 23:59:59"),
        },
    }

    fmt.Printf("Hora atual: %s\n", now.Format("2006-01-02 15:04:05"))
    fmt.Println(strings.Repeat("─", 50))

    for _, act := range activities {
        status := act.Status(now)
        remaining := act.Remaining(now)

        fmt.Printf("Atividade: %s\n", act.Name)
        fmt.Printf("  Período: %s ~ %s\n",
            act.StartTime.Format("2006-01-02"),
            act.EndTime.Format("2006-01-02"))
        fmt.Printf("  Status: %s | %s\n\n", status, remaining)
    }

    fmt.Println("Atividades em andamento:")
    found := false
    for _, act := range activities {
        if act.Status(now) == "Em andamento" {
            fmt.Printf("  - %s\n", act.Name)
            found = true
        }
    }
    if !found {
        fmt.Println("  Nenhuma atividade em andamento no momento")
    }
}

func mustParse(s string) time.Time {
    t, err := time.ParseInLocation("2006-01-02 15:04:05", s, time.Local)
    if err != nil {
        panic(err)
    }
    return t
}

❓ Perguntas Frequentes

P1: Por que minha regex não consegue corresponder caracteres chineses?

O pacote regexp do Go lida com codificação UTF-8 por padrão, então caracteres chineses podem ser correspondidos normalmente. O problema comum é não usar a classe de caracteres correta:

GO
// Errado: \w não corresponde chinês
re := regexp.MustCompile(`^\w+$`)
re.MatchString("你好") // false

// Correto: usar classe Unicode
re2 := regexp.MustCompile(`^[\p{Han}]+$`)
re2.MatchString("你好") // true

// Ou correspondência mista
re3 := regexp.MustCompile(`^[\w\p{Han}]+$`)
re3.MatchString("hello你好123") // true

P2: Qual a diferença entre time.Parse e time.ParseInLocation?

GO
// Parse usa UTC quando nenhuma informação de fuso horário está presente
t1, _ := time.Parse("2006-01-02", "2025-06-27")
fmt.Println(t1.Location()) // UTC

// ParseInLocation especifica um fuso horário padrão
t2, _ := time.ParseInLocation("2006-01-02", "2025-06-27", time.Local)
fmt.Println(t2.Location()) // Local
💡 Recomendação: Ao analisar strings de data sem fuso horário, sempre use ParseInLocation e especifique explicitamente o fuso horário.

P3: E se o desempenho da regex for ruim?

GO
// Errado: recompilar a cada chamada
func validatePhone(phone string) bool {
    re := regexp.MustCompile(`^1[3-9]\d{9}$`)
    return re.MatchString(phone)
}

// Correto: pré-compilar como variável de nível de pacote
var phoneRe = regexp.MustCompile(`^1[3-9]\d{9}$`)

func validatePhone2(phone string) bool {
    return phoneRe.MatchString(phone)
}

P4: Como lidar com conversão de fuso horário?

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    saoPaulo, _ := time.LoadLocation("America/Sao_Paulo")
    tokyo, _ := time.LoadLocation("Asia/Tokyo")
    newyork, _ := time.LoadLocation("America/New_York")

    t := time.Date(2025, 6, 27, 14, 0, 0, 0, saoPaulo)
    fmt.Println("São Paulo:", t.Format("15:04 MST"))
    fmt.Println("Tóquio:", t.In(tokyo).Format("15:04 MST"))
    fmt.Println("Nova York:", t.In(newyork).Format("15:04 MST"))
}
⚠️ time.LoadLocation pode exigir instalação de tzdata em alguns sistemas. Go 1.15+ pode incorporar dados de fuso horário importando _ "time/tzdata".


📖 Resumo

Tópico Pacote Tipos Centrais Operações Principais
Expressões Regulares regexp Regexp Compile, Match, Find, Replace
Data e Hora time Time, Duration Format, Parse, Calculate, Timer

Pontos-chave de Regex:

Pontos-chave de Tempo:


📝 Exercícios

Escreva um programa que extrai todos os links de texto Markdown, exibindo no formato Título -> URL.

GO
// Dica: corresponder formato [Título](URL)
// Entrada: `Visite [Go官网](https://golang.org) ou [GitHub](https://github.com)`
// Saída:
//   Go官网 -> https://golang.org
//   GitHub -> https://github.com

Exercício 2: Calculadora de Dias Úteis

Escreva uma função AddBusinessDays(t time.Time, n int) time.Time que calcula a data após n dias úteis de uma data fornecida (pulando sábado e domingo).

GO
// Testes:
// AddBusinessDays(2025-06-27 Sexta, 1) → 2025-06-30 Segunda
// AddBusinessDays(2025-06-27 Sexta, 5) → 2025-07-04 Sexta

Exercício 3: Sistema de Filtro de Palavras Sensíveis

Implementar um filtro de palavras sensíveis:

  1. Carregar lista de palavras sensíveis da configuração, compilar para regex
  2. Suportar modos de substituição por * e remoção completa
  3. Suportar detecção de variantes de palavras sensíveis (ex.: espaços inseridos no meio)
GO
// Texto de entrada: "Este é um site de jogos, oferecendo serviços de jo gos"
// Saída do modo substituição: "Este é um site de *, oferecendo serviços de *"
// Saída do modo remoção: "Este é um site de , oferecendo serviços de"

Próxima Lição

Na próxima lição, aprenderemos sobre Desenvolvimento de Programas de Linha de Comando, entendendo como usar Go para construir ferramentas CLI poderosas, incluindo análise de argumentos, subcomandos, entrada interativa e outras habilidades práticas.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%