0

Go Zero to Hero - Bài 31: Khởi tạo, gán giá trị và Nested Struct (Struct lồng nhau)

Khi tổ chức dữ liệu cho các hệ thống backend lớn, việc gom nhóm các trường dữ liệu thành các module nhỏ là điều tối quan trọng. Kỹ thuật Nested Struct (Struct lồng nhau) là công cụ hoàn hảo để biểu diễn các mối quan hệ sở hữu trong lập trình.


1. Nghệ thuật Khởi tạo: Đừng bao giờ lười biếng!

Khi tạo ra một bản sao (instance) của Struct, Go cho phép bạn khởi tạo bằng hai cách: Theo thứ tự (Positional) và Theo tên (Named).

Hãy xem định nghĩa cấu trúc của một hộp đựng tiền lẻ trong máy bán vé:

package main

import "fmt"

type CashBox struct {
    BoxID    string
    Capacity int
    Current  int
}

func main() {
    // ❌ CÁCH 1: KHỞI TẠO THEO THỨ TỰ (Positional - Cực kỳ nguy hiểm)
    // Go sẽ map giá trị theo đúng thứ tự khai báo trong Struct
    box1 := CashBox{"CB-001", 5000, 1500} 
    
    // ✅ CÁCH 2: KHỞI TẠO THEO TÊN (Named - Chuẩn thực chiến)
    box2 := CashBox{
        BoxID:    "CB-002",
        Capacity: 5000,
        Current:  1500,
    }
}

Tại sao Cách 1 lại là "Cái bẫy"?

Giả sử tháng sau, yêu cầu hệ thống thay đổi. Bạn chèn thêm một trường Status string vào giữa BoxID và Capacity trong Struct gốc.

Lúc này, toàn bộ những đoạn code khởi tạo theo Cách 1 sẽ báo lỗi đỏ lòm hoặc bị gán nhầm dữ liệu, vì thứ tự đã bị lệch!

Với Cách 2 (Khởi tạo theo tên), dù bạn có đổi vị trí, thêm hay bớt trường dữ liệu trong Struct gốc, code của bạn vẫn sống khỏe và cực kỳ dễ đọc. Đây là luật bất thành văn của mọi Gopher.


2. Nested Struct (Struct lồng nhau) là gì?

Ở bài học về Composition, chúng ta đã học về việc nhúng một Struct vào Struct khác mà không cần đặt tên trường (Embedded Struct).

Còn Nested Struct là khi bạn đặt một Struct vào bên trong một Struct khác, và gán cho nó một Tên trường (Field Name) đàng hoàng. Kỹ thuật này dùng khi một thực thể "SỞ HỮU" (Has-A) một thực thể khác.

Hãy xem cách thiết kế một cỗ máy bán vé (TVM) hoàn chỉnh:

// Struct mô tả cấu hình mạng
type NetworkConfig struct {
    IPAddress  string
    MacAddress string
    SubnetMask string
}

// Struct mô tả Máy bán vé
type TicketMachine struct {
    MachineID string
    Location  string
    
    // ĐÂY LÀ NESTED STRUCT: Máy bán vé "sở hữu" 1 cấu hình mạng
    // Trường tên là Config, kiểu dữ liệu là NetworkConfig
    Config    NetworkConfig 
    
    // Máy bán vé "sở hữu" 1 hộp tiền
    Box       CashBox       
}

3. Khởi tạo và Truy cập Nested Struct

Khi khởi tạo một Struct lồng nhau, bạn sẽ dùng cú pháp lồng các dấu ngoặc nhọn {}. Trông nó sẽ rất giống với cấu trúc của một chuỗi JSON.

func main() {
    // Khởi tạo Struct lồng nhau
    tvm01 := TicketMachine{
        MachineID: "TVM-Q1-001",
        Location:  "Ga Bến Thành",
        Config: NetworkConfig{
            IPAddress:  "10.0.0.50",
            MacAddress: "00:1B:44:11:3A:B7",
            SubnetMask: "255.255.255.0", // Bắt buộc phải có dấu phẩy ở cuối
        },
        Box: CashBox{
            BoxID:    "BOX-001",
            Capacity: 10000,
            Current:  250,
        },
    }

    // Truy cập dữ liệu lồng nhau bằng cách "chấm" liên tiếp
    fmt.Printf("Máy %s có IP là: %s\n", tvm01.MachineID, tvm01.Config.IPAddress)
    
    // Gán lại giá trị
    tvm01.Box.Current += 1 
    fmt.Println("Số lượng tiền hiện tại:", tvm01.Box.Current)
}

4. Tuyệt kỹ Backend: Dùng Con trỏ cho Nested Struct

Đây là bí kíp giúp bạn xử lý những cục dữ liệu trả về từ Database hoặc API một cách chuyên nghiệp.

Nếu bạn khai báo Config NetworkConfig như ở trên, biến Config sẽ luôn luôn được tạo ra trên RAM với các Zero Value (chuỗi rỗng), kể cả khi máy bán vé đó chưa được cài đặt mạng.

Trong thiết kế API, nếu bạn muốn thể hiện rằng "Dữ liệu mạng đang bị null" (chưa tồn tại), bạn BẮT BUỘC phải dùng Con trỏ (Pointer) cho Nested Struct.

type TicketMachinePtr struct {
    MachineID string
    Location  string
    // Dùng con trỏ *NetworkConfig thay vì NetworkConfig
    Config    *NetworkConfig 
}

func main() {
    // Khởi tạo máy bán vé nhưng CHƯA lắp card mạng
    tvm02 := TicketMachinePtr{
        MachineID: "TVM-Q2-002",
        Location:  "Ga Suối Tiên",
        Config:    nil, // Rõ ràng là chưa có mạng!
    }

    // Khi có con trỏ, BẮT BUỘC phải check nil trước khi chấm (.)
    // Nếu không kiểm tra mà gọi tvm02.Config.IPAddress -> SERVER SẼ CRASH (Panic)
    if tvm02.Config != nil {
        fmt.Println("IP:", tvm02.Config.IPAddress)
    } else {
        fmt.Println("Máy này đang Offline, chưa cấu hình mạng.")
    }
}

Nhờ sử dụng con trỏ *NetworkConfig, khi chuyển Struct này thành JSON trả về cho Frontend, trường Config sẽ hiển thị là null thay vì một object rỗng vô nghĩa. Điều này giúp frontend dễ dàng bắt luồng logic (if null thì hiện giao diện cảnh báo mất mạng).


Tổng kết

Việc tổ chức dữ liệu qua Struct trong Go yêu cầu sự tỉ mỉ, nhưng đổi lại là một kiến trúc vô cùng vững chãi:

  1. Khởi tạo: Luôn dùng phương pháp Named (Theo tên) để code an toàn khi nâng cấp, tránh xa Positional (Theo thứ tự).
  2. Lồng ghép: Nested Struct dùng để biểu diễn mối quan hệ sở hữu (Has-A), giúp gom nhóm các trường dữ liệu khổng lồ thành các module nhỏ dễ quản lý.
  3. Tối ưu với Con trỏ: Dùng *Struct cho các trường dữ liệu có thể không tồn tại (Optional/Nullable) để phân biệt rõ ràng giữa giá trị rỗng và không có dữ liệu.

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í