Go Zero to Hero - Bài 30: Struct - Cấu trúc dữ liệu tự định nghĩa
Trong Golang, Struct là kiểu dữ liệu tùy chỉnh do người dùng định nghĩa để nhóm các biến có liên quan (khác hoặc cùng kiểu dữ liệu) lại với nhau dưới một tên chung. Mỗi biến bên trong một Struct được gọi là một Field (Trường dữ liệu).
1. Cú pháp khai báo Struct cơ bản
Hãy định nghĩa một giao dịch (Transaction) của hệ thống soát vé tự động (AFC):
package main
import (
"fmt"
"time"
)
// Khai báo một Struct tên là TicketTransaction
type TicketTransaction struct {
TxnID string
CardNumber string
Amount float64
StationID int
IsSuccess bool
CreatedAt time.Time
}
💡 Quy tắc Visibility (Đóng gói): Giống như biến, nếu tên Struct hoặc tên Field viết hoa chữ cái đầu (như
TxnID), nó sẽ đượcPublic(truy cập được từ các package khác). Nếu viết thường (nhưamount), nó sẽ mang tínhPrivate(chỉ dùng nội bộ trong package). Đây là cách Go thực thi tính Đóng gói (Encapsulation) mà không cần đến từ khóa rườm rà.
2. Các cách khởi tạo Struct
Có nhiều cách để tạo ra một "bản sao" (instance) từ bản thiết kế Struct:
Cách 1: Khai báo tường minh với Zero Value
Khi khai báo mà không gán giá trị, Go tự động điền Zero Value cho các field (string thành "", int thành 0, bool thành false).
var txn1 TicketTransaction
fmt.Println(txn1.TxnID) // In ra chuỗi rỗng ""
Cách 2: Khởi tạo bằng Struct Literal (Hay dùng nhất)
Khởi tạo và gán giá trị ngay lập tức bằng các cặp Tên_Field: Giá_trị.
txn2 := TicketTransaction{
TxnID: "TXN-2026-001",
CardNumber: "1234-5678-9012",
Amount: 15000.0,
StationID: 10,
IsSuccess: true,
CreatedAt: time.Now(),
}
// Truy cập và sửa đổi field bằng dấu chấm (.)
txn2.Amount = 20000.0
Cách 3: Trả về Con trỏ của Struct
Nếu Struct chứa nhiều trường dữ liệu phức tạp, việc truyền qua lại giữa các hàm sẽ tốn RAM. Các Gopher thường khởi tạo và lấy luôn địa chỉ con trỏ bằng dấu &.
// pTxn lúc này là kiểu con trỏ: *TicketTransaction
pTxn := &TicketTransaction{
TxnID: "TXN-PTR-001",
}
// Điều tuyệt vời của Go: Bạn KHÔNG CẦN dùng toán tử giải tham chiếu rườm rà (*pTxn).TxnID
// Go tự động giải tham chiếu cho bạn khi dùng dấu chấm!
fmt.Println(pTxn.TxnID)
3. Struct ẩn danh (Anonymous Struct)
Đôi khi, bạn chỉ cần tạo ra một cấu trúc dữ liệu dùng một lần duy nhất (như hứng JSON trả về từ API của bên thứ ba, hoặc format dữ liệu trả về cho Frontend). Việc định nghĩa hẳn một type ... struct ở ngoài là thừa thãi.
Lúc này, Struct ẩn danh sinh ra để cứu cánh:
func main() {
// Vừa định nghĩa cấu trúc, vừa khởi tạo giá trị ngay lập tức
apiResponse := struct {
Status int
Message string
}{
Status: 200,
Message: "Cập nhật dữ liệu thiết bị thành công",
}
fmt.Println(apiResponse.Message)
}
4. Composition (Tổ hợp) thay vì Inheritance (Kế thừa)
Go dũng cảm vứt bỏ cơ chế Kế thừa (Inheritance) phức tạp của C++ hay Java. Thay vào đó, Go áp dụng triết lý Composition (Tổ hợp): Bạn lắp ráp các sự vật nhỏ lại để thành sự vật lớn thay vì kế thừa bản chất.
Hãy xem cách Go "nhúng" (Embed) một Struct vào trong một Struct khác:
// Định nghĩa phần cứng chung
type Hardware struct {
IPAddress string
MacAddress string
Firmware string
}
// Định nghĩa Máy bán vé (TVM)
type TicketVendingMachine struct {
Hardware // Nhúng toàn bộ Struct Hardware vào đây (Embed)
MachineID string
CashInBox float64
}
func main() {
tvm := TicketVendingMachine{
MachineID: "TVM-ST1-01",
Hardware: Hardware{
IPAddress: "192.168.1.15",
Firmware: "v2.1.0",
},
}
// TÍNH NĂNG ĐỈNH CAO:
// Bạn có thể truy cập thẳng IPAddress mà không cần gõ tvm.Hardware.IPAddress
fmt.Println("IP của máy:", tvm.IPAddress)
}
5. Phép thuật Backend: Struct Tags
Khi làm việc với Database (như PostgreSQL qua GORM) hoặc trả về API (JSON/XML), làm sao để Go biết trường trong Struct tương ứng với cột nào trong DB hoặc key nào trong JSON?
Go giải quyết điều này bằng Struct Tags – những đoạn text được bao bởi cặp dấu backtick ` nằm ngay sau định nghĩa của Field.
import (
"encoding/json"
"fmt"
)
type UserProfile struct {
// Khi parse JSON, field này sẽ map với key "user_id"
ID int `json:"user_id"`
// Nếu email rỗng, nó sẽ bị bỏ qua khi convert sang JSON (omitempty)
Email string `json:"email,omitempty"`
// Dấu - nghĩa là tuyệt đối giấu field này đi, không bao giờ xuất ra JSON
Password string `json:"-"`
}
func main() {
user := UserProfile{ID: 101, Email: "", Password: "secret123"}
// Convert Struct sang định dạng JSON
jsonData, _ := json.Marshal(user)
// Kết quả chỉ có mỗi {"user_id":101} vì Email rỗng đã bị bỏ qua, Password bị ẩn.
fmt.Println(string(jsonData))
}
Tổng kết
Trái tim của việc tổ chức dữ liệu trong Go được gói gọn qua các điểm sau:
- Struct: Gom nhóm các field có liên quan lại với nhau, khởi tạo nhanh gọn.
- Anonymous Struct: Phù hợp để hứng/đẩy các cục dữ liệu dùng một lần (như JSON requests).
- Composition: Lắp ráp các khối Struct nhỏ thành Struct lớn thay vì lạm dụng tính kế thừa rườm rà.
- Struct Tags: Vũ khí tối thượng của Backend để định tuyến dữ liệu với JSON, DB.
All rights reserved