0

Series Go Backend: Functions nâng cao - Anonymous functions, Closures

Như đã đề cập ở bài trước, Go coi function là "First-class citizen". Điều này mở ra những kỹ thuật lập trình vô cùng mạnh mẽ, đặc biệt là khi thiết kế các kiến trúc linh hoạt như Middleware, Worker Pool, hoặc xử lý các tác vụ bất đồng bộ. Bài này sẽ bóc tách hai khái niệm nâng cao nhưng xuất hiện dày đặc trong source code backend: Hàm ẩn danh (Anonymous Functions) và Bao đóng (Closures).

1. Hàm ẩn danh (Anonymous Functions)

Đúng như tên gọi, đây là những hàm được định nghĩa mà không có tên. Chúng thường được tạo ra tại đúng nơi mà chúng được sử dụng.

Gán hàm cho một biến: Bạn có thể khai báo một hàm ẩn danh và gán nó cho một biến. Biến này sau đó có thể được gọi như một hàm bình thường.

Go

func main() {
    // Định nghĩa hàm ẩn danh và gán vào biến 'greet'
    greet := func(name string) {
        fmt.Printf("Xin chào %s từ hàm ẩn danh!\n", name)
    }

    // Gọi hàm
    greet("Hiếu")
}

Thực thi ngay lập tức (IIFE - Immediately Invoked Function Expression): Bạn có thể định nghĩa một hàm ẩn danh và gọi nó chạy ngay lập tức bằng cách thêm cặp ngoặc đơn () ở cuối. Kỹ thuật này cực kỳ hữu dụng khi dùng với defer hoặc khởi chạy goroutine.

Go

func main() {
    // Khai báo và gọi ngay lập tức
    func(status string) {
        fmt.Println("Hệ thống khởi động với trạng thái:", status)
    }("OK")
}

2. Truyền hàm như một tham số (Higher-Order Functions)

Vì hàm cũng là một kiểu dữ liệu, bạn có thể truyền nó vào một hàm khác như một tham số thông thường (Callback pattern).

Go

// Hàm processData nhận vào một slice và một hàm xử lý (transform)
func processData(data []int, transform func(int) int) []int {
    result := make([]int, len(data))
    for i, val := range data {
        // Áp dụng logic của hàm transform được truyền vào
        result[i] = transform(val)
    }
    return result
}

func main() {
    numbers := []int{1, 2, 3, 4}

    // Truyền một hàm ẩn danh nhân đôi giá trị
    doubled := processData(numbers, func(n int) int {
        return n * 2
    })
    
    fmt.Println(doubled) // Output: [2, 4, 6, 8]
}

3. Bao đóng (Closures): "Bắt cóc" biến môi trường

Closure là một khái niệm tuyệt đẹp nhưng cũng dễ gây lú lẫn nhất. Closure xảy ra khi một hàm ẩn danh sử dụng (truy cập hoặc sửa đổi) các biến nằm ngoài phạm vi thân hàm của chính nó.

Khi điều này xảy ra, hàm ẩn danh đó sẽ "neo" (bind) các biến đó lại vào bộ nhớ của riêng nó, ngay cả khi hàm chứa các biến đó đã thực thi xong.

Go

// Hàm counter trả về một hàm ẩn danh (kiểu func() int)
func counter() func() int {
    count := 0 // Biến cục bộ của hàm counter

    // Trả về một Closure
    return func() int {
        count++ // Hàm ẩn danh này "bắt cóc" và ghi nhớ biến count
        return count
    }
}

func main() {
    nextNum := counter()

    // Mỗi lần gọi, biến count được bảo toàn trạng thái và tiếp tục tăng
    fmt.Println(nextNum()) // In ra: 1
    fmt.Println(nextNum()) // In ra: 2
    fmt.Println(nextNum()) // In ra: 3

    // Nếu khởi tạo một bộ đếm mới, nó sẽ có vùng nhớ độc lập
    anotherCounter := counter()
    fmt.Println(anotherCounter()) // In ra: 1
}

Tại sao Closure lại quan trọng trong Backend Go? Closure cho phép bạn tạo ra các biến trạng thái (state) được đóng gói chặt chẽ mà không cần phải định nghĩa một Struct cồng kềnh. Nó đặc biệt được dùng để cấu hình các kết nối Database hoặc thiết lập Route Middleware.

4. Ứng dụng thực chiến: Khai báo Middleware

Giả sử bạn đang viết một Web API cơ bản bằng thư viện net/http và muốn tạo một Middleware để log lại thời gian xử lý của mọi request. Closure chính là chìa khóa:

Go

import (
    "fmt"
    "net/http"
    "time"
)

// LoggerMiddleware là một hàm trả về một hàm xử lý HTTP (HandlerFunc)
func LoggerMiddleware(next http.HandlerFunc) http.HandlerFunc {
    // Trả về một Closure đóng vai trò là handler mới
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // Gọi handler gốc (nhờ việc 'next' bị bắt cóc vào Closure)
        next(w, r)
        
        duration := time.Since(start)
        fmt.Printf("[LOG] %s %s tốn %v\n", r.Method, r.URL.Path, duration)
    }
}

func main() {
    // Handler chính
    helloHandler := func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello, Client!")
    }

    // Bọc handler chính bằng Middleware
    http.HandleFunc("/hello", LoggerMiddleware(helloHandler))
    // http.ListenAndServe(":8080", nil)
}

Góc kinh nghiệm - Sự thay đổi của Go 1.22: Trước đây, việc sử dụng Closure bên trong vòng lặp for với Goroutine là một "cái bẫy" khét tiếng (Loop variable capture), khiến mọi Goroutine thường in ra giá trị của phần tử lặp cuối cùng do biến lặp chia sẻ chung một vùng nhớ. Tuy nhiên, từ Go 1.22 (đầu năm 2024), đội ngũ phát triển Go đã thay đổi hành vi cốt lõi này. Mỗi vòng lặp sẽ tự động tạo một biến lặp mới với vùng nhớ riêng. Bạn không cần phải copy biến ra một biến tạm (val := val) hoặc truyền tham số vào hàm ẩn danh (func(v int){...}(val)) để chống lỗi Closure trong vòng lặp nữa.


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í