0

Go Zero to Hero - Bài 26: Closures - Sức mạnh của hàm bậc cao

Trong Golang, cú pháp tuy tối giản nhưng lại hỗ trợ đầy đủ các mô hình lập trình nâng cao. Một trong những vũ khí sắc bén nhất của các Gopher kỳ cựu chính là Higher-Order Functions và Closures.


1. Hàm bậc cao (Higher-Order Functions) là gì?

Trong Go, Hàm là "Công dân hạng nhất" (First-class citizen). Điều này có nghĩa là Hàm cũng chỉ là một loại dữ liệu bình thường (giống như int, string...).

Vì Hàm là dữ liệu, nên bạn hoàn toàn có thể:

  • Truyền một hàm vào làm tham số cho một hàm khác.
  • Dùng một hàm để trả về (return) một hàm khác.

Bất kỳ hàm nào thực hiện một trong hai (hoặc cả hai) điều trên đều được gọi là Hàm bậc cao.


2. Closure (Bao đóng) - Ký ức của một Hàm

Closure thực chất là một Hàm ẩn danh (Anonymous Function), nhưng nó có một "siêu năng lực": Nó có thể truy cập và "ghi nhớ" các biến nằm bên ngoài phạm vi (scope) của nó, ngay cả khi hàm chứa các biến đó đã chạy xong và kết thúc.

Hãy xem ngay một ví dụ kinh điển: Hàm tạo bộ đếm (Generator / Counter).

package main

import "fmt"

// Hàm newCounter trả về một hàm khác (func() int)
func newCounter() func() int {
    // Biến count là một biến cục bộ bên trong newCounter
    count := 0 
    
    // Trả về một Hàm ẩn danh. 
    // Hàm này SỬ DỤNG biến count từ phạm vi bên ngoài nó. 
    // => Đây chính là một CLOSURE!
    return func() int {
        count++ 
        return count
    }
}

func main() {
    // Tạo ra một bộ đếm mới.
    // Lúc này hàm newCounter() đã chạy XONG. Đáng lẽ biến 'count' phải bị hủy.
    counterA := newCounter()

    // NHƯNG KHÔNG! Closure bên trong vẫn "ôm" lấy vùng nhớ của biến count.
    fmt.Println("Counter A:", counterA()) // In ra 1
    fmt.Println("Counter A:", counterA()) // In ra 2
    fmt.Println("Counter A:", counterA()) // In ra 3

    // Tạo ra một bộ đếm hoàn toàn độc lập khác
    counterB := newCounter()
    fmt.Println("Counter B:", counterB()) // In ra 1 (Bắt đầu lại từ đầu)
}

Tại sao Closure lại vi diệu đến vậy?

Trong logic thông thường, khi hàm newCounter() kết thúc, toàn bộ biến cục bộ bên trong nó (biến count) sẽ bị Garbage Collector (Bộ dọn rác) dọn sạch.

Tuy nhiên, trình biên dịch của Go đủ thông minh để nhận ra: "Khoan đã, cái hàm ẩn danh được return kia vẫn đang cần xài biến count. Mình phải di dời biến count này từ Stack (bộ nhớ tạm) sang Heap (bộ nhớ lâu dài) để giữ cho nó sống!"

Nhờ vậy, Closure tạo ra một không gian Data Encapsulation (Che giấu dữ liệu) tuyệt vời. Không ai ở hàm main() có thể trực tiếp sửa được biến count, họ bắt buộc phải gọi qua hàm counterA() để tăng giá trị một cách an toàn, tuyệt đối loại bỏ được rủi ro của Biến toàn cục (Global Variables).


3. Ứng dụng thực chiến: Viết Wrapper (Hàm bọc)

Trong Backend, chúng ta rất hay gặp bài toán: Làm sao để đo thời gian chạy của một hàm bất kỳ mà không phải chèn code đo thời gian vào tận bên trong ruột của hàm đó?

Closure là giải pháp thanh lịch nhất. Chúng ta sẽ viết một Wrapper (Hàm bậc cao):

package main

import (
    "fmt"
    "time"
)

// timerWrapper nhận vào 1 tên mô tả và 1 hàm logic cần chạy
// Nó trả về 1 hàm mới đã được "độ" thêm tính năng đo thời gian
func timerWrapper(name string, logicFunc func()) func() {
    // Trả về Closure
    return func() {
        start := time.Now() // Bắt đầu tính giờ
        
        logicFunc() // Chạy logic thực sự
        
        // Tính toán và in ra thời gian chạy
        fmt.Printf("[%s] Hoàn thành trong: %v\n", name, time.Since(start))
    }
}

// Một hàm nghiệp vụ tốn thời gian
func processTicketData() {
    time.Sleep(1 * time.Second) // Giả lập quét Database mất 1 giây
    fmt.Println("Đã xử lý xong dữ liệu vé TVM.")
}

func main() {
    // "Bọc" hàm nghiệp vụ lại bằng timerWrapper
    wrappedFunc := timerWrapper("Ticket_Batch_Job", processTicketData)
    
    // Chạy hàm đã được bọc
    wrappedFunc()
}

4. Trái tim của Middleware trong Go

Nếu bạn áp dụng tư duy Wrapper ở phần 3 vào các HTTP Request, bạn sẽ có ngay khái niệm Middleware – thứ mà bất kỳ Backend Developer nào cũng xài hàng ngày.

Middleware đơn giản là một Closure nhận vào một Handler (Hàm xử lý Request) và trả về một Handler mới đã được bọc thêm các tính năng như:

  • Kiểm tra token đăng nhập (Authentication).
  • Ghi log truy cập (Logging).
  • Giới hạn số lượng request (Rate Limiting).
// Giả lập hàm kiểm tra quyền trong DB
func checkRoleInDatabase() bool {
    return true
}

// Middleware kiểm tra quyền Admin
func requireAdmin(nextHandler func()) func() {
    return func() {
        isAdmin := checkRoleInDatabase() // Giả lập hàm check
        if !isAdmin {
            fmt.Println("❌ Lỗi 403: Bạn không có quyền Admin!")
            return // Chặn đứng, không cho chạy hàm bên trong
        }
        
        fmt.Println("✅ Xác thực thành công. Đang chuyển hướng...")
        nextHandler() // Cho phép chạy logic tiếp theo
    }
}

Bằng cách lồng các Closure vào nhau, bạn có thể tạo ra những chuỗi xử lý (Pipeline) cực kỳ mạnh mẽ: Logger(RequireAdmin(ProcessPayment)).


Tổng kết

Closures và Hàm bậc cao là minh chứng cho thấy Go tuy cú pháp tối giản nhưng lại mang tính biểu đạt cực cao:

  1. Hàm là dữ liệu: Có thể truyền vào và trả về thoải mái.
  2. Khả năng "nhớ" dai: Closure giữ lại được trạng thái của các biến bên ngoài (môi trường bao quanh nó) ngay cả khi vòng đời gốc đã kết thúc.
  3. Thực chiến: Là nền tảng để triển khai Data Isolation (cách ly bộ nhớ), Wrapper (hàm bọc) và đặc biệt là Middleware pattern trong lập trình Web/API.

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí