Lesson 11 — Hàm trong Go: Visualization
Ba module tương tác: (1) Function call tracer — visualize call stack cho recursion, (2) Closure inspector — xem biến heap được share giữa các invocation, (3) Defer stack — animate LIFO order khi function return.
1. Function call tracer — Recursion stack
Recursion = hàm gọi chính nó. Mỗi lần gọi đẩy thêm 1 stack frame lên call stack. Khi base case được chạm, các frame được pop ngược trở lại, mỗi pop tính kết quả từ con.
factorial(n) push 1 frame, gọi factorial(n-1) push tiếp. Khi chạm base case (n <= 1), frame trả về 1, các frame phía trên lần lượt nhân với n và pop ra. Go không có TCO → depth lớn = stack overflow. Thử n=8 (max ở demo này) để cảm nhận stack 8 tầng.
2. Closure inspector — Biến trên heap được share
So sánh 2 phiên bản counter: bên trái dùng closure (biến count sống trên heap, share giữa các invocation), bên phải dùng non-closure (mỗi call khởi tạo lại biến local, reset về 0).
makeCounter()counterReset()makeCounter return, biến count đáng lẽ bị hủy. Nhưng Go thấy có inner function vẫn giữ reference → chuyển count từ stack lên heap, giữ sống đến khi closure không còn ai dùng. Mỗi lần gọi makeCounter() tạo một count heap riêng → c1 và c2 độc lập.
3. Defer stack — LIFO order khi return
defer đẩy một call lên defer stack của hàm hiện tại. Khi hàm return (bình thường hoặc panic), runtime pop từ đỉnh stack chạy ngược về đáy → LIFO. Argument được evaluate ngay tại lúc defer, không phải lúc chạy.
defer fmt.Println("loop", i) trong loop — i được "đóng băng" tại lúc defer (0, rồi 1, rồi 2), không phải đọc lúc chạy. Khi pop, từng frame in giá trị đã capture sẵn. Nếu thay bằng defer func() { fmt.Println(i) }(), closure sẽ đọc i lúc chạy → cả 3 dòng in giá trị cuối (trước Go 1.22).