Embedding qua một ví dụ
Một trường có kiểu nhưng không có tên là trường được nhúng (embedded field). Các trường và phương thức của kiểu được nhúng trở nên truy cập được trực tiếp trên kiểu bên ngoài.
Đầu ra:
Ana
Hi, I'm Ana
ana@example.com
{User:{Name:Ana Email:ana@example.com} Level:2}
Tên của trường được nhúng chính là tên kiểu của nó, User. Đó là cách bạn gọi nó trong literal (User: User{...}) và khi cần chính giá trị bên trong (a.User). Không thể gán một trường được nâng bằng tên ngắn của nó trong literal: Admin{Name: "Ana"} báo lỗi unknown field Name in struct literal of type Admin.
Phương thức được nâng thỏa mãn interface
Việc nâng thêm các phương thức của kiểu được nhúng vào tập phương thức của kiểu bên ngoài. Vì vậy kiểu bên ngoài thỏa mãn mọi interface mà kiểu bên trong thỏa mãn.
Quy tắc về tập phương thức đi theo kiểu được nhúng. Nhúng T sẽ nâng các phương thức có receiver giá trị của T lên cả giá trị lẫn con trỏ của kiểu bên ngoài, nhưng các phương thức có receiver con trỏ của *T chỉ được nâng lên con trỏ của kiểu bên ngoài. Nhúng *T thì nâng cả hai tập lên cả hai. Nếu phân vân, hãy nhúng con trỏ, hoặc dùng kiểu bên ngoài qua con trỏ.
Nhúng con trỏ có cái giá của nó: con trỏ có thể là nil. Service{} không có Logger vẫn biên dịch được, và khi đó svc.Log(...) sẽ panic.
Embedding không phải kế thừa
Embedding trông giống tạo lớp con, nhưng có ba điểm hoạt động khác với Java hay Python.
Kiểu bên ngoài không phải là kiểu bên trong. Admin không phải là User. Một hàm nhận User sẽ không chấp nhận Admin; hãy truyền a.User.
Không có virtual dispatch. Phương thức được nâng chạy trên giá trị được nhúng, và nó không biết gì về kiểu bên ngoài. Nếu nó gọi một phương thức khác, nó gọi phiên bản của chính kiểu nó, kể cả khi kiểu bên ngoài định nghĩa một phương thức cùng tên.
Đầu ra:
woof
The animal says ...
Trong ngôn ngữ dựa trên lớp, Speak sẽ in ra "woof". Trong Go, d.Speak() là cách viết tắt của d.Animal.Speak(), và receiver của lời gọi đó là Animal. Khi muốn hành vi thay đổi theo kiểu, hãy dùng interface: truyền một Sounder vào đoạn code cần nó.
Kiểu bên trong không biết mình đang được nhúng. Không có super. Để mở rộng một phương thức được nâng, hãy định nghĩa phương thức cùng tên trên kiểu bên ngoài và gọi tường minh phương thức bên trong:
func (a Admin) Greet() string {
return a.User.Greet() + " (admin)"
}
Xung đột tên và che khuất
Một trường hay phương thức trên kiểu bên ngoài sẽ che khuất trường hay phương thức được nâng cùng tên, như Dog.Sound ở trên. Tên ở tầng nông hơn sẽ thắng.
Khi hai kiểu được nhúng ở cùng độ sâu cùng nâng một tên, tên đó trở nên mơ hồ. Chương trình vẫn biên dịch được miễn là không chỗ nào dùng tên mơ hồ đó:
Giải quyết xung đột bằng cách dùng đường dẫn đầy đủ, hoặc định nghĩa ID trên kiểu bên ngoài.
Nhúng interface
Interface có thể nhúng các interface khác. Thư viện chuẩn dựng các interface lớn hơn theo cách này:
type ReadWriter interface {
Reader
Writer
}
Struct cũng có thể nhúng một interface. Khi đó struct thỏa mãn interface đó thông qua giá trị bạn lưu trong trường, và bạn chỉ ghi đè những phương thức mình quan tâm. Đây là mẫu decorator với gần như không tốn code:
Lời gọi tường minh c.Reader.Read(p) là bắt buộc. Viết c.Read(p) bên trong Read sẽ tự gọi chính nó mãi mãi.
Nhúng interface vào một đối tượng giả trong test là lối tắt hay dùng: nhúng interface lớn, cài đặt đúng một phương thức test cần, và bỏ qua phần còn lại. Phương thức nào chưa cài đặt mà bị gọi sẽ gây panic do dereference con trỏ nil, thường đó lại là điều bạn muốn trong test.
Một công dụng thực tế phổ biến: nhúng sync.Mutex
type Stats struct {
sync.Mutex
hits map[string]int
}
func (s *Stats) Hit(page string) {
s.Lock()
defer s.Unlock()
s.hits[page]++
}
Cách này đọc dễ, nhưng nó cũng xuất Lock và Unlock thành một phần API của Stats, nên nơi gọi nào cũng có thể khóa struct của bạn. Với kiểu dùng bên ngoài package, một trường có tên (mu sync.Mutex) giữ khóa ở chế độ riêng tư. Sự đánh đổi này áp dụng cho mọi trường hợp embedding: mọi thứ kiểu được nhúng xuất ra đều trở thành một phần giao diện công khai của kiểu bạn.
Lỗi thường gặp
- Mong đợi virtual dispatch. Phương thức được nâng không bao giờ gọi các bản ghi đè của kiểu bên ngoài.
- Gán trường được nâng trong literal. Hãy dùng tên kiểu được nhúng:
Admin{User: User{Name: "Ana"}}. - Con trỏ được nhúng là nil.
*Thay interface được nhúng phải được gán trước khi gọi phương thức của nó. - Vô tình mở rộng API. Embedding xuất mọi phương thức được export của kiểu bên trong lên kiểu của bạn.
Câu hỏi thường gặp
Nhúng struct (struct embedding) trong Go là gì?
Là khai báo một trường chỉ có kiểu mà không có tên: type Admin struct { User; Level int }. Các trường và phương thức của User được nhúng sẽ được nâng lên, nên a.Name và a.Greet() dùng được trực tiếp trên Admin. Giá trị được nhúng vẫn là một trường bình thường, truy cập được qua a.User.
Go có kế thừa không?
Không. Go không có lớp và không có kế thừa kiểu con. Embedding cho phép tái sử dụng code bằng composition: kiểu bên ngoài có được các phương thức của kiểu bên trong, nhưng Admin không phải là User. Bạn không thể truyền Admin vào chỗ cần User, và phương thức được nâng không thể gọi ngược lại phương thức của kiểu bên ngoài. Đa hình trong Go đến từ interface.
Khởi tạo struct được nhúng trong Go như thế nào?
Trong composite literal, gọi trường được nhúng bằng tên kiểu của nó: Admin{User: User{Name: "Ana"}, Level: 2}. Bạn không thể gán trực tiếp các trường được nâng trong literal: Admin{Name: "Ana"} báo lỗi unknown field Name in struct literal of type Admin.
Có nhúng interface vào struct trong Go được không?
Có. Khi đó struct thỏa mãn interface thông qua giá trị được nhúng, và bạn có thể ghi đè từng phương thức. Cách này hay dùng cho decorator và đối tượng giả trong test. Nếu trường interface được nhúng là nil, gọi một phương thức bạn chưa ghi đè sẽ gây panic do dereference con trỏ nil.