0

Go Zero to Hero - Bài 23: Khai báo Hàm (Function) và Hàm ẩn danh (Anonymous Function)

Trong các ngôn ngữ như C++ hay Java, kiểu dữ liệu trả về thường được đặt trước tên hàm (int calculate()). Tuy nhiên, Go áp dụng tư duy "đọc từ trái sang phải" tự nhiên hơn: Từ khóa -> Tên hàm -> Tham số đầu vào -> Kiểu trả về.


1. Cú pháp khai báo cơ bản: Gọn gàng và Rõ ràng

package main

import "fmt"

// Cú pháp: func Tên_hàm(tham_số kiểu_dữ_liệu) Kiểu_trả_về { ... }
func calculateDiscount(price float64, discountRate float64) float64 {
    return price - (price * discountRate)
}

// Cú pháp rút gọn tham số: 
// Nếu các tham số đứng cạnh nhau CÙNG KIỂU, bạn chỉ cần ghi kiểu ở tham số cuối.
func multiply(a, b, c int) int {
    return a * b * c
}

func main() {
    finalPrice := calculateDiscount(100000, 0.2)
    fmt.Println("Giá sau giảm:", finalPrice)
}

2. Đặc sản của Go: Trả về nhiều giá trị (Multiple Return Values)

Đây chính là tính năng "ăn tiền" nhất của Go so với các ngôn ngữ tiền bối. Thay vì phải tạo ra một Object/Struct tạm thời chỉ để gói ghém nhiều giá trị trả về, Go cho phép bạn return bao nhiêu giá trị tùy thích.

Tính năng này là xương sống của triết lý xử lý lỗi (Error Handling) trong Go: Một hàm thường trả về Kết quả và một Lỗi (Error) đi kèm.

package main

import (
    "errors"
    "fmt"
)

// Trả về 2 giá trị: (int, error)
func divide(a, b int) (int, error) {
    if b == 0 {
        // Trả về kết quả mặc định (0) và thông báo lỗi
        return 0, errors.New("không thể chia cho 0")
    }
    return a / b, nil // nil nghĩa là không có lỗi
}

func main() {
    // Hứng 2 giá trị cùng lúc
    result, err := divide(10, 2)
    
    if err != nil {
        fmt.Println("Lỗi xảy ra:", err)
    } else {
        fmt.Println("Kết quả phép chia:", result)
    }
}

3. Named Return Values (Giá trị trả về có tên) và Naked Return

Đôi khi, để code tự làm tài liệu (self-documenting), bạn có thể đặt tên luôn cho các biến sẽ được trả về ngay trên phần khai báo hàm.

Lúc này, Go sẽ tự động khởi tạo các biến đó với Zero Value, và bạn chỉ cần gọi từ khóa return trống không (Naked Return).

// Khai báo sẵn biến result và err ở phần chữ ký hàm
func processTicket(ticketID string) (result string, err error) {
    if ticketID == "" {
        err = errors.New("mã vé không hợp lệ")
        return // Naked return: Tự động trả về giá trị hiện tại của result và err
    }
    
    result = "Vé " + ticketID + " đã được xử lý"
    return // Tự động trả về result và err (lúc này err đang là nil)
}

⚠️ Lời khuyên thực chiến: Naked Return chỉ nên dùng cho những hàm cực kỳ ngắn (khoảng dưới 10 dòng code). Đối với những hàm logic dài, việc dùng Naked Return sẽ khiến đồng nghiệp của bạn bị "lạc trôi", không biết chính xác biến nào đang được trả về ở cuối hàm.


4. Hàm ẩn danh (Anonymous Function) và Kỹ thuật IIFE

Hàm ẩn danh là những hàm không có tên. Chúng được khai báo ngay tại nơi cần sử dụng và thường được lưu vào một biến, hoặc thực thi ngay lập tức.

Khai báo và gán vào biến

Trong Go, Function là một "First-class citizen" (Công dân hạng nhất), nghĩa là nó có thể được truyền qua lại như một biến bình thường (int, string...).

func main() {
    // Khai báo một hàm ẩn danh và gán vào biến logSystem
    logSystem := func(message string) {
        fmt.Println("[SYSTEM LOG]:", message)
    }

    // Gọi hàm thông qua biến
    logSystem("Khởi động server thành công.")
    logSystem("Kết nối RabbitMQ thành công.")
}

Kỹ thuật IIFE (Immediately Invoked Function Expression)

Có những đoạn logic (như tính toán cấu hình ban đầu, hoặc dọn dẹp bộ nhớ) bạn chỉ muốn nó chạy đúng 1 lần duy nhất ngay lúc khai báo, không cần lưu lại để gọi lần sau.

Bạn bọc ngoặc đơn () ở cuối hàm ẩn danh để yêu cầu Go thực thi nó ngay lập tức.

func main() {
    // Khai báo và thực thi ngay lập tức (Để ý cặp ngoặc () ở dòng cuối)
    func(serviceName string) {
        fmt.Printf("Đang khởi tạo cấu hình cho %s...\n", serviceName)
        // ... các logic phức tạp
    }("Microservice Thanh Toán")
}

Tại sao dân Backend Go lại cuồng Hàm ẩn danh?

Nếu bạn làm việc với các hệ thống phân tán, xử lý hàng ngàn request đồng thời, bạn sẽ sử dụng hàm ẩn danh hàng ngày cho hai mục đích tối thượng:

  1. Goroutines (Xử lý đa luồng): Để khởi chạy một luồng mới chạy ngầm (chạy song song), bạn chỉ cần đặt từ khóa go trước một hàm ẩn danh.
    go func(id int) {
        // Đoạn code này sẽ chạy ngầm, không block luồng chính
        fmt.Println("Đang gửi email biên lai cho user", id)
    }(1024)
    
  2. Defer (Trì hoãn thực thi - Dọn dẹp tài nguyên): Khi kết nối DB hay ghi file, bạn phải đảm bảo tài nguyên được đóng lại trước khi thoát hàm, bất chấp hàm có bị lỗi hay không. Hàm ẩn danh kết hợp defer là cứu cánh.
    func queryData() {
        db := connectDB()
        
        // Đảm bảo hàm ẩn danh này LUÔN CHẠY cuối cùng trước khi queryData() kết thúc
        defer func() {
            fmt.Println("Đóng kết nối DB.")
            db.Close()
        }()
        
        // ... logic truy vấn
    }
    

Tổng kết

Bạn đã nắm trong tay cách vận hành logic của Go:

  1. Cú pháp chuẩn: Tên hàm, tham số gộp kiểu, và trả về nhiều giá trị cùng lúc (rất quan trọng cho Error Handling).
  2. Naked Return: Tiện lợi cho hàm ngắn, nhưng nên cẩn thận với hàm dài.
  3. Hàm ẩn danh: Vũ khí hạng nặng khi cần nhúng logic xử lý 1 lần, cực kỳ hữu dụng khi kết hợp với go (đa luồng) và defer (dọn dẹp tài nguyên).

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í