Hàm ẩn danh
Function literal là một hàm không có tên. Nó là một biểu thức, nên có thể đặt ở bất cứ đâu một giá trị đặt được: một biến, một đối số, một giá trị trả về, một trường của struct.
Go không có cú pháp viết tắt kiểu mũi tên hay lambda. Dạng đầy đủ func(params) results { body } là cách duy nhất để viết hàm ẩn danh (anonymous function).
Giữ lại biến
Một function literal có thể dùng các biến của hàm bao quanh. Nó không sao chép chúng: closure và code xung quanh dùng chung một biến.
Biến được giữ lại sống chừng nào còn closure tham chiếu tới nó, kể cả sau khi hàm khai báo ra nó đã trả về. Trình biên dịch chuyển nó lên heap khi cần; bạn không bao giờ phải tự quản lý việc đó.
Generator và bộ đếm
Trả về một closure từ một hàm sẽ cho mỗi hàm được trả về một trạng thái riêng.
Mỗi lần gọi counter() tạo ra một n mới, nên c1 và c2 đếm độc lập. Không gì bên ngoài đọc hay đặt lại được n, nên đây là một dạng đóng gói nhỏ.
Closure trong vòng lặp (Go 1.22 trở lên)
Trước Go 1.22, một vòng lặp for chỉ có một biến vòng lặp cho cả vòng lặp. Các closure tạo bên trong đều giữ lại đúng một biến đó và thấy giá trị cuối cùng của nó. Đây là lỗi closure phổ biến nhất trong Go.
Từ Go 1.22, mỗi lần lặp có bản sao riêng, nên đoạn code dưới đây in ra 0 1 2:
Với Go 1.21 trở về trước, cùng vòng lặp đó viết dưới dạng for i := 0; i < 3; i++ (range trên một số nguyên cũng mới có từ 1.22) in ra 3 3 3. Quy tắc mới áp dụng theo từng module, dựa trên dòng go trong go.mod, nên một module cũ vẫn giữ hành vi cũ cho đến khi dòng go của nó được nâng lên 1.22 trở lên. Bạn vẫn sẽ thấy i := i hay v := v bên trong vòng lặp ở code cũ: đó là cách sửa thủ công và giờ đã thừa.
Quy tắc theo từng lần lặp chỉ áp dụng cho các biến do chính câu lệnh for khai báo. Một biến khai báo trước vòng lặp và được cập nhật trong thân vẫn là một biến dùng chung:
var last string
for _, s := range items {
last = s
handlers = append(handlers, func() { use(last) }) // every closure sees the final value
}
Closure và goroutine
Một goroutine khởi chạy bằng function literal cũng là một closure. Các quy tắc dùng chung vẫn áp dụng, cộng thêm quy tắc quen thuộc về đồng thời: nếu nhiều goroutine cùng ghi vào một biến được giữ lại, bạn cần mutex hoặc channel.
Đoạn code này luôn in ra 5050. Mỗi goroutine đọc i của riêng nó (theo ngữ nghĩa vòng lặp của Go 1.22) và ghi vào total dùng chung trong khi giữ khóa. Bỏ mutex đi thì kết quả trở nên khó đoán; go run -race báo đó là một data race. Xem goroutines về WaitGroup và race detector.
Closure xuất hiện ở đâu trong code thực tế
- Sắp xếp và tìm kiếm:
slices.SortFunc,slices.IndexFunc,sort.Sliceđều nhận một function literal. - Dọn dẹp trì hoãn:
defer func() { ... }()chạy một khối lệnh khi hàm kết thúc và đọc được các biến của hàm. Xem defer. - HTTP middleware: một hàm nhận một handler và trả về một literal
http.HandlerFuncmới bọc quanh handler đó. - Cấu hình: functional option (
func WithTimeout(d time.Duration) Option { return func(c *Config) { c.Timeout = d } }) là closure giữ lại đối số.
Một ví dụ middleware giữ lại cả một tham số lẫn một bộ đếm:
Lỗi thường gặp
- Tưởng closure chụp lại giá trị. Closure đọc giá trị hiện tại của biến lúc nó chạy, không phải giá trị lúc closure được tạo. Hãy truyền giá trị làm tham số nếu bạn cần một bản chụp:
go func(v int) { ... }(x). - Literal đệ quy. Một function literal không thể tự gọi mình qua biến mà nó đang được gán vào trong cùng một
:=. Hãy khai báo biến trước:var walk func(n int); walk = func(n int) { ... walk(n-1) }. - Ghi không đồng bộ từ các goroutine. Giữ lại biến không phải là đồng bộ. Hãy bảo vệ các lần ghi dùng chung.
Câu hỏi thường gặp
Hàm ẩn danh trong Go là gì?
Là một function literal không có tên: func(x int) int { return x * 2 }. Bạn có thể gán nó cho một biến, truyền nó làm đối số, trả nó về, hoặc gọi nó ngay bằng cách thêm () sau dấu ngoặc nhọn đóng. Go không có cú pháp lambda riêng; function literal chính là lambda.
Closure trong Go là gì?
Là một function literal tham chiếu tới các biến được khai báo bên ngoài nó. Hàm giữ các biến đó sống và dùng chung chúng với phạm vi bao quanh, nên thay đổi bên trong closure nhìn thấy được ở bên ngoài và ngược lại. Biến được giữ theo tham chiếu, không bị sao chép.
Closure trong Go giữ biến theo giá trị hay theo tham chiếu?
Theo tham chiếu. Closure và code xung quanh dùng chung một biến. Nếu cần một bản chụp, hãy sao chép giá trị sang một biến mới trước khi tạo closure, hoặc truyền nó làm đối số.
Lỗi closure với biến vòng lặp đã được sửa trong Go chưa?
Rồi, từ Go 1.22. Mỗi lần lặp của vòng lặp for giờ khai báo một biến vòng lặp mới, nên closure và goroutine tạo trong vòng lặp thấy giá trị của đúng lần lặp đó. Quy tắc này áp dụng cho các module có go.mod ghi go 1.22 trở lên. Code cũ thường có i := i bên trong vòng lặp để né lỗi; viết vậy không hại gì nhưng không còn cần nữa.