0

Go Zero to Hero - Bài 38: Interface rỗng (interface{} / any)

Trong hệ thống định kiểu tĩnh (Statically Typed) của Golang, việc kiểm tra kiểu dữ liệu được thực thi nghiêm ngặt tại thời điểm biên dịch. Tuy nhiên, sẽ có những tình huống bạn cần xử lý những dữ liệu có kiểu không xác định. Đó là lúc Interface Rỗng (interface{} hay any) phát huy sức mạnh.


1. Bản chất của Interface rỗng: Khái niệm "Ổ cắm không có lỗ"

Hãy nhớ lại định nghĩa: Một Struct muốn thỏa mãn một Interface thì nó phải sở hữu tất cả các Method mà Interface đó yêu cầu.

Vậy điều gì sẽ xảy ra nếu một Interface... không yêu cầu bất kỳ Method nào?

Đúng vậy! Bất kỳ kiểu dữ liệu nào trên cõi đời này (từ int, bool, string cho đến Struct, Pointer, hay thậm chí là một Interface khác) đều sở hữu "không method". Do đó, tất cả mọi kiểu dữ liệu đều tự động thỏa mãn Interface rỗng.

package main

import "fmt"

func main() {
    // Khai báo một biến kiểu interface rỗng
    var blackHole interface{}

    blackHole = 404                   // Hợp lệ (int)
    blackHole = "Server Not Found"    // Hợp lệ (string)
    blackHole = true                  // Hợp lệ (bool)
    blackHole = []int{1, 2, 3}        // Hợp lệ (Slice)
    
    fmt.Println(blackHole)
}

Nó hoạt động giống hệt kiểu void* trong C/C++, kiểu Object trong Java, hay kiểu any trong TypeScript.


2. Từ khóa any: Cuộc cách mạng làm gọn code từ Go 1.18

Việc gõ interface{} liên tục sẽ khiến code trông rất rối rắm và tốn diện tích, đặc biệt là khi khai báo Map hay Slice (ví dụ: map[string]interface{}).

Từ phiên bản Go 1.18, Google đã thấu hiểu điều này và tung ra một "bí danh" (alias) cực kỳ ngắn gọn: any. Bản chất của any chính là interface{}. Trình biên dịch coi 2 từ này là 1.

// Hai cách viết này là hoàn toàn tương đương:
func printDataOld(data interface{}) { /* ... */ }
func printDataNew(data any) { /* ... */ }

3. Ứng dụng thực chiến trong Backend

Bạn sẽ hiếm khi dùng any cho các biến logic thông thường. Nơi nó thực sự tỏa sáng rực rỡ là ở biên giới giao tiếp hệ thống (I/O) và định dạng dữ liệu không xác định.

Kịch bản 1: Hàm nhận mọi tham số (như fmt.Println)

Nếu bạn muốn viết một hàm Log hệ thống có thể nhận vào bất kỳ dữ liệu nào (một số lỗi, một chuỗi báo cáo, hoặc nguyên một Struct giao dịch vé), bạn phải dùng any.

// Kết hợp Variadic Function và kiểu any
func SystemLogger(level string, data ...any) {
    fmt.Printf("[LOG - %s] Nội dung: ", level)
    for _, item := range data {
        fmt.Printf("%v ", item)
    }
    fmt.Println()
}

func main() {
    SystemLogger("INFO", "Hệ thống AFC khởi động")
    SystemLogger("ERROR", "Mất kết nối cổng số", 5, "Mã lỗi DB:", 503)
}

Kịch bản 2: Parse JSON vô định (Dynamic JSON)

Giả sử hệ thống máy bán vé (TVM) của bạn tích hợp với cổng thanh toán VNPay. Khi thanh toán xong, VNPay bắn một Webhook về Server của bạn.

Nếu bạn đã biết rõ cấu trúc JSON, bạn sẽ tạo một Struct và dùng Struct Tags. Nhưng nếu payload JSON chứa những trường extra_data mà mỗi ngân hàng trả về một kiểu khác nhau, bạn không thể tạo Struct cứng được. Lúc này, map[string]any là cứu cánh.

import (
    "encoding/json"
    "fmt"
)

func main() {
    // Giả lập cục JSON Webhook từ cổng thanh toán
    rawJSON := []byte(`{
        "txn_id": "VNP-123456",
        "amount": 50000,
        "is_success": true,
        "extra_data": {
            "bank_code": "VCB",
            "promotion_applied": false
        }
    }`)

    // Khai báo một Map với Key là string, Value có thể là BẤT CỨ THỨ GÌ
    var payload map[string]any

    // Đổ JSON vào Map
    if err := json.Unmarshal(rawJSON, &payload); err != nil {
        fmt.Println("Lỗi parse JSON:", err)
        return
    }

    fmt.Println("Mã giao dịch:", payload["txn_id"])
    fmt.Println("Dữ liệu ẩn:", payload["extra_data"])
}

4. "Con dao hai lưỡi": Rủi ro khi mất Type Safety

any mang lại sức mạnh to lớn, nhưng nó cũng là con quái vật phá vỡ nguyên tắc định kiểu tĩnh an toàn của Go.

Khi một biến mang kiểu any, trình biên dịch hoàn toàn mù tịt về kiểu dữ liệu gốc của nó ở bên trong. Do đó, bạn KHÔNG THỂ thực hiện bất kỳ phép toán nào lên nó, kể cả khi bạn biết chắc chắn bên trong nó đang chứa một con số.

func main() {
    var price any = 15000 // Bạn gán một số int vào
    
    // 🚨 LỖI BIÊN DỊCH: invalid operation: price + 5000 (mismatched types any and int)
    // Trình biên dịch từ chối cộng một thứ "không xác định" với một số nguyên.
    // finalPrice := price + 5000 
}

Để sử dụng lại được dữ liệu đang bị "nhốt" bên trong hộp any, chúng ta bắt buộc phải mở hộp ra và Ép kiểu (Type Assertion).


Tổng kết

Bạn hãy khắc cốt ghi tâm về Interface rỗng:

  1. Chứa vạn vật: interface{} (hay any) là một chiếc hộp đen có thể chứa bất kỳ kiểu dữ liệu nào trong Go.
  2. Ứng dụng I/O: Cực kỳ hữu dụng khi thiết kế các hàm in log, hoặc hứng dữ liệu JSON động (map[string]any).
  3. Hạn chế lạm dụng: Việc dùng any tràn lan trong logic nội bộ sẽ làm mất đi sức mạnh kiểm tra kiểu lúc biên dịch.

All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.