0

Series Go Backend: Interfaces (Phần 2) - Empty Interface và Type Assertion

Ở bài trước, chúng ta đã biết Interface là một bản hợp đồng yêu cầu Struct phải có các method nhất định. Vậy điều gì sẽ xảy ra nếu chúng ta định nghĩa một bản hợp đồng... không yêu cầu bất kỳ method nào?

Đó chính là Empty Interface, một công cụ phá vỡ ranh giới kiểu tĩnh của Go, cho phép bạn xử lý các dữ liệu "không xác định" một cách linh hoạt.

1. Empty Interface (interface{}) và bí danh any

Một interface không chứa bất kỳ method nào được gọi là Empty Interface, ký hiệu là interface{}.

Theo nguyên lý Duck Typing: Nếu một Struct có đủ các method của Interface thì nó thỏa mãn Interface đó. Vì Empty Interface yêu cầu 0 method, nên hệ quả tất yếu là: TẤT CẢ mọi kiểu dữ liệu trong Go (từ int, string, Struct, cho đến Slice, Map, Pointer) đều tự động thỏa mãn Empty Interface.

Từ phiên bản Go 1.18, Go giới thiệu thêm từ khóa any đóng vai trò là một bí danh (alias) ngắn gọn hơn cho interface{}.

Go

package main

import "fmt"

// Hàm này nhận vào tham số data thuộc kiểu 'any' (hay interface{})
// Nó có thể nuốt trọn mọi loại dữ liệu bạn truyền vào
func PrintAnything(data any) {
    fmt.Printf("Giá trị: %v | Kiểu dữ liệu ngầm định: %T\n", data, data)
}

func main() {
    PrintAnything(1024)                  // Truyền số nguyên
    PrintAnything("Hello Backend")       // Truyền chuỗi
    PrintAnything([]int{1, 2, 3})        // Truyền Slice
    PrintAnything(struct{ ID int }{1})   // Truyền Struct ẩn danh
}

Ứng dụng trong Backend: Hàm fmt.Println kinh điển mà chúng ta hay dùng bản chất chính là một hàm nhận vào ...any. Trong thực tế, bạn sẽ dùng any rất nhiều khi phải parse một đoạn JSON payload có cấu trúc không cố định từ request của client thành một map[string]any.

2. Type Assertion: "Mở hộp" lấy dữ liệu

Khi bạn gán một biến (ví dụ: age := 22) vào một tham số kiểu any, Go sẽ "đóng gói" (box) số 22 đó vào một chiếc hộp giao diện trống. Trình biên dịch lúc này chỉ nhìn thấy cái hộp any, nó hoàn toàn quên mất bên trong là int.

Bạn KHÔNG THỂ thực hiện các phép toán gốc trên biến any.

Go

var myData any = 100

// LỖI: invalid operation: myData + 50 (mismatched types any and untyped int)
// result := myData + 50 

Để lấy lại giá trị thực sự và báo cho Go biết kiểu dữ liệu gốc, chúng ta phải dùng kỹ thuật Type Assertion (Ép kiểu ngược) với cú pháp biến_interface.(Kiểu_Dữ_Liệu).

Go

// Lấy giá trị số nguyên ra khỏi chiếc hộp any
realNumber := myData.(int)
result := realNumber + 50 // Chạy thành công, result = 150

CẢNH BÁO BẪY PANIC: Nếu bạn dự đoán sai kiểu dữ liệu đang nằm trong hộp, ứng dụng sẽ văng Panic (Crash).

Go

var username any = "hieudev"

// PANIC: interface conversion: interface {} is string, not int
// number := username.(int) 

Cách giải quyết: Kỹ thuật "Comma OK" Giống hệt như khi truy xuất Map (Bài 6), Type Assertion cung cấp một biến thứ hai trả về bool để kiểm tra ép kiểu có thành công hay không.

Go

if num, ok := username.(int); ok {
    fmt.Println("Ép kiểu thành công, đây là số nguyên:", num)
} else {
    fmt.Println("Ép kiểu thất bại, dữ liệu không phải là int")
}

3. Type Switch: Phân loại hàng loạt

Khi xử lý các cấu trúc dữ liệu linh động (như xử lý event từ Kafka nơi message có thể thuộc nhiều định dạng khác nhau), bạn không thể viết hàng tá câu lệnh if-else với "Comma OK" được. Go cung cấp Type Switch để giải quyết bài toán này.

Cú pháp đặc biệt switch v := bien_interface.(type) cho phép bạn rẽ nhánh logic dựa trên kiểu dữ liệu thực tế đang nằm trong hộp.

Go

func HandleEvent(payload any) {
    // Biến v sẽ tự động mang kiểu dữ liệu tương ứng bên trong từng khối case
    switch v := payload.(type) {
    case int:
        fmt.Println("Nhận được số ID:", v)
    case string:
        fmt.Println("Nhận được chuỗi message:", v)
    case bool:
        fmt.Println("Nhận được cờ trạng thái:", v)
    default:
        fmt.Printf("Không hỗ trợ xử lý kiểu dữ liệu: %T\n", v)
    }
}

4. Lời khuyên thực chiến cho Backend Engineer

Sức mạnh của any là vô song, nhưng đừng lạm dụng nó.

Một trong những lý do chọn Go làm Backend là nhờ sự chặt chẽ của hệ thống kiểu tĩnh (Statically Typed), giúp phát hiện 90% lỗi ngay lúc compile. Nếu bạn khai báo mọi thứ là any, bạn đang biến Go thành Javascript hay Python. Bạn vô tình đẩy rủi ro lỗi type mismatch về phía Runtime (lúc ứng dụng đang chạy), khiến ứng dụng dễ bị crash bất ngờ.

Chỉ sử dụng any / interface{} trong 2 trường hợp:

  1. Bạn thực sự không thể biết trước kiểu dữ liệu sẽ nhận được (Ví dụ: viết thư viện JSON parser, Log formatter).

  2. Khi làm việc với các codebase cũ trước Go 1.18. (Từ Go 1.18, Generics đã ra đời giúp giải quyết phần lớn nhu cầu dùng Empty Interface một cách an toàn hơn rất nhiều, chúng ta sẽ học ở Bài 19).


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í