Hãy coi any như một cái hộp các-tông không dán nhãn. Bạn bỏ được bất cứ thứ gì vào — một số nguyên, một chuỗi, một struct — và đó chính là sức mạnh của nó. Nhưng khi cần lấy ra, bạn đứng trước một cái hộp câm lặng và phải đoán bên trong là gì. Đoán đúng thì dùng được; đoán sai thì, tuỳ cách bạn mở, hoặc nhận một câu trả lời "không phải", hoặc làm cả chương trình vỡ tan. Cả bài này là về cách mở cái hộp đó cho an toàn, và vì sao hễ thấy mình dùng nó nhiều thì gần như chắc chắn đã chọn sai hộp. any là cửa thoát khỏi hệ thống kiểu của Go.
any chỉ là tên khác của interface{}
var x any = 42
Từ Go 1.18, any là bí danh của interface{} — hoàn toàn tương đương, chỉ dễ đọc hơn. Dùng any trong mã mới.
Vì interface rỗng không đòi method nào, mọi kiểu đều thoả mãn nó. Đó là điểm mạnh và cũng là vấn đề: bạn nhận được giá trị nhưng mất hết thông tin kiểu — đúng cái hộp không nhãn.
Kích thước của nó là 16 byte — một con trỏ tới thông tin kiểu, một con trỏ tới dữ liệu. Đây là lý do người ta hay nói "interface trong Go là một cặp (kiểu, giá trị)", và bài 17 sẽ cho thấy cặp đó gây ra cái bẫy lớn nhất của ngôn ngữ.
Type assertion: hai dạng
v := x.(int) // dạng một giá trị -> PANIC nếu sai kiểu
v, ok := x.(int) // dạng comma-ok -> ok=false nếu sai
x.(int) -> 42, ok=true
x.(string) -> ok=false (không panic vì dùng comma-ok)
Dạng một giá trị là xé toạc hộp ra, tin chắc bên trong là cái mình nghĩ. Dạng comma-ok là lắc hộp hỏi "có phải int không?" rồi mới mở. Luôn dùng dạng comma-ok trừ khi bạn tuyệt đối chắc chắn về kiểu. Dạng một giá trị là một dòng panic đang chờ đến giờ, và nó rất khó phát hiện khi đọc mã vì trông giống một phép ép kiểu vô hại.
Đây là quy tắc tương tự comma-ok của map ở bài 10 — Go dùng lại đúng mẫu cú pháp đó cho cả hai.
Type switch
Khi có nhiều kiểu có thể xảy ra:
switch v := x.(type) {
case int:
fmt.Printf("số nguyên %d\n", v)
case string:
fmt.Printf("chuỗi %s\n", v)
case nil:
fmt.Println("nil")
default:
fmt.Printf("kiểu khác: %T\n", v)
}
type switch: int 42
Bên trong mỗi case, v có kiểu cụ thể của case đó — không cần ép kiểu thêm. Ở default, v giữ kiểu any.
Chú ý case nil bắt được trường hợp interface thật sự nil. Và một case liệt kê nhiều kiểu thì v vẫn là any, vì trình biên dịch không biết chọn kiểu nào.
Động từ %T in ra kiểu thật — công cụ gỡ lỗi hữu ích nhất khi làm việc với any.
Khi nào dùng any là hợp lý
Danh sách này ngắn, và đó là chủ ý:
API nhận dữ liệu tuỳ ý — fmt.Println(a ...any), json.Marshal(v any). Chúng phải nhận mọi thứ.
Phân tích dữ liệu không có lược đồ — json.Unmarshal vào map[string]any khi bạn thật sự không biết cấu trúc. Bài 45 sẽ nói kỹ.
Bọc giá trị trong hệ thống chung — cache, hàng đợi, log có cấu trúc.
Trước Go 1.18, any là cách duy nhất để viết hàm dùng chung cho nhiều kiểu. Giờ đã có generics, và bài 16 sẽ cho thấy nó thay được phần lớn trường hợp đó.
Khi nào any là dấu hiệu sai
Nếu bạn thấy mình viết:
func XuLy(v any) error {
switch t := v.(type) {
case DonHang: ...
case KhachHang: ...
}
}
thì gần như chắc chắn nên có một interface với method chung, hoặc một kiểu tổng rõ ràng. Type switch trên các kiểu của chính bạn là cách né hệ thống kiểu, và trình biên dịch không còn giúp được gì — thêm một kiểu mới, không ai nhắc bạn cập nhật switch.
Go không có sealed interface như Java, nên không có cách nào bắt trình biên dịch kiểm đủ nhánh. Vì vậy nếu buộc phải dùng mẫu này, luôn có default trả về lỗi rõ ràng thay vì im lặng bỏ qua.
Chuyển đổi giữa các interface
Type assertion cũng dùng để hỏi "giá trị này có thoả mãn interface kia không":
if c, ok := r.(io.Closer); ok {
defer c.Close()
}
Mẫu này rất phổ biến trong thư viện chuẩn: nhận io.Reader, kiểm xem nó có phải io.Closer không, nếu có thì đóng. Nó cho phép API linh hoạt mà không bắt mọi cài đặt phải có Close.
Và nếu muốn tự tay thấy cái giá của việc mở hộp ẩu, thử đúng ba mươi giây:
var x any = "chuỗi"
n := x.(int)
panic: interface conversion: interface {} is string, not int
Thêm , ok vào và chương trình chạy tiếp bình thường. Ba mươi giây, và đó là khác biệt giữa một dịch vụ chết lúc 2 giờ sáng và một dòng log vô hại.
Mẫu số chung
Mọi ngôn ngữ có kiểu tĩnh đều phải chừa một cửa thoát khỏi hệ thống kiểu — và đi kèm nó luôn là câu hỏi "làm sao lấy lại kiểu cụ thể cho an toàn". Go gọi cửa đó là any; chỗ khác tên khác, nhưng bài toán y hệt.
- Java dùng
Objectcộnginstanceofrồi ép kiểu, vớiClassCastExceptionđóng vai trò cái panic. Từ Java 16, so khớp mẫu (if (x instanceof Integer i)) vàswitchtrên mẫu chính là type switch của Go viết theo kiểu Java — gộp luôn bước kiểm và bước gán. - C# tách đúng hai dạng như Go: toán tử
astrả vềnullkhi sai (y hệt comma-ok, không ném), còn ép kiểu thẳng thì ném ngoại lệ (y hệt dạng một giá trị). Cộng thêmswitchtrên mẫu. - TypeScript mã hoá chính kỷ luật comma-ok vào hệ thống kiểu:
anytắt hết kiểm tra (nguy hiểm), cònunknownbắt buộc bạn thu hẹp kiểu trước khi dùng — tức là trình biên dịch ép bạn "lắc hộp hỏi" thay vì xé ẩu. - Rust gần như không có cửa thoát theo mặc định: thay vì một hộp không nhãn, bạn khai một kiểu tổng (
enum) rồimatch— đúng cái "kiểu tổng rõ ràng" mà phần trên khuyên.Box<dyn Any>có tồn tại nhưng hiếm dùng và bị khuyên tránh.
Sợi chỉ chung đáng mang theo có hai tầng. Tầng cú pháp: ở đâu cũng có dạng an toàn (hỏi trước) và dạng ẩu (vỡ nếu sai) — comma-ok so với x.(int), as so với ép kiểu, instanceof-mẫu so với ép trần; chọn dạng an toàn là kỷ luật không tốn gì. Tầng thiết kế, quan trọng hơn: hễ bạn mở hộp không nhãn trên các kiểu của chính mình, nghĩa là bạn đã bỏ qua một interface hoặc một kiểu tổng đáng lẽ phải khai. Rust làm điều đó hiển nhiên bằng cách gần như không cho bạn cái hộp; TypeScript làm nó hiển nhiên bằng unknown; còn Go và Java cho bạn tự do, nên kỷ luật nằm ở bạn. Cái hộp không nhãn luôn có ích ở biên hệ thống — nơi dữ liệu thật sự chưa có hình — nhưng càng đi sâu vào trong miền nghiệp vụ của mình, thấy nó là thấy một cái nhãn lẽ ra nên dán.
Ngày mai: embedding — Go thay kế thừa bằng cái gì.