0

Go Zero to Hero - Bài 39: Type Assertion và Type Switch

Trong hệ thống định kiểu tĩnh của Golang, khi bạn nhận dữ liệu từ các kiểu động như any (hoặc interface{}), trình biên dịch sẽ không biết chính xác bên trong đang chứa kiểu dữ liệu gì. Để khôi phục lại kiểu dữ liệu ban đầu, chúng ta cần sử dụng Type Assertion và Type Switch.


1. Type Assertion: Lời khẳng định đanh thép (và rủi ro)

Type Assertion cung cấp cách thức để bạn bóc lớp vỏ any ra và trích xuất giá trị gốc với kiểu dữ liệu cụ thể nằm bên trong nó.

  • Cú pháp: t := i.(T)
  • Trong đó i là biến interface, và T là kiểu dữ liệu bạn muốn ép (khẳng định).

Nhưng hãy cẩn thận, đây là một nước đi đầy rủi ro. Khác với C hay Java (ép kiểu sai có thể trả về null hoặc rác), Go xử lý cực kỳ gắt gao: Khẳng định sai thì lập tức đánh sập hệ thống (Panic).

package main

import "fmt"

func main() {
    var mysteryBox any = "Thẻ AFC-1024" // Đưa một chuỗi vào hộp any

    // 1. Khẳng định đúng (Hợp lệ)
    // Chúng ta bảo Go: "Tôi khẳng định trong hộp này là kiểu string"
    cardID := mysteryBox.(string) 
    fmt.Println("Mã thẻ lấy ra được:", cardID)

    // 2. 🚨 KHẲNG ĐỊNH SAI (Bẫy tử thần PANIC)
    // Cố tình ép một chuỗi string thành kiểu int
    // amount := mysteryBox.(int) // SERVER SẼ CRASH NGAY LẬP TỨC (panic: interface conversion)
}

2. Idiom "Comma Ok" cứu rỗi sinh mạng Server

Vì dữ liệu truyền vào any thường đến từ bên ngoài (như JSON request), chúng ta không bao giờ có thể tin tưởng 100% vào nó. Để tránh Crash, Go cung cấp lại cứu cánh huyền thoại tương tự như khi thao tác với Map: val, ok.

  • Cú pháp an toàn: val, ok := i.(T)
    • Nếu hộp chứa đúng kiểu T: val nhận giá trị thực, ok bằng true.
    • Nếu hộp chứa sai kiểu T: Go không Crash, val nhận Zero Value của kiểu T, ok bằng false.
func processPayload(payload any) {
    // Ép kiểu an toàn kết hợp với Init Statement của if
    if amount, ok := payload.(float64); ok {
        fmt.Printf("✅ Đã nhận được số tiền: %.0f VNĐ\n", amount)
    } else {
        fmt.Println("❌ Dữ liệu không phải là số (float64)!")
    }
}

func main() {
    processPayload(15000.0)         // Chạy nhánh ✅
    processPayload("Mười lăm nghìn") // Chạy nhánh ❌ (Không bị sập server)
}

💡 Bí quyết thực chiến: Trong 99% trường hợp làm việc với Backend, khi dùng Type Assertion, bạn BẮT BUỘC phải dùng cú pháp val, ok. Đừng bao giờ chủ quan bỏ đi biến ok.


3. Type Switch: Cỗ máy phân loại tự động

Khi bạn xử lý một luồng sự kiện (Event Stream) từ Message Broker (như Kafka), cục payload any trả về có thể là một trong số hàng tá loại sự kiện khác nhau: Sự kiện nạp tiền (int), Sự kiện quẹt thẻ (string), Sự kiện lỗi thiết bị (error).

Nếu dùng Type Assertion val, ok liên tục với các lệnh if - else if, code của bạn sẽ trở thành một mớ rác (Spaghetti code) kinh dị. Giải pháp thanh lịch nhất của Go là Type Switch. Nó kết hợp cấu trúc switch-case với any để kiểm tra thẳng "kiểu dữ liệu" thay vì kiểm tra "giá trị".

  • Cú pháp: switch v := i.(type) { ... }
package main

import (
    "fmt"
    "time"
)

// Cấu trúc đại diện cho lỗi phần cứng
type HardwareError struct {
    Code int
}

func handleEventStream(event any) {
    // Từ khóa "type" được truyền thẳng vào trong ngoặc
    // Biến v sẽ TỰ ĐỘNG BIẾN HÌNH thành đúng kiểu dữ liệu tương ứng ở mỗi case
    switch v := event.(type) {
    case string:
        fmt.Println("[EVENT] Nhận được mã thẻ:", v) // v lúc này là string
    case float64:
        fmt.Println("[EVENT] Giao dịch nạp tiền:", v) // v lúc này là float64
    case HardwareError:
        fmt.Println("[ALARM] Lỗi phần cứng mã:", v.Code) // v là struct HardwareError
    case nil:
        fmt.Println("[WARNING] Sự kiện rỗng (nil)")
    default:
        // Cú pháp %T dùng để in ra "Kiểu dữ liệu" của biến
        fmt.Printf("[UNKNOWN] Không hỗ trợ loại sự kiện này: %T\n", v) 
    }
}

func main() {
    handleEventStream("CARD-8899")
    handleEventStream(50000.0)
    handleEventStream(HardwareError{Code: 503})
    handleEventStream(time.Now())
}

⚡ Ưu điểm tuyệt đối: Biến v bên trong mỗi case đã được Go âm thầm ép kiểu thành công và an toàn tuyệt đối. Bạn cứ việc chấm (.) để truy xuất các thuộc tính (như v.Code ở HardwareError) mà không cần check lại.


4. Ứng dụng thực chiến: Custom Error (Lỗi tùy biến)

Sức mạnh lớn nhất của Type Assertion nằm ở việc bóc tách lỗi (Error Handling). Hàm trong Go thường trả về kiểu error (vốn là một Interface). Đôi khi, bạn muốn biết cái lỗi đó cụ thể là "Lỗi do Database rớt mạng" hay "Lỗi do API bên thứ 3 trả về 404" để quyết định có Retry hay không.

// Giả sử ta định nghĩa một cấu trúc lỗi riêng biệt
type APIError struct {
    StatusCode int
    Message    string
}

func (e *APIError) Error() string { // Thỏa mãn error interface
    return fmt.Sprintf("Mã %d: %s", e.StatusCode, e.Message)
}

func performTransaction() error {
    // Trả về một con trỏ APIError nhưng dưới lớp vỏ của interface 'error'
    return &APIError{StatusCode: 503, Message: "Cổng thanh toán đang bảo trì"}
}

func main() {
    err := performTransaction()
    if err != nil {
        // Dùng Type Assertion để "bóc" lớp vỏ error ra, lấy lại APIError gốc
        if apiErr, ok := err.(*APIError); ok {
            if apiErr.StatusCode == 503 {
                fmt.Println("Hệ thống quá tải, tiến hành đưa vào hàng đợi Retry...")
            }
        } else {
            fmt.Println("Lỗi hệ thống nghiêm trọng chưa xác định:", err)
        }
    }
}

Tổng kết

Bạn đã chính thức làm chủ được nghệ thuật biến ảo dữ liệu động trong Go:

  1. Giam dữ liệu vào any: Nhận mọi thể loại input mà không sợ lỗi biên dịch.
  2. Lấy ra bằng Type Assertion val, ok := i.(T): Luôn nhớ kẹp biến ok để giữ mạng cho Server không bị Panic.
  3. Phân luồng bằng Type Switch i.(type): Vũ khí sắc bén nhất để viết các Worker xử lý đủ loại Message/Event trong kiến trúc Microservices.

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í