Chuỗi (string) trong Go là một dãy byte, và mã nguồn Go cùng các literal chuỗi đều là UTF-8. Một byte là một trong các byte đó. Một rune là một ký tự Unicode (một code point), được UTF-8 lưu bằng 1 đến 4 byte. Phần lớn nhầm lẫn về chuỗi trong Go đến từ việc lẫn lộn hai khái niệm này:
語 chiếm ba byte trong UTF-8, nên "語Go" dài 5 byte nhưng chỉ có 3 rune. Vòng lặp chỉ số thấy năm byte, và các byte 0, 1, 2 đứng riêng thì không có nghĩa gì. Vòng lặp range giải mã UTF-8 và trả về ba rune ở các chỉ số 0, 3 và 4: i luôn là vị trí byte, không phải số thứ tự ký tự.
byte và rune là bí danh
| Tên | Cùng kiểu với | Literal | Ý nghĩa |
|---|---|---|---|
byte | uint8 | byte('A'), 0x41 | một byte dữ liệu |
rune | int32 | 'A', 'é', '世' | một code point Unicode |
Vì là bí danh, byte chính là uint8 và rune chính là int32; không cần chuyển đổi giữa bí danh và kiểu gốc của nó. Dấu nháy đơn tạo ra rune, dấu nháy kép tạo ra chuỗi. 'ab' là lỗi biên dịch, vì literal rune chỉ chứa đúng một ký tự.
Rune là số, nên bạn có thể làm phép tính trên chúng:
'7' - '0' là cách kinh điển để đổi một ký tự chữ số thành giá trị của nó. Package unicode phân loại rune (IsLetter, IsDigit, IsSpace, IsUpper) và đổi hoa thường đúng cả với chữ cái không phải ASCII. %c in rune dưới dạng ký tự, %U theo dạng U+4E16, %q dưới dạng literal rune có dấu nháy.
UTF-8 lưu ký tự như thế nào
| Code point | Số byte | Ví dụ |
|---|---|---|
| U+0000 đến U+007F | 1 | ASCII: a, 7, { |
| U+0080 đến U+07FF | 2 | é, ß, ж, ع |
| U+0800 đến U+FFFF | 3 | 語, €, 한 |
| U+10000 trở lên | 4 | emoji, chữ CJK hiếm |
Văn bản ASCII giữ nguyên trong UTF-8, vì thế code xử lý theo byte thường chạy tốt khi test rồi hỏng ngay ở cái tên có dấu đầu tiên. Công cụ chuyển văn bản sang nhị phân hiển thị các byte UTF-8 của bất kỳ đoạn văn bản nào bạn dán vào. Package unicode/utf8 làm việc trực tiếp với cách mã hóa này:
% x (có dấu cách) in các byte ở hệ thập lục phân, cách nhau bằng dấu cách. utf8.ValidString cho biết chuỗi có phải UTF-8 hợp lệ hay không. Chuỗi Go có thể chứa bất kỳ byte nào, không chỉ UTF-8 hợp lệ; khi range gặp một byte không hợp lệ, nó trả về utf8.RuneError (U+FFFD, ký tự thay thế) và tiến lên một byte.
Chuyển đổi giữa string, []byte và []rune
| Chuyển đổi | Kết quả | Chi phí |
|---|---|---|
[]byte(s) | các byte UTF-8 | sao chép |
[]rune(s) | các code point đã giải mã | giải mã và sao chép (4 byte mỗi rune) |
string(b) | chuỗi tạo từ byte | sao chép |
string(r) với r là []rune | mã hóa sang UTF-8 | sao chép |
string(r) với r là một rune | chuỗi một ký tự | nhỏ |
Đảo ngược theo byte sẽ biến 世界 thành UTF-8 không hợp lệ, vì thế reverse làm việc trên rune. Ngay cả rune cũng chưa phải là tất cả: một ký tự như é còn có thể viết bằng e cộng một dấu kết hợp, tức là hai rune. Với ký tự người dùng nhìn thấy (grapheme cluster), hãy dùng một thư viện như github.com/rivo/uniseg.
Mỗi lần chuyển đổi đều sao chép, vì chuỗi là bất biến còn slice thì không. Trong đoạn code chạy nhiều, tránh chuyển qua chuyển lại; trình biên dịch tối ưu một số trường hợp (như string(b) dùng làm khóa map hoặc trong phép so sánh) để bỏ qua bước sao chép.
Truy cập theo chỉ số trả về byte
s[i] trả về một byte, và s[i:j] cắt theo vị trí byte:
s := "café"
fmt.Println(s[3]) // 195: the first byte of é
fmt.Println(string(s[3])) // "Ã": that byte read as a code point
fmt.Println(s[:3]) // "caf"
fmt.Println(s[:4]) // "caf\xc3": half of é, invalid UTF-8
string(s[3]) là một cái bẫy: chuyển một byte đơn lẻ sang string sẽ coi nó là code point 195, tức là Ã, chứ không phải chính byte đó. Để tìm ranh giới ký tự, hãy dùng range, nhóm hàm strings.Index (trả về vị trí byte luôn nằm đúng ranh giới) hoặc utf8.DecodeRuneInString.
Package bytes
bytes là bản tương ứng của strings cho []byte: bytes.Contains, bytes.Split, bytes.Fields, bytes.TrimSpace, bytes.Equal, bytes.ToUpper và nhiều hàm khác. Dùng nó khi dữ liệu đến dưới dạng byte (nội dung tệp, body HTTP, bộ đệm mạng), để không phải chuyển sang chuỗi rồi chuyển ngược lại:
bytes.Buffer là bộ đệm byte có thể tự mở rộng, cài đặt io.Reader và io.Writer. Đây là công cụ phù hợp khi bạn vừa ghi vừa đọc byte; để ghép một chuỗi, strings.Builder rẻ hơn một chút. So sánh slice byte bằng bytes.Equal, vì == không dùng được với slice.
Nên chọn loại nào
- Văn bản để hiển thị, so sánh hay tìm kiếm: giữ nguyên là
string, và dùngrangekhi cần đi qua từng ký tự. - Vị trí ký tự, đảo ngược, cắt còn n ký tự: chuyển sang
[]rune. - I/O, băm, mã hóa, giao thức nhị phân: dùng
[]bytevà packagebytes. - Một ký tự đơn:
rune. Một byte thô đơn lẻ:byte.
Câu hỏi thường gặp
Rune trong Go là gì?
rune là bí danh của int32 và biểu diễn một code point Unicode. Literal rune được viết trong dấu nháy đơn: 'a' là 97, 'é' là 233, '世' là 19990. Duyệt chuỗi bằng for i, r := range s cho bạn các rune, được giải mã từ các byte UTF-8 của chuỗi.
Rune và byte trong Go khác nhau thế nào?
Một byte (bí danh của uint8) là 8 bit dữ liệu thô. Một rune (bí danh của int32) là cả một ký tự Unicode. Trong chuỗi UTF-8, mỗi ký tự ASCII chiếm một byte, còn các ký tự khác chiếm từ hai đến bốn byte nhưng vẫn chỉ là một rune. len(s) đếm byte; utf8.RuneCountInString(s) đếm rune.
Làm sao duyệt từng ký tự của chuỗi trong Go?
Dùng for i, r := range s. Mỗi lần lặp cho bạn vị trí byte i và rune r, nên ký tự nhiều byte được xử lý đúng. Vòng lặp chỉ số thông thường for i := 0; i < len(s); i++ đi qua từng byte, khiến ký tự không phải ASCII bị cắt thành nhiều mảnh.
Chuyển chuỗi sang slice byte trong Go như thế nào?
b := []byte(s) sao chép các byte của chuỗi sang một slice mới. string(b) chuyển ngược lại, cũng bằng một bản sao. Khi cần làm việc với ký tự, ví dụ đảo ngược chuỗi hay lấy n ký tự đầu, hãy dùng []rune(s).