Bài boxing đo chi phí đưa một giá trị vào interface. Bài này đo thao tác ngược: lấy giá trị ra — h.(Vuong), h.(type). Nhiều người ngại type assertion vì nghĩ nó "đắt như reflection" hay "type switch nhiều nhánh thì chậm dần". Cả hai đều sai. Ta sẽ đọc assembly để thấy cơ chế thật, rồi đo bằng benchmark.

Cơ chế: interface là cặp {itab, data}

Nhắc lại từ bài interface internals: một giá trị interface là cặp {itab, data}. itab là con trỏ tới một bảng mô tả cặp (kiểu động, interface) — và quan trọng: mỗi cặp (kiểu, interface) chỉ có đúng một itab duy nhất trong toàn chương trình. Điều này biến việc kiểm kiểu thành so sánh con trỏ.

Ảnh chụp đoạn mã Go nền tối minh hoạ cơ chế tầng thấp của type assertion và type switch, interface là cặp itab và data kiểm kiểu bằng so sánh con trỏ itab O 1 không quét không phản chiếu, một assert tới kiểu cụ thể dạng comma-ok var h Hinh bằng Vuong 2 nếu v ok bằng h chấm Vuong ok thì ok true nếu kiểu động đúng là Vuong v là Vuong đã lấy ra không panic dạng một trả về sẽ panic nếu sai kiểu dạng comma-ok trả ok false an toàn, hai cơ chế thật nhìn assembly arm64 h chấm Vuong biên dịch thành MOVD địa chỉ go itab main Vuong main Hinh tĩnh CMP R2 R0 so với itab trong interface BNE sai khác thì nhánh thất bại chỉ một phép so sánh con trỏ itab của mỗi cặp kiểu interface là duy nhất nên so địa chỉ là đủ, ba type switch bảng nhảy theo con trỏ kiểu switch v bằng h chấm type case Vuong sinkF cộng v c case Tron sinkF cộng v r default nhiều nhánh không so tuần tự từng cái compiler dựng bảng băm nhảy trên con trỏ kiểu nên 8 nhánh nhanh ngang 2 nhánh, bốn assert tới interface khác tra itab nếu c ok bằng h chấm CoDientich kiểu động có thoả interface kia không không phải so một con trỏ tĩnh mà tra tính itab cho cặp kiểu động CoDientich đắt hơn assert cụ thể một chút nhưng vẫn 0 cấp phát

Hình 1: Bốn thao tác. Assert cụ thể so một con trỏ itab tĩnh; type switch dựng bảng nhảy; assert tới interface khác tra itab của kiểu động.

Đọc assembly: assert cụ thể là một phép so con trỏ

Đây là phần thuyết phục nhất. Biên dịch h.(Vuong) với -gcflags=-S trên arm64, phần cốt lõi ra đúng ba lệnh:

MOVD  $go:itab.main.Vuong,main.Hinh(SB), R2  // nạp địa chỉ itab TĨNH của (Vuong, Hinh)
CMP   R2, R0                                 // so với itab trong interface
BNE   sai                                    // khác → nhánh thất bại

Compiler biết tĩnh lúc biên dịch địa chỉ itab của cặp (Vuong, Hinh) — nó là một hằng địa chỉ. Nên assert chỉ là: nạp hằng đó, so với con trỏ itab trong giá trị interface, rẽ nhánh. Không quét bảng, không gọi runtime, không phản chiếu. Đó là lý do nó ~1 ns.

Đo thật: assert rẻ hơn cả một lời gọi

Benchmark bốn thao tác, dùng slice kiểu động trộn (Vuong/Tron) cấp package để buộc kiểu động thật (tránh compiler devirtualize):

Ảnh chụp bảng kết quả đo thật nền tối chi phí type assertion type switch Go 1.23 arm64 10 core slice kiểu động trộn để buộc dispatch thật, bốn thao tác trên interface chú ý cột allocs op AssertCuThe assert Vuong comma-ok 1,166 ns/op 0 B/op 0 allocs/op TypeSwitch 2 nhánh Vuong Tron 1,211 ns 0 0 GoiTrucTiep baseline gọi qua interface 1,881 ns 0 0 AssertInterface assert CoDientich cộng gọi 1,902 ns 0 0 mọi thao tác 0 cấp phát assert cụ thể và type switch khoảng 1,2 ns rẻ hơn cả một lời gọi qua interface khoảng 1,88 ns chúng chỉ là so con trỏ còn dispatch phải nhảy gián tiếp, type switch không tuyến tính theo số nhánh Switch 2 nhánh 1,206 ns 0 Switch 8 nhánh khớp mọi kiểu 1,195 ns 0 tám nhánh nhanh ngang hai nhánh compiler dựng bảng nhảy theo con trỏ kiểu không so tuần tự từng case thêm case gần như miễn phí, cốt lõi assert cụ thể một phép so con trỏ itab tĩnh khoảng 1,2 ns type switch bảng nhảy theo con trỏ kiểu O 1 theo số case assert interface tra itab đắt hơn chút vẫn 0 alloc tất cả 0 cấp phát lấy giá trị ra khỏi box không tốn heap

Hình 2: AssertCuThe 1,166 ns và TypeSwitch 1,211 ns — rẻ hơn baseline gọi qua interface (1,881 ns). Mọi thao tác 0 cấp phát. Switch 8 nhánh (1,195 ns) nhanh ngang Switch 2 nhánh (1,206 ns).

Con số:

  • Assert cụ thể (h.(Vuong), comma-ok): 1,166 ns, 0 cấp phát.
  • Type switch (2 nhánh): 1,211 ns, 0 cấp phát.
  • Gọi qua interface (baseline, không assert): 1,881 ns.
  • Assert tới interface khác (h.(CoDientich) rồi gọi): 1,902 ns.

Điều đáng chú ý: assert cụ thể và type switch (~1,2 ns) rẻ hơn cả một lời gọi phương thức qua interface (~1,88 ns). Vì assert chỉ là so con trỏ, còn dispatch phải nhảy gián tiếp qua itab. Và tất cả 0 cấp phát — lấy giá trị ra khỏi "hộp" interface không tốn heap (ngược với đưa vào có thể tốn, như bài trước đo).

Type switch không tuyến tính theo số nhánh

Ngộ nhận phổ biến: "type switch 8 case phải chậm hơn 2 case vì nó thử từng cái". Đo thật bác bỏ:

  • Switch 2 nhánh: 1,206 ns.
  • Switch 8 nhánh (khớp mọi kiểu): 1,195 ns.

Bằng nhau. Compiler không so tuần tự từng case; nó dựng một bảng nhảy (jump table) dựa trên con trỏ kiểu, nên tra nhánh đúng là O(1) bất kể số case. Thêm một case gần như miễn phí. Đây là lý do type switch là công cụ chính đáng để phân loại kiểu, không cần né vì sợ chậm.

Ứng dụng thực tế

errors.As và phân loại lỗi. errors.As(err, &target) bản chất là một chuỗi type assertion đi qua chuỗi Unwrap(). Mỗi bước là một phép so itab rẻ. Đừng ngại phân loại lỗi bằng type switch/errors.As — chi phí không đáng kể so với logic xử lý lỗi.

Xử lý giá trị động từ JSON/any. Khi giải mã JSON vào any, bạn nhận map[string]any, []any, float64, string... và phải type switch để xử lý. Đây là chỗ interface thật sự cần thiết (kiểu không biết lúc biên dịch) — và type switch ở đây rẻ, cứ dùng thoải mái.

comma-ok để tránh panic. Luôn ưu tiên dạng v, ok := h.(T) thay v := h.(T) khi không chắc kiểu. Dạng một-trả-về panic nếu sai kiểu; dạng comma-ok trả ok=false an toàn, và như đo được, không đắt hơn gì.

Đánh đổi cần cân nhắc

Type assertion rẻ, nhưng lạm dụng nó là mùi thiết kế. Chi phí runtime không phải lý do để tránh type assertion — nhưng một hàm đầy switch v.(type) để "bắt chước đa hình" thường nên thay bằng một phương thức trên interface (để dispatch lo việc rẽ nhánh) hoặc bằng generics. Đó là vấn đề thiết kế, không phải hiệu năng.

Assert tới interface đắt hơn assert cụ thể một chút. h.(CoDientich) phải tra itab cho cặp (kiểu động, interface đích) thay vì so một con trỏ tĩnh. Chênh lệch nhỏ (và vẫn 0 cấp phát), nhưng trong vòng cực nóng, assert thẳng tới kiểu cụ thể nếu biết trước sẽ nhỉnh hơn.

Generics khi kiểu biết lúc biên dịch. Nếu bạn type switch chỉ để xử lý vài kiểu đã biết, generics cho code không cần assertion, không dispatch runtime, và compiler kiểm kiểu chặt hơn. Type switch dành cho kiểu thật sự chỉ biết lúc chạy.

Ba ý mang về

  1. Assert tới kiểu cụ thể là một phép so con trỏ itab tĩnh: đọc assembly thấy h.(Vuong) biên dịch thành nạp hằng địa chỉ itab → CMP → rẽ nhánh, không quét không phản chiếu — đo thật 1,166 ns, 0 cấp phát, rẻ hơn cả một lời gọi qua interface (1,881 ns).
  2. Type switch không tuyến tính theo số nhánh: đo thật 8 nhánh (1,195 ns) nhanh ngang 2 nhánh (1,206 ns) vì compiler dựng bảng nhảy theo con trỏ kiểu — thêm case gần như miễn phí.
  3. Lấy giá trị ra khỏi interface không tốn heap: mọi thao tác assert/type switch đo được 0 cấp phát — ưu tiên dạng comma-ok để an toàn, dùng thoải mái cho errors.As và xử lý any từ JSON, nhưng cân nhắc generics/phương thức khi kiểu biết lúc biên dịch.

Phần sau ta rời chủ đề interface để mổ xẻ một cấu trúc cũng ẩn chứa con trỏ: Phần sau xem con trỏ hàm và closure được biểu diễn thật thế nào trong bộ nhớ — closure thực chất là gì, khi nào nó cấp phát heap, và chi phí gọi qua con trỏ hàm.