Go là ngôn ngữ an toàn kiểu và có bộ thu gom rác — bạn không thao tác con trỏ thô như C. Nhưng có một cánh cửa hậu: package unsafe. Nó cho phép chuyển đổi giữa các kiểu con trỏ tùy ý, đọc lại byte của một giá trị dưới kiểu khác, làm số học địa chỉ. Sức mạnh này đi kèm cạm bẫy chết người: chỉ vài mẫu chuyển đổi được ngôn ngữ bảo đảm hợp lệ; đi chệch khỏi chúng là hành vi không xác định, và bug có thể chỉ lộ ra khi GC tình cờ dời một đối tượng. Bài này đo thật các mẫu hợp lệ, chỉ ra mẫu sai mà go vet bắt được, và cái giá trị thật sự khiến người ta chấp nhận rủi ro này.
Ba mẫu chuyển đổi cốt lõi
unsafe.Pointer là kiểu con trỏ "vạn năng": bất kỳ *T nào chuyển sang unsafe.Pointer được, và ngược lại. Tài liệu unsafe liệt kê một số mẫu hợp lệ; ba mẫu quan trọng nhất:
Mẫu 1 — chuyển *T1 thành *T2 khi bố cục bộ nhớ tương đương. Đây là "đọc lại" (reinterpret) cùng vùng byte dưới kiểu khác:
var f float64 = 3.14
bits := *(*uint64)(unsafe.Pointer(&f)) // đọc bit của float64 như uint64
Mẫu 2 — Pointer → uintptr → Pointer, chỉ trong lời gọi syscall. uintptr là số nguyên giữ giá trị địa chỉ, cần để truyền cho syscall.Syscall. Nhưng nó có ràng buộc chặt (nói ở dưới).
Mẫu 3 — số học offset để truy cập trường/phần tử. Điểm sống còn: toàn bộ phép tính phải nằm gọn trong một biểu thức:
// ĐÚNG: chuyển đổi trọn vẹn trong một biểu thức
pY := (*int32)(unsafe.Pointer(
uintptr(unsafe.Pointer(&d)) + unsafe.Offsetof(d.Y)))

Hình 1: Bốn mẫu hợp lệ — reinterpret bit (*T1→*T2), Pointer↔uintptr cho syscall, số học offset trong một biểu thức, và helper unsafe.Slice/String. Quy tắc vàng: không lưu uintptr vào biến rồi dùng lại.
Mẫu 4: helper mới an toàn hơn
Go 1.17 thêm unsafe.Slice, Go 1.20 thêm unsafe.String/unsafe.StringData — các hàm tiện ích thay cho việc dựng reflect.SliceHeader thủ công (dễ sai và không an toàn với GC):
sl := unsafe.Slice(&arr[0], 4) // con trỏ + độ dài -> []int32
s := unsafe.String(&b[0], len(b)) // []byte -> string, KHÔNG sao chép
Cùng nhóm là ba hàm trả hằng số biên dịch: unsafe.Sizeof, unsafe.Alignof, unsafe.Offsetof. Đo thật (Go 1.23): tất cả các mẫu trên chạy đúng — float64 3.14 cho bits 0x40091eb851eb851f, đọc d.Y qua offset ra 20, Sizeof(Diem)=16 Alignof=8 Offsetof(Z)=8, unsafe.Slice và unsafe.String cho kết quả đúng. Và go vet sạch — nó công nhận đây là các mẫu hợp lệ.
Quy tắc sống còn: đừng lưu uintptr vào biến
Đây là bug kinh điển và nguy hiểm nhất khi dùng unsafe. uintptr chỉ là một số nguyên — không phải con trỏ. Bộ thu gom rác của Go không coi một uintptr là tham chiếu sống, nên nó có thể dời (hoặc thu hồi) đối tượng mà uintptr đang trỏ tới. Nếu bạn tách phép tính offset thành hai bước qua một biến trung gian:
u := uintptr(unsafe.Pointer(t)) + unsafe.Offsetof(t.B) // u là số
p := (*int64)(unsafe.Pointer(u)) // u đã "chết"!
Giữa hai dòng đó, GC có thể chạy và dời t sang địa chỉ mới — khiến u trỏ vào vùng nhớ cũ, không còn hợp lệ. p giờ là con trỏ hỏng. Bug này rất khó tái hiện vì phụ thuộc thời điểm GC. May mắn là go vet bắt được:

Hình 2: Các mẫu hợp lệ chạy sạch (offset đọc đúng d.Y=20), go vet không cảnh báo. Mẫu sai (uintptr tách ra biến) bị vet bắt: possible misuse of unsafe.Pointer. Payoff: unsafe.String (0,7799 ns, 0 cấp phát) so với string(b) (105,8 ns, 1024 B) — nhanh hơn ~136x.
Đo thật: go vet báo ./bad.go:10:16: possible misuse of unsafe.Pointer. Luôn chạy go vet khi dùng unsafe — nó là lưới an toàn quan trọng nhất bạn có.
Cái giá trị thật: chuyển đổi không sao chép
Vì sao chấp nhận rủi ro này? Câu trả lời là hiệu năng ở đường nóng. Chuyển []byte thành string bằng cách thường (string(b)) sao chép toàn bộ dữ liệu — vì string bất biến còn []byte thì không, Go phải tạo bản sao để bảo toàn tính bất biến. Với unsafe.String, bạn chia sẻ cùng backing array, không sao chép. Đo thật trên slice 1 KB:
- unsafe.String (không sao chép): 0,7799 ns/op, 0 B, 0 cấp phát.
- string(b) (sao chép an toàn): 105,8 ns/op, 1024 B, 1 cấp phát.
Nhanh hơn ~136 lần và 0 cấp phát. Với một máy chủ parse hàng triệu request, chuyển đổi []byte↔string không sao chép ở biên giới I/O tiết kiệm khổng lồ cả CPU lẫn áp lực GC. Đây là lý do các thư viện hiệu năng cao (JSON parser, router) dùng unsafe ở những chỗ nóng nhất.
Đánh đổi cần cân nhắc
unsafe.String đòi hỏi string phải bất biến sau đó. Vì nó chia sẻ backing array với []byte gốc, nếu bạn sửa slice sau khi tạo string, string cũng đổi theo — phá vỡ bất biến mà cả runtime lẫn code khác trông cậy. Chỉ dùng khi bạn chắc chắn []byte không bị sửa nữa (ví dụ buffer đọc-một-lần). Sai lầm ở đây cho bug cực khó lần.
Đừng dùng unsafe nếu không đo thấy nút cổ chai. unsafe phá vỡ mọi bảo đảm của Go: an toàn kiểu, an toàn bộ nhớ, tính di động (bố cục struct có thể khác giữa kiến trúc). Package unsafe cũng không được bảo đảm tương thích như phần còn lại của Go 1. Chỉ dùng sau khi profiling chỉ ra đúng chỗ, và cô lập nó vào một lớp nhỏ có test kỹ.
Kết quả unsafe.Sizeof/Alignof/Offsetof phụ thuộc kiến trúc. Chúng là hằng biên dịch nhưng giá trị đổi theo nền tảng (con trỏ 4 byte trên 32-bit, 8 byte trên 64-bit; padding khác nhau). Đừng hard-code offset — luôn tính qua unsafe.Offsetof để code đúng trên mọi kiến trúc.
Ba ý mang về
- Chỉ vài mẫu chuyển đổi
unsafe.Pointerlà hợp lệ và được bảo đảm: đọc lại bit (*T1→*T2bố cục tương đương), Pointer↔uintptr cho syscall, số học offset trong một biểu thức, và helper mớiunsafe.Slice/unsafe.String— đo thật tất cả chạy đúng vàgo vetsạch. - Không bao giờ lưu
uintptrvào biến rồi dùng lại: GC không coiuintptrlà con trỏ nên có thể dời đối tượng, khiến địa chỉ "chết" — đo thật,go vetbắt mẫu sai vớipossible misuse of unsafe.Pointer, luôn chạy vet khi dùng unsafe. - Cái giá trị thật là chuyển đổi không sao chép:
unsafe.String(0,7799 ns, 0 cấp phát) nhanh hơnstring(b)(105,8 ns, 1024 B) ~136 lần — nhưng đòi hỏi[]bytebất biến sau đó, và chỉ nên dùng sau khi profiling chỉ ra nút cổ chai thật.
Phần sau ta rời tầng con trỏ để về với công cụ quản lý mã đa module: Phần sau mổ xẻ go.work — cách làm việc với nhiều module cùng lúc trong một workspace mà không cần replace thủ công.