0

Go Zero to Hero - Bài 25: Hàm Variadic (Tham số số lượng biến đổi)

Trong quá trình phát triển các ứng dụng Backend, đôi khi bạn sẽ gặp những hàm cần tiếp nhận số lượng đối số không cố định (như hàm log hệ thống, hàm nối chuỗi hay hàm tính toán tổng). Golang cung cấp một tính năng cực kỳ thanh lịch để giải quyết bài toán này: Variadic Functions (Hàm nhận tham số biến thiên).


1. Hàm Variadic là gì? Sự thật đằng sau dấu ...

Hàm Variadic là một hàm cho phép bạn truyền vào một số lượng tham số tùy ý (từ 0 đến vô hạn) đối với một kiểu dữ liệu cụ thể.

Để biến một tham số bình thường thành tham số Variadic, bạn chỉ cần đặt dấu ba chấm ... ngay trước kiểu dữ liệu của nó ở phần chữ ký hàm.

Bí mật ở hậu trường:

Trình biên dịch của Go sẽ tự động gom tất cả các đối số rời rạc mà bạn truyền vào, và đóng gói chúng thành một Slice (Mảng động) trước khi ném vào thân hàm.

package main

import "fmt"

// Cú pháp: ...type
func calculateTotalFare(fares ...float64) float64 {
    // Lúc này, biến 'fares' bên trong hàm chính là một Slice: []float64
    // Vì là Slice, chúng ta có thể dùng len() hoặc duyệt bằng for range
    fmt.Printf("Đang tính tổng cho %d giao dịch...\n", len(fares))
    
    total := 0.0
    for _, fare := range fares {
        total += fare
    }
    
    return total
}

func main() {
    // Có thể truyền bao nhiêu đối số tùy thích, cách nhau bởi dấu phẩy
    sum1 := calculateTotalFare(15000, 20000)
    sum2 := calculateTotalFare(10000, 15000, 5000, 25000)
    
    // Thậm chí truyền 0 đối số cũng hợp lệ (hàm sẽ nhận được một slice rỗng)
    sum3 := calculateTotalFare()
    
    fmt.Println("Tổng 1:", sum1)
    fmt.Println("Tổng 2:", sum2)
    fmt.Println("Tổng 3:", sum3)
}

2. Quy tắc "Trùm cuối": Tham số Variadic phải đứng cuối cùng

Một hàm có thể nhận nhiều tham số bình thường kết hợp với tham số Variadic. Tuy nhiên, Go quy định cực kỳ nghiêm ngặt: Tham số Variadic BẮT BUỘC phải là tham số cuối cùng trong danh sách. Và mỗi hàm chỉ được phép có tối đa một tham số Variadic.

// ✅ HỢP LỆ: Tham số ...string đứng cuối cùng
func logSystemEvents(level string, deviceID int, messages ...string) {
    fmt.Printf("[%s] Device %d:\n", level, deviceID)
    for _, msg := range messages {
        fmt.Println(" -", msg)
    }
}

// ❌ BÁO LỖI: Trình biên dịch không biết đâu là điểm dừng của ...int để chuyển sang string
// func invalidFunc(numbers ...int, name string) { ... }

Nhờ quy tắc này, khi bạn gọi logSystemEvents("ERROR", 101, "Timeout", "Mất kết nối"), Go sẽ biết chính xác "ERROR" là level, 101 là deviceID, và toàn bộ phần còn lại được gom vào slice messages.


3. Toán tử Spread: Truyền Slice có sẵn vào hàm Variadic

Khi học về append, chúng ta đã dùng cú pháp slice... để gộp 2 mảng. Kỹ thuật này cũng áp dụng y hệt khi gọi hàm Variadic.

Giả sinh hệ thống xử lý vé AFC của bạn đang gom một lô (batch) các giao dịch lỗi thành một Slice. Bây giờ bạn muốn truyền cái Slice đó vào hàm Variadic logSystemEvents ở trên. Bạn không thể truyền trực tiếp cái Slice vào được, vì hàm mong đợi các chuỗi rời rạc.

Giải pháp: Dùng dấu ... ở ngay sau biến Slice khi gọi hàm. Hành động này sẽ "giải nén" (unpack) Slice thành các tham số độc lập.

func main() {
    // Danh sách lỗi đang nằm gọn trong một Slice
    errorLogs := []string{
        "Cổng từ chối thẻ",
        "Hết kết nối Redis",
        "Máy in kẹt giấy",
    }
    
    // GIẢI NÉN Slice bằng errorLogs...
    logSystemEvents("FATAL", 102, errorLogs...)
}

4. Ứng dụng thực chiến: Viết hàm tùy biến tương tự fmt.Printf

Nếu bạn tò mò mã nguồn của thư viện fmt viết hàm Printf như thế nào, thì đây là cấu trúc của nó:

  • Nó nhận vào một chuỗi format làm chuẩn.
  • Và một tham số Variadic kiểu interface{} (nghĩa là nhận MỌI kiểu dữ liệu).

Chúng ta có thể vận dụng kiến thức này để viết một hàm Custom Logger cực kỳ linh hoạt cho các Microservices:

package main

import (
    "fmt"
    "time"
)

// Hàm logger tự định nghĩa, nhận format và các biến tham số tùy ý
func APILogger(format string, args ...interface{}) {
    // Lấy thời gian hiện tại
    now := time.Now().Format("2006-01-02 15:04:05")
    
    // Xây dựng chuỗi tiền tố
    prefix := fmt.Sprintf("[%s] [API LOG] ", now)
    
    // Gộp tiền tố với chuỗi format do người dùng truyền vào
    fullFormat := prefix + format + "\n"
    
    // Dùng hàm fmt.Printf nội bộ và truyền giải nén mảng args...
    fmt.Printf(fullFormat, args...)
}

func main() {
    // Gọi hàm logger với 2 tham số, 3 tham số hoặc nhiều hơn
    APILogger("User %s đã đăng nhập.", "Hoàng")
    APILogger("Đơn hàng %d bị lỗi %s.", 9981, "Timeout DB")
}

Tổng kết

Hàm Variadic là một thủ thuật cú pháp nhỏ nhưng mang lại sự linh hoạt khổng lồ cho việc thiết kế API và các hàm tiện ích (Utility functions):

  1. Cú pháp ...type: Nằm ở chữ ký hàm, biến các tham số rời rạc thành một Slice []type.
  2. Vị trí: Phải luôn luôn đứng ở cuối cùng trong danh sách tham số.
  3. Giải nén (slice...): Dùng khi bạn đã có sẵn một Slice và muốn "bung" nó ra để nhét vào hàm Variadic.

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í