Menu

Rune và byte trong Golang: chuỗi UTF-8 giải thích dễ hiểu

Rune và byte trong Go là gì, chuỗi lưu UTF-8 ra sao, vì sao len đếm byte, range giải mã rune thế nào, cách chuyển giữa string, []byte và []rune, và cách dùng package bytes.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

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ênCùng kiểu vớiLiteralÝ nghĩa
byteuint8byte('A'), 0x41một byte dữ liệu
runeint32'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 pointSố byteVí dụ
U+0000 đến U+007F1ASCII: a, 7, {
U+0080 đến U+07FF2é, ß, ж, ع
U+0800 đến U+FFFF3語, €, 한
U+10000 trở lên4emoji, 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 đổiKết quảChi phí
[]byte(s)các byte UTF-8sao 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ừ bytesao chép
string(r) với r là []runemã hóa sang UTF-8sao chép
string(r) với r là một runechuỗ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ùng range khi 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 []byte và package bytes.
  • 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).

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu