Interface là một tập phương thức
Một kiểu interface liệt kê các chữ ký phương thức (method). Kiểu nào có những phương thức đó thì thỏa mãn interface, mà không cần khai báo nào nói ra điều đó.
Cả Rect lẫn Circle đều không nhắc đến Shape. Đó là triển khai ngầm định, đặc điểm cốt lõi của interface trong Go. Nhờ vậy, bạn có thể định nghĩa trong package của mình một interface mà các kiểu từ package khác đã thỏa mãn sẵn, không cần đụng vào code của chúng.
Các interface nhỏ trong thư viện chuẩn
Code Go ưa chuộng interface chỉ có một hoặc hai phương thức. Những interface quan trọng nhất:
| Interface | Phương thức | Được dùng bởi |
|---|---|---|
fmt.Stringer | String() string | việc in ra của fmt |
error | Error() string | mọi hàm có thể thất bại |
io.Reader | Read(p []byte) (n int, err error) | tệp, mạng, gzip, body HTTP |
io.Writer | Write(p []byte) (n int, err error) | tệp, buffer, hàm băm, response HTTP |
sort.Interface | Len, Less, Swap | package sort |
http.Handler | ServeHTTP(w, r) | net/http |
Vì io.Reader chỉ có một phương thức, hàng chục kiểu triển khai nó, và hàm nào nhận một io.Reader cũng làm việc được với tất cả các kiểu đó:
Câu tục ngữ của Go là "interface càng lớn, trừu tượng càng yếu". Interface lớn hơn được xây bằng cách ghép các interface nhỏ: io.ReadWriter là Reader cộng Writer, viết bằng cách nhúng một interface vào interface khác.
any: interface rỗng
interface{} không có phương thức nào, nên mọi kiểu đều thỏa mãn nó. Go 1.18 thêm any làm alias; hai cái hoàn toàn giống nhau.
Một giá trị any chứa được mọi thứ nhưng bạn gần như không làm được gì với nó cho đến khi lấy lại kiểu cụ thể bằng type assertion hoặc type switch. Hãy ưu tiên một interface thật hoặc generics khi đã biết tập các kiểu. any hợp với dữ liệu thật sự động, như JSON đã giải mã mà không biết trước cấu trúc, và với việc in ra.
Một giá trị interface chứa gì
Một giá trị interface là một cặp: kiểu động và giá trị động. var s Shape = Rect{3, 4} lưu kiểu Rect và một bản sao của giá trị. Gọi s.Area() sẽ tra phương thức của Rect lúc chạy.
Một interface chỉ là nil khi cả hai phần đều trống. Quy tắc đó gây ra bug khó hiểu nhất trong Go.
Bẫy interface nil
Một con trỏ nil được lưu trong interface tạo ra một interface khác nil.
Đầu ra:
false
*main.MyError true
true
validate(true) trả về một interface error chứa kiểu *MyError và giá trị nil. Interface đó có kiểu, nên nó không bằng nil, và nhánh if err != nil của nơi gọi được chạy. Gọi err.Error() ở đó sẽ gây panic khi truy cập trường của receiver nil.
Cách sửa rất đơn giản: khai báo biến với kiểu error, không phải kiểu con trỏ cụ thể, hoặc trả về nil trực tiếp ở nhánh thành công. Đừng bao giờ trả về một kiểu con trỏ lỗi cụ thể từ hàm có kết quả là error. Cái bẫy này áp dụng cho mọi interface, không chỉ lỗi.
Kiểm tra một kiểu có triển khai interface không
Việc triển khai được kiểm tra ở chỗ một giá trị được gán cho interface. Nếu chưa có code nào làm vậy, một lỗi trong chữ ký phương thức sẽ không bị phát hiện. Một phép gán rỗng ở cấp package giúp việc kiểm tra trở nên rõ ràng:
var _ io.Writer = (*LogWriter)(nil)
var _ fmt.Stringer = Temp(0)
Việc này không tốn gì lúc chạy. Nếu *LogWriter có Write(p []byte) error thay vì Write(p []byte) (int, error), quá trình build thất bại:
cannot use (*LogWriter)(nil) (value of type *LogWriter) as io.Writer value in variable declaration: *LogWriter does not implement io.Writer (wrong type for method Write)
have Write([]byte) error
want Write([]byte) (int, error)
Pointer receiver và interface
Nếu một phương thức có pointer receiver, chỉ kiểu con trỏ mới có phương thức đó. *Counter thỏa mãn interface nhờ nó; Counter thì không. Trình biên dịch báo Counter does not implement Incrementer (method Inc has pointer receiver). Hãy lưu &Counter{} vào interface. Trang phương thức giải thích về method set.
Nhận interface, trả về struct
Một nguyên tắc phổ biến trong Go: hàm nên nhận tham số là interface và trả về kiểu cụ thể.
- Nhận một interface cho phép nơi gọi truyền vào bất cứ thứ gì phù hợp, kể cả đối tượng giả trong test. Một hàm đọc dữ liệu nên nhận
io.Reader, không phải*os.File. - Trả về một kiểu cụ thể cho phép nơi gọi dùng mọi phương thức và trường của nó, và tránh được bẫy interface nil.
os.Opentrả về*os.File, không phảiio.Reader.
Một thói quen liên quan: định nghĩa interface ở nơi dùng nó, không phải ở nơi triển khai nó. Nếu service của bạn cần thứ gì đó có thể Get(id) một người dùng, hãy khai báo một interface một phương thức trong package của service, và để package cơ sở dữ liệu chỉ cần export struct của nó.
So sánh các giá trị interface
Hai giá trị interface bằng nhau khi kiểu động của chúng giống hệt nhau và giá trị động bằng nhau. Nếu kiểu động không so sánh được (một slice, một map), == vẫn biên dịch được nhưng gây panic lúc chạy: runtime error: comparing uncomparable type []int.
Lỗi thường gặp
- Trả về một con trỏ nil có kiểu dưới dạng interface. Hãy trả về
niltrực tiếp. - Tạo interface quá sớm. Hãy viết kiểu cụ thể trước. Thêm interface khi có cách triển khai thứ hai hoặc khi test cần đến.
- Con trỏ tới interface.
*io.Readergần như không bao giờ đúng. Một interface vốn đã giữ con trỏ khi bạn lưu con trỏ vào nó. - Interface lớn. Interface mười phương thức khó triển khai và khó làm giả. Hãy tách chúng ra.
Câu hỏi thường gặp
Triển khai một interface trong Go như thế nào?
Định nghĩa trên kiểu của bạn các phương thức mà interface liệt kê, với cùng tên và chữ ký. Không có từ khóa implements. Nếu *File có Read(p []byte) (int, error), nó tự động là một io.Reader. Trình biên dịch kiểm tra điều này ở bất cứ chỗ nào bạn gán giá trị cho kiểu interface.
Interface rỗng hay any trong Go là gì?
interface{} không có phương thức nào, nên mọi kiểu đều thỏa mãn nó. Từ Go 1.18, any là một alias có sẵn của interface{}. Một giá trị kiểu any chứa được mọi thứ, nhưng bạn cần type assertion hoặc type switch để lấy lại kiểu cụ thể.
Vì sao interface trong Go không phải nil dù tôi đã gán một con trỏ nil?
Một giá trị interface chứa một kiểu và một giá trị. Gán một *MyError nil cho một error sẽ cho ra interface có kiểu là *MyError và giá trị là nil, và interface đó không bằng nil. Khi không có lỗi, hãy trả về nil trực tiếp thay vì một con trỏ nil có kiểu.
Làm sao kiểm tra lúc biên dịch rằng một kiểu triển khai một interface?
Thêm một phép gán rỗng ở cấp package: var _ io.Reader = (*MyReader)(nil). Nếu *MyReader thiếu một phương thức, quá trình build thất bại với thông báo nêu tên phương thức còn thiếu.