Biên dịch một lần, rồi so khớp
Hãy viết mẫu dưới dạng literal chuỗi thô trong dấu backtick. Trong chuỗi nháy kép thông thường, mọi dấu gạch chéo ngược phải nhân đôi ("\\d+"), và mẫu nhanh chóng trở nên khó đọc.
regexp.MustCompile gây panic nếu mẫu không hợp lệ, và đó là điều bạn muốn với một mẫu nằm trong mã nguồn: lỗi gõ nhầm thất bại ngay khi khởi động, không phải ở request đầu tiên. Với các mẫu đến từ người dùng hoặc cấu hình, hãy dùng regexp.Compile và xử lý lỗi:
re, err := regexp.Compile(userPattern)
if err != nil {
return fmt.Errorf("bad pattern: %w", err)
}
Biên dịch một lần. Biên dịch tốn kém hơn so khớp rất nhiều. Một lời gọi regexp.MustCompile bên trong hàm chạy cho mỗi request hay mỗi dòng sẽ lặp lại công việc đó mọi lúc. Hãy đặt *regexp.Regexp đã biên dịch vào một var cấp package hoặc một trường của struct. Một regexp đã biên dịch dùng an toàn từ nhiều goroutine cùng lúc.
regexp.MatchString(pattern, s) biên dịch và so khớp trong một lời gọi. Dùng cho một lần kiểm tra lẻ thì được, nhưng trong vòng lặp thì lãng phí.
Tìm kết quả khớp
Tên các phương thức theo một khuôn mẫu: Find + All? + String? + Submatch? + Index?.
| Phần | Ý nghĩa |
|---|---|
All | mọi kết quả khớp không chồng lấn; nhận giới hạn n (-1 để lấy tất cả) |
String | làm việc trên string; nếu không có phần này, phương thức nhận và trả về []byte |
Submatch | trả về cả các nhóm bắt |
Index | trả về vị trí byte thay vì văn bản |
FindString trả về "" khi không khớp, và điều này không phân biệt được với việc khớp một chuỗi rỗng. Khi mẫu có thể khớp văn bản rỗng, hãy dùng FindStringIndex (trả về nil khi không khớp) hoặc gọi MatchString trước. Các biến thể All trả về nil khi không có gì khớp.
Nhóm bắt
Dấu ngoặc đơn dùng để bắt. FindStringSubmatch trả về toàn bộ chuỗi khớp ở chỉ số 0, sau đó mỗi nhóm một phần tử:
Luôn kiểm tra nil trước khi truy cập kết quả theo chỉ số; khi không khớp, m[1] sẽ panic.
(?:...) nhóm lại mà không bắt, dùng cho phép chọn hoặc lặp: (?:ab)+. Nhóm có tên được viết là (?P<name>...), và từ Go 1.22 có thêm cách viết (?<name>...).
Thay thế
Một cái bẫy kinh điển khi tham chiếu nhóm: "$1x" được hiểu là nhóm có tên 1x, nhóm này không tồn tại, nên được thay bằng chuỗi rỗng. Hãy viết "${1}x".
Với một chuỗi cố định, strings.ReplaceAll, strings.Contains và strings.Split đơn giản và nhanh hơn regexp. Chỉ dùng regexp khi văn bản bạn tìm có một hình dạng, chứ không phải một giá trị cố định.
Tra cứu nhanh cú pháp
Go dùng cú pháp RE2, bao gồm các tính năng quen thuộc kiểu Perl:
| Cú pháp | Khớp với |
|---|---|
. | mọi ký tự trừ xuống dòng (với (?s) thì cả xuống dòng) |
\d \w \s | chữ số, ký tự từ [0-9A-Za-z_], khoảng trắng (chỉ ASCII) |
\D \W \S | phủ định của các lớp trên |
[abc] [^abc] [a-z] | lớp ký tự |
\pL \p{Greek} | lớp Unicode: mọi chữ cái, mọi ký tự Hy Lạp |
* + ? {n,m} | lặp, kiểu tham lam |
*? +? ?? | lặp, kiểu lười |
^ $ | đầu và cuối văn bản (đầu và cuối dòng, với (?m)) |
\b | ranh giới từ (ASCII) |
a|b | phép chọn |
(?i) | không phân biệt hoa thường từ vị trí đó đến hết nhóm bao quanh; thường được viết ở đầu mẫu |
\d và \w chỉ khớp ASCII. Để khớp "mọi chữ cái của mọi ngôn ngữ", hãy dùng \pL, và với mọi chữ số Unicode, dùng \p{Nd}. regexp.QuoteMeta(s) escape mọi ký tự đặc biệt trong s, điều bạn cần khi tạo mẫu từ đầu vào của người dùng.
Những gì RE2 không làm được
regexp của Go bảo đảm việc so khớp mất thời gian tuyến tính theo độ dài đầu vào. Những tính năng có thể phá vỡ bảo đảm đó không được hỗ trợ:
- Không có lookahead hay lookbehind:
(?=...),(?!...),(?<=...),(?<!...)là lỗi biên dịch. Vì Go 1.22 bắt đầu chấp nhận(?<name>...)cho nhóm có tên, một lookbehind giờ thất bại với thông báo khó hiểuinvalid named capture, như chương trình bên dưới cho thấy. - Không có backreference: không thể dùng
(\w)\1để khớp một chữ cái lặp đôi. - Không có lượng từ possessive hay nhóm atomic.
Cách khắc phục gần như luôn giống nhau: bắt nhiều hơn một chút so với cần, rồi lọc hoặc cắt bằng code Go. Bù lại, regexp của Go không thể bị đẩy vào backtracking thảm họa. Một mẫu như (a+)+$, có thể làm đóng băng engine PCRE chỉ với một đầu vào ngắn, ở đây chạy trong thời gian tuyến tính, điều rất quan trọng khi mẫu hoặc đầu vào đến từ bên ngoài.
Lỗi thường gặp
- Biên dịch bên trong vòng lặp hay handler. Hãy biên dịch một lần vào một biến cấp package.
- Mẫu trong nháy kép.
"\d"thậm chí không phải chuỗi Go hợp lệ; hãy dùng backtick. - Quên neo khi kiểm tra hợp lệ.
\d{5}khớp bên trong"abc123456xyz". Hãy dùng^\d{5}$. - Truy cập kết quả submatch theo chỉ số mà không kiểm tra
nil. - Trông chờ
\whay\bhiểu văn bản không phải ASCII. Hãy dùng các lớp Unicode như\pL. - Dùng regexp cho chuỗi cố định. Package
stringsrõ ràng và nhanh hơn.
Câu hỏi thường gặp
Làm sao kiểm tra một chuỗi có khớp regex trong Go không?
Biên dịch mẫu một lần bằng regexp.MustCompile, viết nó dưới dạng chuỗi thô trong dấu backtick để không phải escape dấu gạch chéo ngược, rồi gọi re.MatchString(s). Nếu không có neo (^ và $), MatchString trả về true khi mẫu khớp ở bất kỳ đâu trong chuỗi.
regexp của Go có hỗ trợ lookahead và lookbehind không?
Không. regexp của Go dùng cú pháp RE2, không có lookahead, lookbehind hay backreference. Đổi lại, việc so khớp chạy trong thời gian tuyến tính theo kích thước đầu vào, nên một mẫu hay đầu vào độc hại không thể làm treo chương trình. Hãy dùng nhóm bắt kèm một chút code Go, hoặc chia phép kiểm tra thành hai regex.
regexp.Compile và regexp.MustCompile khác nhau thế nào?
Compile trả về mẫu đã biên dịch và một lỗi. MustCompile thì panic khi mẫu không hợp lệ. Dùng MustCompile cho các mẫu viết trong mã nguồn, thường là trong một biến cấp package, để lỗi gõ nhầm lộ ra ngay khi khởi động. Dùng Compile cho các mẫu đến từ người dùng hoặc tệp cấu hình.
Làm sao lấy nhóm bắt từ một regex trong Go?
Dùng FindStringSubmatch, hàm này trả về một slice trong đó chỉ số 0 là toàn bộ chuỗi khớp và chỉ số 1, 2, ... là các nhóm, hoặc nil nếu không khớp. Để lấy mọi kết quả khớp, dùng FindAllStringSubmatch(s, -1). Nhóm có tên (?P<year>\d{4}) có thể tra bằng re.SubexpIndex("year").