Bài string internals kết luận: chuyển string↔[]byte phải copy vì string bất biến. Nhưng "phải" đó là quy tắc của Go an toàn — có một cửa hậu bằng unsafe để chuyển không copy, nhanh gấp 20 lần và không cấp phát. Nó tồn tại vì đôi khi cần, nhưng dùng sai một chút là phá hỏng chương trình theo cách khó tìm nhất. Bài này đo mức tiết kiệm thật, chứng minh nó nguy hiểm thế nào, và chỉ rõ khi nào (hiếm) được dùng.

Chuyển không copy bằng unsafe (Go 1.20+)

Từ Go 1.20, thư viện unsafe có API sạch để lấy con trỏ dữ liệu và tạo string/slice từ con trỏ:

// KHÔNG COPY: chia sẻ backing, 0 cấp phát
func byteToString(b []byte) string {
    return unsafe.String(unsafe.SliceData(b), len(b))
}
func stringToByte(s string) []byte {
    return unsafe.Slice(unsafe.StringData(s), len(s))
}

unsafe.SliceData/unsafe.StringData lấy con trỏ tới dữ liệu; unsafe.String/unsafe.Slice tạo string/slice mới trỏ vào cùng con trỏ đó — không copy dữ liệu. Kết quả chia sẻ vùng nhớ với gốc.

Ảnh chụp đoạn mã Go nền tối minh hoạ chuyển string byte không copy nhanh 20 lần nhưng nguy hiểm, bài trước chuyển string byte phải copy vì bất biến có cách không copy bằng unsafe Go 1.20 nhanh hơn nhiều nhưng phá vỡ đảm bảo an toàn nếu dùng sai, an toàn copy 1 cấp phát mỗi lần b bằng byte s s2 bằng string b, không copy unsafe Go 1.20 chia sẻ backing 0 cấp phát func byteToString b byte return unsafe String unsafe SliceData b len b func stringToByte s string return unsafe Slice unsafe StringData s len s, cạm bẫy chết người unsafe chuyển đổi tạo string và byte chia sẻ cùng vùng nhớ nếu sau đó sửa byte string bất biến cũng đổi theo phá vỡ đảm bảo cốt lõi tệ hơn sửa byte lấy từ string literal ở vùng chỉ đọc sẽ crash segfault chỉ dùng unsafe khi tuyệt đối chắc không sửa byte string byte gốc còn sống, khi nào an toàn dùng chuyển tạm thời chỉ để đọc so sánh tra map đường siêu nóng xử lý chuỗi byte khối lượng lớn kiểm soát được vòng đời

Hình 1: unsafe.String/unsafe.Slice chuyển không copy, chia sẻ backing. Cạm bẫy: sửa []byte làm hỏng string. Chỉ an toàn khi chỉ đọc và kiểm soát vòng đời.

Đo thật: nhanh 20 lần, và nguy hiểm

Benchmark cách an toàn (copy) vs unsafe (không copy):

Ảnh chụp bảng kết quả đo thật nền tối unsafe zero-copy Go 1.23 arm64, an toàn copy vs unsafe không copy BenchmarkByteToStringSafe 14.11 ns/op 80 B/op 1 allocs/op BenchmarkByteToStringUnsafe 0.70 ns/op 0 B/op 0 allocs/op nhanh 20x BenchmarkStringToByteSafe 16.87 ns/op 80 B/op 1 allocs/op BenchmarkStringToByteUnsafe 0.80 ns/op 0 B/op 0 allocs/op nhanh 21x unsafe nhanh khoảng 20 lần 0 cấp phát chia sẻ backing không copy, chứng minh nguy hiểm sửa byte làm hỏng string string gốc AAAAA b bằng unsafe Slice unsafe StringData s len s chia sẻ backing b 0 bằng X b 1 bằng X string bây giờ XXAAA string bất biến đã bị đổi phá vỡ đảm bảo an toàn với literal ở rodata sẽ crash segfault

Hình 2: Unsafe zero-copy nhanh ~20 lần (0,70/0,80 ns vs 14/17 ns) và 0 cấp phát. Nhưng chứng minh nguy hiểm: sửa b[0],b[1]='X' làm string "AAAAA" thành "XXAAA" — string bất biến đã bị đổi.

Kết quả hai mặt:

  • Nhanh 20 lần, 0 cấp phát: unsafe byteToString tốn 0,70 ns (so với 14,11 ns cách an toàn), stringToByte tốn 0,80 ns (so với 16,87 ns). Cả hai 0 cấp phát vì chia sẻ backing thay vì copy.
  • Nguy hiểm chết người: sau khi chuyển s → b không copy, sửa b[0]='X', b[1]='X' làm string s từ "AAAAA" thành "XXAAA". String "bất biến" đã bị đổi — phá vỡ đảm bảo cốt lõi của Go. Nếu s là literal ở vùng chỉ-đọc (rodata), sửa sẽ crash segfault thay vì hỏng âm thầm.

Khi nào an toàn (và không) dùng

An toàn khi CHỈ ĐỌC và kiểm soát vòng đời. Dùng được khi: (1) bạn chỉ đọc kết quả, không bao giờ sửa []byte; (2) dữ liệu gốc còn sống suốt thời gian dùng kết quả. Ví dụ: chuyển []byte sang string tạm để tra map (chỉ đọc), so sánh chuỗi, hoặc truyền vào hàm nhận string chỉ đọc — trên đường siêu nóng nơi 1 cấp phát mỗi lần cộng dồn thành nút thắt.

TUYỆT ĐỐI KHÔNG khi có thể sửa hoặc dữ liệu chết. Không dùng nếu: []byte có thể bị sửa sau đó, string kết quả bị giữ lâu hơn dữ liệu gốc, hoặc bạn không chắc 100% về vòng đời. Rủi ro là bug corruption âm thầm (dữ liệu sai) hoặc crash — cực khó tìm.

Ứng dụng thực tế

Thư viện hiệu năng cao dùng nó có kiểm soát chặt. Các thư viện như JSON parser tốc độ cao, database driver, template engine dùng unsafe zero-copy ở những điểm nóng cụ thể, với comment rõ ràng và bất biến được đảm bảo bằng thiết kế (dữ liệu gốc không đổi trong phạm vi dùng). Đây là "unsafe có kỷ luật".

Đo trước khi dùng. Chỉ đổi sang unsafe khi profiler cho thấy chuyển đổi string↔[]byte là nút thắt thật (allocs/op cao ở hàm nóng). Với đa số code, 14 ns và 1 cấp phát không đáng để chấp nhận rủi ro corruption.

Ghi comment và cô lập. Nếu dùng, đặt nó trong một hàm helper nhỏ có tên rõ (unsafeBytesToString), comment giải thích vì sao an toàn ở đây, và không rải unsafe khắp code. Cô lập rủi ro vào một chỗ dễ audit.

Đánh đổi cần cân nhắc

20 lần nhanh nghe hấp dẫn nhưng con số tuyệt đối nhỏ. 14 ns → 0,7 ns là tiết kiệm 13 ns mỗi lần. Chỉ đáng khi hàm chạy hàng trăm triệu lần. Với hàm chạy vài nghìn lần, tiết kiệm là micro-giây — không đáng đổi lấy rủi ro phá hỏng chương trình.

unsafe phá vỡ đảm bảo mà GC/compiler dựa vào. Ngoài corruption, unsafe conversion có thể gây vấn đề với GC (con trỏ không được theo dõi đúng) hay tối ưu compiler. Go không đảm bảo hành vi khi bạn dùng unsafe sai — đó là lý do nó tên "unsafe".

Cách copy an toàn nên là mặc định tuyệt đối. Trừ khi có lý do đo được và bạn hiểu rõ mọi rủi ro, luôn dùng []byte(s)/string(b). Chi phí copy là cái giá rẻ cho sự an toàn — và Go được thiết kế để an toàn là mặc định. unsafe là ngoại lệ cần biện minh, không phải công cụ thường ngày.

Ba ý mang về

  1. unsafe.String/unsafe.Slice chuyển string↔[]byte không copy: đo thật nhanh ~20 lần (0,7 ns vs 14 ns) và 0 cấp phát vì chia sẻ backing thay vì copy — dùng được từ Go 1.20+.
  2. Chúng chia sẻ vùng nhớ nên cực nguy hiểm: đo thật, sửa []byte làm string "bất biến" đổi theo (AAAAA → XXAAA), phá vỡ đảm bảo cốt lõi — hoặc crash segfault nếu string ở vùng chỉ-đọc.
  3. Chỉ dùng khi chỉ đọc và kiểm soát vòng đời, trên đường siêu nóng: cô lập vào helper có comment, đo trước bằng profiler — mặc định tuyệt đối là cách copy an toàn vì sự an toàn đáng giá hơn 13 ns.

Phần sau ta mở chặng mới về một trong những kiểu tinh vi nhất của Go: Phần sau mổ xẻ interface internals — cấu trúc iface và eface, bảng phương thức itab, và vì sao lưu giá trị vào interface có thể gây cấp phát bất ngờ.