Go không có kế thừa. Nó có embedding, trông giống nhưng khác về bản chất — và bài này chỉ ra khác ở đâu.

Thăng cấp trường và method

type CoSo struct{ ID int }
func (c CoSo) MoTa() string { return fmt.Sprintf("CoSo#%d", c.ID) }

type Nguoi struct {
	CoSo            // NHÚNG: không có tên trường
	Ten string
}
  n.ID = 7                    (không cần n.CoSo.ID)
  n.MoTa() = "CoSo#7"         (method của CoSo gọi được qua Nguoi)

Trường và method của kiểu nhúng được thăng cấp lên ngoài. Trông y hệt kế thừa.

Nhưng không có quan hệ cha con

var c CoSo = n     // LỖI BIÊN DỊCH

Nguoi không phảiCoSo. Không có upcast, không có đa hình qua embedding, không có "gán con vào biến kiểu cha".

Đây là khác biệt cốt lõi. Embedding chỉ là đường tắt cú pháp: trình biên dịch viết hộ bạn n.CoSo.ID thành n.ID. Không có gì hơn thế.

Muốn đa hình thì dùng interface — và đó chính là cách Go tách hai chuyện mà kế thừa gộp làm một: dùng lại mã (embedding) và thay thế được cho nhau (interface).

Method ngoài che method nhúng

func (n Nguoi) MoTa() string { return "Nguoi " + n.Ten }
  n.MoTa()      = "Nguoi Minh"   <- bản của Nguoi
  n.CoSo.MoTa() = "CoSo#7"       <- bản gốc vẫn gọi được

Method ở tầng ngoài che method cùng tên của kiểu nhúng. Nhìn thì giống @Override, nhưng có một khác biệt quyết định:

Không có dispatch động. Nếu CoSo có một method khác gọi c.MoTa(), nó gọi bản của CoSo, không gọi bản của Nguoi. Ở Java, đó là điều ngược lại — và mẫu template method dựa hẳn vào hành vi đó.

Nên nếu bạn quen thiết kế "lớp cha gọi method trừu tượng do con cài đặt", mẫu đó không dịch được sang Go. Cách thay thế là truyền một interface hoặc một hàm vào.

Nhúng hai kiểu cùng method

type Ghep struct {
	CoSo    // có MoTa()
	Dong    // cũng có MoTa()
}
g.MoTa()
  ambiguous selector g.MoTa

Go không chọn hộ. Trình biên dịch báo lỗi và bạn phải viết rõ g.CoSo.MoTa() hoặc g.Dong.MoTa().

Đây là cách Go né bài toán kim cương của đa kế thừa: không có quy tắc phân giải phức tạp, chỉ có lỗi biên dịch và một yêu cầu viết rõ ràng.

Nhúng interface vào struct

Mẫu này ít gặp nhưng rất hữu ích:

type LogGhi struct {
	io.Writer          // nhúng INTERFACE
	tienTo string
}

LogGhi tự động thoả mãn io.Writer — nó chuyển tiếp Write cho giá trị bên trong. Bạn chỉ cần ghi đè method nào muốn đổi.

Ứng dụng thực tế nhất: giả lập một phần trong test. Nhúng interface, để nó nil, chỉ cài đặt method bạn cần. Method không cài mà bị gọi thì panic — và đó là cách nhanh để biết test đang chạm vào thứ bạn không ngờ.

Khi nào dùng embedding

Thêm hành vi cho một kiểu có sẵn — bọc io.Writer để thêm đếm byte, bọc http.Handler để thêm log.

Chia sẻ trường chung giữa các struct — CoSo{ID, TaoLuc, SuaLuc} nhúng vào nhiều entity.

Ghép interface nhỏ như bài 13 đã nói.

Đừng dùng nó để mô phỏng cây kế thừa. Nếu bạn thấy mình nhúng ba tầng, hãy dừng lại: gần như luôn có cách phẳng hơn bằng interface và composition thường.

Và nhớ một chi tiết dễ quên: struct nhúng vẫn là một trường, nên json.Marshal sẽ làm phẳng nó ra theo mặc định — thứ đôi khi bạn muốn, đôi khi không.

Thử ba mươi giây

type A struct{}
func (A) Chao() string { return "A" }
type B struct{ A }
func (B) Chao() string { return "B" }

var b B
fmt.Println(b.Chao(), b.A.Chao())

In ra B A. Cả hai vẫn tồn tại, và bạn chọn cái nào bằng cách viết đường dẫn — không có bảng ảo nào quyết định hộ.

Ngày mai: generics — Go 1.18 thêm gì, và khi nào không nên dùng.