Closures và cách dùng closures trong Golang?

Photo by Luca Bravo on Unsplash

Closures và cách dùng closures trong Golang?

Chào các bác,

Nếu các bác từng code Go hoặc một số ngôn ngữ như JavaScript hay PHP, chắc các bác đã từng gặp một thuật ngữ gọi là closure nhưng chưa biết nó là cái gì và dùng nó như thế nào.

Để hiểu được closure là cái gì, trước tiên chúng ta cần phải nói về functionanonymous functions, tức là hàmhàm vô danh. Thuật ngữ hàm thì dễ rồi, chẳng hạn như chúng ta có một hàm như sau dùng để tính tổng 2 số nguyên:

func tinhtong(a, b int) int {
  return a+b
}

Hàm vô danh thì cũng giống như hàm thường, nhưng khác ở chỗ thay vì đặt tên cho nó, ta lại gán nó vào một biến hoặc lấy nó làm tham số cho một hàm khác. Chẳng hạn như sau:

import "fmt"

func main() {
    hamTinhTong := func(a, b int) int {
        return a + b
    }
    inKetQua(3, 2, hamTinhTong)
}

func inKetQua(a, b int, fn func(a, b int) int) {
    fmt.Println(fn(a, b))
}

Như ví dụ trên, chúng ta định nghĩa biến hamTinhTong có giá trị là một hàm nhận 2 tham số a và b. Biến này sau đó được dùng làm tham số cho hàm inKetQua. Gọi hàm inKetQua với các tham số như trên màn hình terminal sẽ trả về cho chúng ta kết quả là số 5.

Tại sao ta lại cần dùng hàm vô danh?

Có nhiều lý do, nhưng chủ yếu là bởi chúng ta muốn tích hợp phần định nghĩa hàm thay vì định nghĩa một hàm nhỏ mà cả hệ thống chỉ dùng ở một nơi duy nhất. Ví dụ như khi ta viết test chẳng hạn:

func TestTinhTong(t *testing.T) {
  t.Run("subtest", func(t *testing.T) {
    // đây là một hàm vô danh
  })
}

Mặc dù chúng ta có thể tạo ra một hàm bình thường để gọi ở đây, thực tế cho thấy code không sạch sẽ hơn cũng chẳng hữu dụng hơn. Đấy là vì khi cần, các bác lại phải truy ngược lại xem hàm đó được định nghĩa ở đâu và có gì trong đó. Giả dụ chúng ta dùng một hàm ở nhiều nơi thì đã đành, nhưng ở trường hợp viết test kể trên thì định nghĩa rõ hàm ra chỉ làm code khó đọc hơn mà chẳng có chút ích lợi gì. Do đó, chúng ta nên dùng hàm vô danh.

Thế rồi thì đã đến đoạn nói về Closures chưa?

Các bác cứ từ từ, tôi đang định nói về Closures đây. Closures thực ra chính là anonymous functions nhưng khác ở chỗ nó bắt các bác phải dùng ít nhất một biến đã được định nghĩa bên ngoài hàm. Ví dụ:

package main

import "fmt"

func main() {
  n := 0
  multiplier := func(m int) int {
    n *= m
    return n
  }
  fmt.Println(multiplier(2))
  fmt.Println(multiplier(3))
}

Trong ví dụ cụ thể này, biến n được định nghĩa bên ngoài hàm vô danh của chúng ta, cơ mà chúng ta vẫn có thể truy cập được nó. Điều này có nghĩa là chúng ta đã tạo ra một closure.

Closures có thể được dùng cho nhiều mục đích khác nhau. Ở ví dụ kể trên, biến n cho phép chúng ta "ghi nhớ" số mà hàm multiplier trả về. Chỉnh sửa một chút chúng ta thậm chí có thể tránh việc code của bên khác sửa đổi biến n.

Closures còn khá nhiều thứ hay ho để tìm hiểu, hẹn gặp lại các bác trong những bài viết sau nhé!