Slice là kiểu dữ liệu được dùng nhiều nhất trong Go, và cũng là nguồn của những bug khó hiểu nhất — sửa một slice bỗng dưng làm hỏng dữ liệu ở nơi khác, truyền slice vào hàm bị hàm đó thay đổi phần tử. Toàn bộ những hành vi này đến từ một sự thật đơn giản mà nhiều người bỏ qua: slice không phải mảng, nó là một header 24 byte. Hiểu ba trường của header đó là hiểu mọi hành vi slice. Bài này đo trực tiếp cấu trúc slice tận bytes.
Slice header: ba từ máy
Một slice trên máy 64-bit là một struct 24 byte gồm ba từ máy (mỗi từ 8 byte):
ptr: con trỏ tới phần tử đầu tiên của slice trong mảng nền (backing array).len: số phần tử hiện dùng được.cap: số phần tử mảng nền chứa được tính từptr.
Điểm mấu chốt: dữ liệu thực không nằm trong header — nó ở mảng nền, một vùng nhớ chỗ khác. Header chỉ trỏ vào đó. Và nhiều slice có thể cùng trỏ vào một mảng nền, chia sẻ dữ liệu.

Hình 1: Slice header 24 byte {ptr, len, cap}. Dữ liệu ở mảng nền chỗ khác. Sub-slice và truyền slice đều chia sẻ mảng nền — nguồn của sức mạnh lẫn bug.
Đo thật: cấu trúc và chia sẻ mảng nền
Dùng unsafe để soi ba trường của một slice và một sub-slice của nó:
type hdr struct{ ptr uintptr; len int; cap int }
h := (*hdr)(unsafe.Pointer(&s)) // đọc header của s
sub := s[2:4] // sub-slice

Hình 2: Header = 24 byte. s có ptr=...2000, len=5, cap=10. s[2:4] có ptr=...2010 (lệch đúng 16 byte = 2 int), len=2, cap=8 — chia sẻ mảng nền. Sửa sub[0]=999 làm s[2] đổi theo. Truyền vào hàm sửa x[0]=-1 làm s[0] đổi thật.
Kết quả cho thấy toàn bộ hành vi slice:
- Header = 24 byte (3 từ máy), đúng như thiết kế.
- Sub-slice chia sẻ mảng nền:
s[2:4]cóptrlệch đúng 16 byte (2 phần tửint) so vớiptrcủas. Chúng trỏ vào cùng một mảng nền, chỉ khác điểm bắt đầu và độ dài.capcủa sub là 8 (từ vị trí 2 tới hết mảng nền cap 10). - Aliasing: sửa
sub[0] = 999thực chất sửas[2]— vì cả hai trỏ cùng ô nhớ.sđổi từ[0 10 20 30 40]thành[0 10 999 30 40]. - Truyền slice = copy header, chia sẻ data:
suaPhanTu(s)nhận một bản copy của header 24 byte, nhưngptrtrong bản copy trỏ cùng mảng nền, nênx[0] = -1sửas[0]thật.
Ứng dụng thực tế
Truyền slice vào hàm luôn rẻ. Dù mảng nền lớn cỡ nào (1 phần tử hay 1 triệu), truyền slice chỉ copy 24 byte header. Bạn không cần truyền *[]T để "tránh copy" — slice đã hiệu quả. (Truyền con trỏ chỉ cần khi hàm phải thay đổi len/cap của slice gốc, ví dụ append.)
Cẩn thận aliasing khi cắt slice. Nếu bạn cắt một slice (sub := s[a:b]) rồi sửa sub, bạn đang sửa s. Đây là nguồn bug kinh điển: một hàm nhận slice, cắt nó, sửa phần cắt, và vô tình làm hỏng slice của người gọi. Nếu cần bản độc lập, phải copy ra slice mới.
append có thể phá vỡ chia sẻ — hoặc không. Đây là chỗ tinh vi nhất: append vào một sub-slice, nếu còn cap thừa, sẽ ghi đè lên mảng nền chung (ảnh hưởng slice khác); nếu hết cap, nó cấp mảng mới (tách khỏi chia sẻ). Hành vi phụ thuộc cap — bài sau về slice growth sẽ đo kỹ.
Đánh đổi cần cân nhắc
Chia sẻ mảng nền vừa là sức mạnh vừa là bẫy. Nó khiến slice cực nhanh (không copy data khi truyền/cắt), nhưng cũng khiến việc sửa dữ liệu lan ra ngoài ý muốn. Quy tắc an toàn: nếu một slice sẽ bị nhiều nơi giữ và sửa độc lập, copy ra bản riêng; nếu chỉ đọc hoặc một nơi sửa, chia sẻ là ổn và nhanh.
Sub-slice giữ cả mảng nền sống. Một hệ quả về bộ nhớ: small := big[:10] giữ toàn bộ mảng nền của big sống, không chỉ 10 phần tử — vì ptr vẫn trỏ vào mảng lớn. Nếu big là 1 triệu phần tử mà bạn chỉ cần 10, đây là rò bộ nhớ (bài phát hiện rò đã nhắc). Cách sửa: copy 10 phần tử ra slice mới, thả big.
Đừng lạm dụng unsafe để đọc header. Bài này dùng unsafe để minh hoạ cấu trúc, nhưng trong code thật đừng đọc header bằng unsafe (dễ sai khi bố cục đổi). Nếu cần, dùng reflect.SliceHeader (đã deprecated) hoặc unsafe.SliceData/unsafe.Slice (Go 1.20+) — API chính thức.
Ba ý mang về
- Slice là header 24 byte {ptr, len, cap}, không chứa dữ liệu: đo thật
unsafe.Sizeof= 24 byte trên 64-bit — dữ liệu ở mảng nền chỗ khác, và nhiều slice có thể cùng trỏ vào một mảng nền. - Sub-slice và cắt slice chia sẻ mảng nền: đo thật
s[2:4]có ptr lệch đúng 16 byte (2 int) so vớis, nên sửasub[0]làms[2]đổi theo — nguồn của bug aliasing kinh điển; cần bản độc lập thì phảicopy. - Truyền slice copy header không copy data: chỉ 24 byte dù mảng lớn cỡ nào (nên luôn rẻ), nhưng hàm sửa được phần tử của bạn vì cùng trỏ mảng nền — và sub-slice giữ cả mảng nền lớn sống, một nguồn rò bộ nhớ.
Phần sau ta đo chính cơ chế mà bài này nhắc tới: Phần sau mổ xẻ chiến lược tăng trưởng slice khi append vượt cap — Go nhân đôi hay nhân 1.25, khi nào tách mảng nền, và cách chọn cap ban đầu để tránh copy thừa.