any là cửa thoát khỏi hệ thống kiểu của Go. Bài này về cách dùng nó cho đúng, và vì sao dùng nó nhiều là dấu hiệu xấu.
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.
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)
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.
Thử 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. 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.
Ngày mai: embedding — Go thay kế thừa bằng cái gì.