Go không có class. Nó có struct — một tập hợp trường — và method gắn vào kiểu. Bài này về nửa đầu.
Bốn cách khởi tạo
a := Diem{1, 2} // theo vị trí
b := Diem{X: 1, Y: 2} // theo tên
c := Diem{X: 1} // thiếu trường -> zero value
var d Diem // tất cả zero value
vị trí={1 2} tên={1 2} thiếu trường={1 0} zero={0 0}
Luôn dùng dạng theo tên trong mã thật. Dạng theo vị trí gãy im lặng khi ai đó thêm hoặc đổi thứ tự trường — mã vẫn biên dịch, giá trị vào nhầm chỗ. Dạng theo tên thì thêm trường mới chỉ nhận zero value, không hỏng gì.
Đây cũng là lý do thư viện Go hay khai trường không xuất khẩu ở cuối struct: nó buộc người ngoài package phải dùng dạng theo tên.
Struct so sánh được
Diem{1,2} == Diem{1,2} -> true
Không cần viết equals, không cần hashCode. Go so từng trường một, và dùng làm khoá map được luôn.
Giới hạn đã nói ở bài 5: chỉ đúng khi mọi trường so sánh được. Struct chứa slice, map hay function thì == là lỗi biên dịch — và trình biên dịch báo ngay, không phải chờ tới lúc chạy.
Struct rỗng chiếm 0 byte
struct{}{} = 0 byte
[1000]struct{} = 0 byte
Một nghìn phần tử, không byte nào. Đây không phải trò vui — nó có hai công dụng thật:
Set: map[string]struct{} như bài 10 đã nói, không tốn byte nào cho giá trị.
Channel tín hiệu: chan struct{} khi bạn chỉ cần báo "có chuyện xảy ra" chứ không truyền dữ liệu. Bạn sẽ gặp mẫu này rất nhiều ở chặng đồng thời.
Thứ tự trường có giá
type Xau struct { A bool; B int64; C bool } // 24 byte
type Tot struct { B int64; A, C bool } // 16 byte
{bool, int64, bool} = 24 byte
{int64, bool, bool} = 16 byte
Cùng ba trường, cùng kiểu, chênh 33%.
Nguyên nhân là căn lề: int64 phải nằm ở địa chỉ chia hết cho 8. Ở struct đầu, sau A bool (1 byte) phải chèn 7 byte đệm để B căn đúng, rồi C lại kéo theo 7 byte đệm nữa.
Quy tắc: khai trường từ lớn xuống nhỏ. Với struct dùng vài cái thì không đáng bận tâm; với struct nằm trong slice hàng triệu phần tử thì 33% là thật.
Kiểm bằng unsafe.Sizeof, hoặc công cụ fieldalignment trong golang.org/x/tools.
Nhúng struct
type Co so struct{ ID int }
type Nguoi struct {
CoSo // NHÚNG, không có tên trường
Ten string
}
Trường của struct nhúng được thăng cấp lên ngoài: n.ID chạy được, không cần n.CoSo.ID.
Đây không phải kế thừa — chỉ là đường tắt cú pháp. Nguoi không phải là CoSo, và không có đa hình ở đây. Bài 15 sẽ nói kỹ, cùng lý do vì sao Go chọn cách này.
Trường không xuất khẩu
type Nguoi struct {
Ten string // chữ hoa: ngoài package thấy
diachi string // chữ thường: chỉ trong package
}
Đây là toàn bộ cơ chế đóng gói của Go — không có private, protected, public. Chi tiết ở bài 19.
Hệ quả cần biết ngay: encoding/json và mọi thư viện dùng phản chiếu chỉ thấy trường chữ hoa. Trường viết thường sẽ bị bỏ qua khi marshal, im lặng. Đây là lỗi rất hay gặp và sẽ gặp lại ở bài 45.
Tag: siêu dữ liệu cho phản chiếu
type Nguoi struct {
Ten string `json:"ten"`
Tuoi int `json:"tuoi,omitempty"`
}
Chuỗi sau tên trường là tag, và trình biên dịch không quan tâm nội dung — chỉ thư viện đọc nó qua phản chiếu. Sai chính tả trong tag không phải lỗi biên dịch, nên nó im lặng không có tác dụng.
Chú ý phải dùng dấu nháy ngược, và không có khoảng trắng quanh dấu hai chấm. json: "ten" (có khoảng trắng) là tag hợp lệ về cú pháp nhưng encoding/json không hiểu.
Thử ba mươi giây
type A struct{ X bool; Y int64; Z bool }
type B struct{ Y int64; X, Z bool }
fmt.Println(unsafe.Sizeof(A{}), unsafe.Sizeof(B{}))
In ra 24 16. Ba mươi giây, và nếu dự án của bạn có struct nằm trong slice lớn, đây là cách giảm bộ nhớ rẻ nhất bạn từng gặp.
Ngày mai: method và receiver — method set quyết định kiểu nào thoả mãn interface nào.