Go Zero to Hero - Bài 48: Khác biệt giữa String, Byte và Rune
Trong hệ thống backend, việc hiểu rõ cấu trúc dữ liệu cơ bản của chuỗi ký tự là chìa khóa để tránh các lỗi mã hóa (encoding) và lỗi vỡ font chữ. Golang phân định rõ ràng giữa String, Byte và Rune.
1. String: Lớp vỏ bọc "chỉ đọc" (Read-only)
Trong Go, string thực chất là một Slice chứa các Byte, và nó là Bất biến (Immutable / Read-only). Bất biến nghĩa là một khi chuỗi "AFC" được tạo ra trên RAM, bạn không thể truy cập vào ký tự ở giữa để đổi thành "AQC" được. Bạn bắt buộc phải tạo ra một chuỗi hoàn toàn mới.
Tuy nhiên, bí mật nằm ở chỗ Go mặc định mã hóa mọi chuỗi bằng chuẩn UTF-8:
- Các ký tự tiếng Anh thông thường (ASCII như
A,B,C,1,2,3) chỉ chiếm 1 byte. - Các ký tự có dấu (như tiếng Việt, Emoji) có thể chiếm từ 2 đến 4 bytes cho một ký tự!
Hãy xem cú lừa kinh điển này:
package main
import "fmt"
func main() {
// Đếm bằng mắt: "Vé tàu" có 6 ký tự (tính cả dấu cách)
text := "Vé tàu"
// NHƯNG ĐÂY LÀ KẾT QUẢ CỦA GO:
fmt.Println("Độ dài của chuỗi:", len(text)) // In ra: 8
}
Hàm len() trong Go không đếm số lượng ký tự, nó đếm số lượng Byte. Ký tự é và à mỗi ký tự chiếm mất 2 bytes.
2. Byte: Ngôn ngữ của máy móc (uint8)
Byte trong Go chỉ là một tên gọi khác (alias) của kiểu uint8 (số nguyên không dấu 8-bit). Một byte đại diện cho một con số từ 0 đến 255.
Khi bạn làm việc với giao thức mạng cấp thấp, hoặc đọc dữ liệu thô (raw payload) trực tiếp từ các cổng COM của máy bán vé tự động (TVM) hay cảm biến cổng xoay, bạn sẽ không bao giờ hứng nó bằng kiểu string, mà phải hứng bằng []byte (Slice of Bytes).
func main() {
// Giả lập dữ liệu nhị phân đọc từ máy quét thẻ RFID
// 65 tương đương với 'A', 70 tương đương 'F', 67 tương đương 'C'
rawPayload := []byte{65, 70, 67, 45, 49, 48, 50, 52}
// In ra mảng số nguyên
fmt.Println("Dữ liệu thô:", rawPayload)
// Ép kiểu (Type Conversion) từ []byte sang string để con người đọc được
fmt.Println("Dữ liệu giải mã:", string(rawPayload)) // In ra: AFC-1024
}
💡 Bí quyết Backend: Ép kiểu từ
[]bytesangstring(hoặc ngược lại) bắt buộc Go phải thực hiện thao tác Copy toàn bộ vùng nhớ. Nếu payload lên tới vài Megabytes, hành động này sẽ gây nghẽn RAM. Hãy hạn chế ép kiểu nếu không thực sự cần thiết, cứ để nguyên[]bytemà xử lý.
3. Rune: Vị cứu tinh của ngôn ngữ loài người (int32)
Nếu byte dùng để nói chuyện với máy móc, thì Rune dùng để biểu diễn các ký tự ngôn ngữ thực sự của con người (Unicode).
Rune là tên gọi khác (alias) của kiểu int32 (số nguyên 32-bit, chiếm 4 bytes). Với sức chứa 4 bytes, một Rune đủ sức đại diện cho bất kỳ ký tự nào trên thế giới (kể cả Emoji 🎟️ hay các chữ cái tượng hình).
Để đếm chính xác số lượng ký tự (Rune) thay vì đếm số Byte, Go cung cấp thư viện unicode/utf8.
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
text := "Vé tàu"
// Đếm theo Byte (Sai logic nghiệp vụ)
fmt.Println("Số Byte:", len(text)) // In ra: 8
// Đếm theo Rune (Chuẩn xác)
fmt.Println("Số Ký tự (Rune):", utf8.RuneCountInString(text)) // In ra: 6
}
4. Thực chiến: Bẫy vòng lặp làm vỡ font chữ
Sự khác biệt giữa Byte và Rune thể hiện rõ nhất khi bạn dùng vòng lặp để duyệt qua một chuỗi. Nếu bạn dùng vòng lặp for i := 0 thông thường, bạn đang cắt chuỗi đó theo từng Byte. Ký tự é (chiếm 2 bytes) sẽ bị "chém đôi", kết quả in ra màn hình sẽ là những ký tự rác (lỗi font/ký tự lạ).
Để duyệt chuỗi an toàn không bao giờ bị vỡ font, BẮT BUỘC phải dùng for range. Vòng lặp range trong Go đã được tối ưu để tự động giải mã UTF-8 và trả về từng Rune.
func main() {
text := "Vé tàu"
// ❌ CÁCH 1: Duyệt theo Byte (Bẫy chết người)
fmt.Println("--- Duyệt theo Byte ---")
for i := 0; i < len(text); i++ {
// Ký tự có dấu sẽ bị vỡ nát thành các mã loằng ngoằng
fmt.Printf("%c ", text[i])
}
fmt.Println()
// ✅ CÁCH 2: Duyệt theo Rune bằng for range (Chuẩn thực chiến)
fmt.Println("--- Duyệt theo Rune ---")
for _, char := range text {
// Tự động nhận diện 'é' và 'à' là một khối toàn vẹn
fmt.Printf("%c ", char)
}
fmt.Println()
}
Tổng kết
Bảng phân trần giúp bạn không bao giờ bị bối rối khi thiết kế luồng dữ liệu:
| Khái niệm | Bản chất | Kích thước | Ứng dụng thực chiến |
|---|---|---|---|
| Byte | uint8 |
1 Byte | Xử lý dữ liệu thô từ phần cứng, file nhị phân, thao tác tối ưu RAM ([]byte). |
| Rune | int32 |
4 Bytes | Biểu diễn một ký tự Unicode độc lập (Tiếng Việt, Emoji). |
| String | []byte (Read-only) |
Biến thiên | Chứa văn bản để hiển thị hoặc giao tiếp qua API. Hàm len() đếm số Byte, không đếm số ký tự. |
Làm chủ được String, Byte và Rune, bạn sẽ tự tin xử lý mọi loại payload dị biệt nhất đẩy lên từ các thiết bị ngoại vi mà không lo hệ thống gặp lỗi encoding.
All rights reserved