Đây là ý tưởng tôi thấy hay nhất của Go, và cũng là thứ khác Java nhiều nhất.
Không có implements
type Noi interface{ Noi() string }
type Cho struct{ Ten string }
func (c Cho) Noi() string { return c.Ten + ": gâu" }
var n Noi = Cho{"Milu"} -> Milu: gâu
Cho không khai rằng nó cài đặt Noi. Nó chỉ tình cờ có method đúng tên và đúng chữ ký, và thế là đủ. Trình biên dịch kiểm lúc gán.
Nghe như chuyện nhỏ, nhưng nó đảo ngược quan hệ phụ thuộc.
Interface thuộc về phía người dùng
Ở Java, interface do người viết thư viện định nghĩa, và người dùng phải nhận lấy. Muốn mock một lớp của thư viện bên thứ ba mà tác giả không tách interface? Không làm được.
Ở Go, bạn khai interface, cho chính nhu cầu của bạn:
// package của bạn
type LuuTru interface {
Luu(ctx context.Context, k string, v []byte) error
}
Bất kỳ kiểu nào có method đó — của bạn, của thư viện, của một SDK bạn không sửa được — đều dùng được. Tác giả thư viện không cần biết interface của bạn tồn tại.
Đây là câu trả lời trực tiếp cho lời khuyên "đừng mock thứ bạn không sở hữu" ở sê-ri Java: trong Go, bạn bọc nó bằng interface của mình và vấn đề biến mất.
Quy tắc cộng đồng hay nhắc: "Accept interfaces, return structs" — hàm nhận interface (để linh hoạt), trả về struct cụ thể (để người gọi thấy đủ thông tin).
Interface nên nhỏ
Thư viện chuẩn là ví dụ mẫu:
error : 1 method
io.Reader : 1 method
io.Writer : 1 method
fmt.Stringer : 1 method
sort.Interface: 3 method
io.Reader chỉ có Read(p []byte) (n int, err error). Và vì nó nhỏ, mọi thứ đều thoả mãn nó: tệp, kết nối mạng, bộ đệm trong bộ nhớ, tệp nén, thân HTTP request. Bạn viết một hàm nhận io.Reader và nó làm việc được với tất cả.
Câu châm ngôn của cộng đồng: "The bigger the interface, the weaker the abstraction."
Trong thực tế, interface một hoặc hai method là phổ biến nhất. Thấy interface mười method thì gần như chắc chắn nó đang mô tả một lớp cụ thể chứ không phải một hành vi.
Nhúng interface
type Doc interface{ Doc() string }
type Ghi interface{ Ghi(string) }
type DocGhi interface { Doc; Ghi }
Ghép interface nhỏ thành lớn mà không khai lại method. Thư viện chuẩn dùng nhiều: io.ReadWriter, io.ReadCloser, io.ReadWriteSeeker đều dựng theo kiểu này.
Kiểm tra lúc biên dịch
Vì không có implements, bạn mất một thứ: trình biên dịch không nhắc khi bạn định cài đặt interface mà viết sai tên method. Mã chỉ hỏng ở chỗ gán, có thể ở package khác.
Cách chuẩn để lấy lại:
var _ Noi = (*Meo)(nil)
Dòng này không tạo biến nào (tên là _), không tốn gì lúc chạy, nhưng báo lỗi biên dịch ngay tại tệp định nghĩa nếu *Meo không thoả mãn Noi.
Đặt nó ngay dưới định nghĩa kiểu. Bạn sẽ thấy dòng này trong hầu hết thư viện Go nghiêm túc.
Khi nào đừng dùng interface
Người đến từ Java hay tạo interface cho mọi thứ theo phản xạ. Trong Go, đó là mã thừa.
Đừng tạo interface khi chỉ có một cài đặt và bạn không cần thay thế nó trong test. Trả về struct. Cần interface sau này thì thêm — không phá vỡ gì cả, vì thoả mãn là ngầm định.
Đừng tạo interface "cho đủ tầng". UserService + UserServiceImpl là mẫu Java, và trong Go nó chỉ làm mã dài ra.
Đừng gói interface vào cùng package với cài đặt trừ khi thật sự cần. Đặt nó ở nơi dùng.
Cái giá phải trả
Interface không miễn phí:
Lời gọi qua interface không nội tuyến hoá được trong phần lớn trường hợp, nên nó chặn một loạt tối ưu của trình biên dịch.
Giá trị nằm trong interface thường phải lên heap, tạo thêm việc cho bộ thu gom rác.
Kích thước 16 byte — hai con trỏ, như tôi đo ở bài đo cho loạt này.
Với mã thường thì không đáng bận tâm. Với vòng lặp nóng hàng triệu lần thì có, và bài 52 sẽ đo.
Thử ba mươi giây
Tìm một kiểu bất kỳ trong dự án của bạn và thêm dòng này ngay dưới nó:
var _ fmt.Stringer = (*KieuCuaBan)(nil)
Nếu biên dịch được, kiểu đó đã có String() và fmt.Println đang gọi nó. Nếu không, bạn vừa biết mình đang in ra dạng mặc định {trường1 trường2} — và có lẽ nên viết String().
Ngày mai: any, type assertion và type switch — khi nào cần thoát khỏi hệ thống kiểu.