0

Go Zero to Hero - Bài 49: Biểu thức chính quy (Regex) với package regexp

Regular Expressions (Regex) là công cụ sắc bén để xử lý pattern matching, validation và trích xuất chuỗi văn bản trong phát triển Backend. Golang cung cấp package chuẩn regexp với hiệu năng cao, đi kèm một số quy tắc kỷ luật nghiêm ngặt.


1. Luật thép của Regex trong Go: Luôn dùng Raw String (Backtick)

Khi viết Regex, bạn sẽ phải dùng rất nhiều ký tự thoát (escape characters) như \d (chữ số), \w (chữ cái), \s (khoảng trắng).

Nếu bạn dùng chuỗi bọc trong dấu ngoặc kép thông thường "...", bạn sẽ phải gõ double gạch chéo \\d, làm cho chuỗi Regex trở thành một mớ hỗn độn không thể đọc nổi. Giải pháp là luôn khai báo chuỗi Regex bằng dấu Backtick ` (nút dưới phím ESC).

// ❌ Cực kỳ rối mắt và dễ sai
var badRegex = "^AFC\\-\\d{4}\\-[A-Z]{6}$"

// ✅ Dễ đọc, chuẩn mực
var goodRegex = `^AFC-\d{4}-[A-Z]{6}$`

2. Compile (Biên dịch) Regex: Đừng vắt kiệt CPU!

Khác với các ngôn ngữ kịch bản (như JavaScript hay PHP) cho phép bạn xài Regex trực tiếp ngay lập tức, Go yêu cầu bạn phải Biên dịch (Compile) cái chuỗi Regex đó thành một cấu trúc dữ liệu tối ưu (*regexp.Regexp) rồi mới được mang đi so khớp.

Có 2 hàm để làm việc này:

  • regexp.Compile(): Trả về đối tượng Regex và biến error. Dùng khi chuỗi regex do người dùng nhập vào linh động.
  • regexp.MustCompile(): Chỉ trả về đối tượng Regex. Nếu chuỗi regex sai cú pháp, hệ thống sẽ PANIC ngay lập tức.

⚡ Tuyệt kỹ Backend: Việc compile Regex cực kỳ tốn CPU. Tuyệt đối KHÔNG ĐƯỢC đặt hàm MustCompile bên trong các hàm xử lý API (những hàm chạy hàng ngàn lần mỗi giây). Hãy biên dịch nó 1 lần duy nhất lúc khởi động server bằng khai báo toàn cục hoặc hàm init().

package main

import (
    "fmt"
    "regexp"
)

// Biên dịch Regex 1 LẦN DUY NHẤT khi ứng dụng vừa chạy.
// Mẫu này kiểm tra thẻ chuẩn: Bắt đầu bằng AFC-, 4 chữ số, -, 6 chữ cái in hoa.
var validCardRegex = regexp.MustCompile(`^AFC-\d{4}-[A-Z]{6}$`)

func validateGatePass(cardID string) bool {
    // Tái sử dụng biến toàn cục đã được compile, cực kỳ nhanh!
    return validCardRegex.MatchString(cardID)
}

func main() {
    fmt.Println(validateGatePass("AFC-2026-QWERTY")) // Hợp lệ -> true
    fmt.Println(validateGatePass("AFC-99-ABC"))      // Sai định dạng -> false
}

3. Các chiêu thức thực chiến với package regexp

Một khi đã có đối tượng đã compile, bạn có thể gọi các Method của nó để thao tác với chuỗi.

Chiêu 1: Kiểm tra hợp lệ (Validation) - MatchString

Dùng để kiểm tra input từ Frontend gửi lên (Email, Số điện thoại, ID thẻ) có đúng định dạng hay không. Trả về true/false. (Như ví dụ ở phần 2).

Chiêu 2: Trích xuất dữ liệu (Extraction) - FindAllString

Giả sử bạn có một file log dài dằng dặc, và bạn muốn "móc" tất cả các mã IP hoặc mã lỗi nằm rải rác trong đó ra ngoài.

func main() {
    rawLog := `
        [INFO] Máy TVM-01 kết nối IP: 192.168.1.15
        [ERROR] Máy TVM-02 rớt mạng tại IP: 10.0.0.99
    `
    // Regex tìm địa chỉ IP cơ bản
    ipRegex := regexp.MustCompile(`\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b`)
    
    // Tìm TẤT CẢ các chuỗi khớp. Tham số -1 nghĩa là "không giới hạn số lượng tìm kiếm"
    ips := ipRegex.FindAllString(rawLog, -1)
    
    for _, ip := range ips {
        fmt.Println("Phát hiện IP:", ip)
    }
}

Chiêu 3: Làm sạch và Che giấu (Masking) - ReplaceAllString

Đây là chức năng sống còn để tuân thủ bảo mật dữ liệu (Data Privacy / PCI-DSS). Trước khi ghi log một giao dịch quẹt thẻ tín dụng xuống Database, bạn BẮT BUỘC phải che các chữ số ở giữa thẻ lại.

func main() {
    logData := "Giao dịch thành công. Số thẻ: 4123-5678-9012-3456, Số tiền: 50K"
    
    // Tìm những cụm 4 chữ số nằm giữa, có dấu gạch ngang
    maskRegex := regexp.MustCompile(`\d{4}-\d{4}-\d{4}`)
    
    // Thay thế toàn bộ đoạn khớp bằng chuỗi dấu sao (*)
    safeLog := maskRegex.ReplaceAllString(logData, "****-****-****")
    
    fmt.Println(safeLog) 
    // Kết quả: Giao dịch thành công. Số thẻ: ****-****-****-3456, Số tiền: 50K
}

4. Đỉnh cao: Capture Groups (Nhóm bóc tách) với FindStringSubmatch

Đây là kỹ thuật mạnh nhất của Regex. Thay vì chỉ tìm nguyên một chuỗi dài, bạn có thể dùng cặp ngoặc đơn () để định nghĩa các Nhóm (Groups) bên trong Regex, và Go sẽ bóc tách riêng từng mảnh đó ra cho bạn thành một Slice.

Ví dụ: Bạn có một chuỗi thông tin Giao dịch TXN_9988: 15000 VND. Bạn muốn lấy riêng mã TXN_9988 và con số 15000.

func main() {
    report := "Giao dịch TXN_9988: 15000 VND"
    
    // Dùng () để tạo 2 Capture Group: 
    // Nhóm 1: (\w+) bắt các chữ và số của mã giao dịch
    // Nhóm 2: (\d+) bắt các con số của số tiền
    parser := regexp.MustCompile(`Giao dịch (\w+): (\d+) VND`)
    
    // Submatch trả về một Slice các chuỗi
    matches := parser.FindStringSubmatch(report)
    
    if len(matches) > 0 {
        // matches[0] luôn là toàn bộ chuỗi khớp hoàn toàn
        // matches[1] là Nhóm 1, matches[2] là Nhóm 2
        fmt.Println("Mã giao dịch:", matches[1]) // TXN_9988
        fmt.Println("Số tiền:", matches[2])      // 15000
    }
}

Tổng kết

Tóm lại vũ khí Regex trong Golang:

  1. Hiệu năng là vàng: Luôn dùng raw string (Backtick) để khai báo Regex. Phải compile Regex 1 lần duy nhất bằng regexp.MustCompile ở cấp độ toàn cục (Global) để không nướng cháy CPU.
  2. Động từ thực chiến:
    • MatchString: Validate hợp lệ (Đúng/Sai).
    • FindAllString: Lọc và trích xuất dữ liệu hàng loạt.
    • ReplaceAllString: Che giấu dữ liệu nhạy cảm trước khi log.
    • FindStringSubmatch: Bổ đôi chuỗi ra để lấy từng thành phần bằng Capture Groups ().

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí