Kiểu Cấu Trúc (Struct) trong Go

Giới thiệu về Cấu trúc (Struct)

Cấu trúc (struct) trong Go là một kiểu dữ liệu tổng hợp mạnh mẽ, cho phép bạn nhóm các trường dữ liệu có thể thuộc các kiểu khác nhau thành một đơn vị logic duy nhất. Điều này rất hữu ích để mô hình hóa các thực thể trong thế giới thực hoặc tổ chức dữ liệu có liên quan lại với nhau.

Mỗi phần tử bên trong một cấu trúc được gọi là một trường (field) hoặc thuộc tính. Không giống như các ngôn ngữ lập trình hướng đối tượng truyền thống, Go sử dụng các cấu trúc thay vì các lớp (class) và không hỗ trợ kế thừa theo cách cổ điển.

Định nghĩa và Khởi tạo Cấu trúc

Để định nghĩa một kiểu cấu trúc mới, bạn sử dụng từ khóa type theo sau là tên cấu trúc, rồi đến từ khóa struct và một khối chứa các trường của cấu trúc đó. Tên trường bắt đầu bằng chữ cái viết hoa sẽ có thể truy cập từ bên ngoài gói (public), trong khi chữ cái viết thường sẽ chỉ truy cập được trong cùng gói (private).

type NhanVien struct {
    HoTen string
    Tuoi int
    GioiTinh string
    DiaChi string
}

Khởi tạo một biến cấu trúc

Có nhiều cách để khởi tạo một biến cấu trúc:

1. Khai báo với giá trị zero mặc định

Go sẽ tự động gán giá trị zero cho tất cả các trường của cấu trúc (ví dụ: 0 cho số nguyên, "" cho chuỗi, false cho boolean, nil cho con trỏ/slice/map).

var nv1 NhanVien // nv1.HoTen="", nv1.Tuoi=0, ...
nv1.HoTen = "Nguyễn Văn A"
nv1.Tuoi = 25
nv1.GioiTinh = "Nam"
nv1.DiaChi = "Hà Nội"

2. Khai báo và gán giá trị từng trường

nv2 := NhanVien{} // Khởi tạo một cấu trúc rỗng, sau đó gán
nv2.HoTen = "Trần Thị B"
nv2.Tuoi = 30
nv2.GioiTinh = "Nữ"
nv2.DiaChi = "TP. Hồ Chí Minh"

3. Khởi tạo bằng cách chỉ định tên trường

Đây là cách an toàn và dễ đọc nhất, đặc biệt khi cấu trúc có nhiều trường. Thứ tự các trường không quan trọng.

var nv3 NhanVien = NhanVien{
    HoTen: "Lê Văn C",
    Tuoi: 40,
    DiaChi: "Đà Nẵng",
    GioiTinh: "Nam", // Lưu ý dấu phẩy nếu '}' không cùng dòng với trường cuối cùng
}

// Hoặc dùng khai báo ngắn gọn
nv4 := NhanVien{
    HoTen: "Phạm Thị D",
    Tuoi: 35,
    GioiTinh: "Nữ",
    DiaChi: "Cần Thơ",
}

4. Khởi tạo theo thứ tự các trường (vị trí)

Bạn phải cung cấp tất cả các giá trị theo đúng thứ tự khai báo các trường trong cấu trúc. Cách này ít được khuyến khích vì khó đọc và dễ gây lỗi nếu thứ tự trường thay đổi.

nv5 := NhanVien{"Hoàng Văn E", 28, "Nam", "Hải Phòng"}

Cấu trúc là kiểu giá trị (Value Type)

Trong Go, cấu trúc là một kiểu giá trị. Điều này có nghĩa là khi bạn gán một biến cấu trúc cho một biến khác hoặc truyền một cấu trúc vào một hàm, một bản sao hoàn chỉnh của cấu trúc đó sẽ được tạo ra. Mọi thay đổi trên bản sao sẽ không ảnh hưởng đến cấu trúc gốc.

package main

import "fmt"

type Diem struct {
    X int
    Y int
}

func main() {
    p1 := Diem{X: 10, Y: 20}
    p2 := p1 // p2 là một bản sao của p1
    p2.X = 30 // Thay đổi p2 không ảnh hưởng p1

    fmt.Println("p1:", p1) // Output: p1: {10 20}
    fmt.Println("p2:", p2) // Output: p2: {30 20}
}

Phân biệt makenew

makenew là hai hàm tích hợp sẵn trong Go dùng để cấp phát bộ nhớ, nhưng chúng có mục đích và cách sử dụng khác nhau:

  • new(T):

    • Cấp phát bộ nhớ cho một giá trị kiểu T và trả về một con trỏ kiểu *T trỏ đến vùng nhớ đó.
    • Giá trị được cấp phát sẽ được khởi tạo với giá trị zero tương ứng với kiểu T (ví dụ: 0 cho số nguyên, "" cho chuỗi, nil cho con trỏ/slice/map/channel).
    • new có thể sử dụng cho mọi kiểu dữ liệu trong Go.
    package main
    
    import "fmt"
    
    type NhanVien struct {
        HoTen string
        Tuoi int
        GioiTinh string
        DiaChi string
    }
    
    func main() {
        var soNguyenConTro *int = new(int)     // *soNguyenConTro là 0
        var nvConTro *NhanVien = new(NhanVien) // *nvConTro là { "", 0, "", "" }
        
        fmt.Printf("Kiểu của nvConTro: %T, Giá trị ban đầu: %+v\n", nvConTro, *nvConTro) 
        // Output: Kiểu của nvConTro: *main.NhanVien, Giá trị ban đầu: {HoTen: Tuoi:0 GioiTinh: DiaChi:}
    }
    
  • make(T, args):

    • Chỉ dùng để tạo và khởi tạo các kiểu dữ liệu tham chiếu tích hợp sẵn như slice, map, và channel.
    • make không trả về một con trỏ mà trả về một giá trị đã được khởi tạo (không phải giá trị zero) của kiểu T.
    • Mục đích của make là khởi tạo cấu trúc dữ liệu nội bộ của các kiểu này để chúng sẵn sàng sử dụng ngay lập tức (ví dụ: cấp phát mảng bên dưới cho slice, bảng băm cho map).
    package main
    
    import "fmt"
    
    func main() {
        sliceMoi := make([]int, 5)        // Tạo slice có 5 phần tử int, giá trị mặc định là 0
        mapMoi := make(map[string]int)    // Tạo map rỗng sẵn sàng sử dụng
        channelMoi := make(chan int)      // Tạo channel
        
        fmt.Printf("Kiểu của sliceMoi: %T, Giá trị ban đầu: %+v\n", sliceMoi, sliceMoi)
        // Output: Kiểu của sliceMoi: []int, Giá trị ban đầu: [0 0 0 0 0]
        _ = mapMoi
        _ = channelMoi
    }
    

Con trỏ Cấu trúc

Bạn có thể tạo một con trỏ đến một cấu trúc, cho phép bạn thao tác với cấu trúc gốc mà không cần sao chép nó. Để truy cập các trường của cấu trúc thông qua con trỏ, bạn có thể sử dụng toán tử . (Go tự động dereference con trỏ cho bạn).

package main

import "fmt"

type NhanVien struct {
    HoTen string
    Tuoi int
}

func main() {
    nv := NhanVien{HoTen: "Phạm Minh", Tuoi: 30}
    conTroNv := &nv // Lấy địa chỉ của nv, conTroNv có kiểu *NhanVien

    // Hai cách truy cập trường qua con trỏ là tương đương:
    fmt.Println((*conTroNv).HoTen) // Phạm Minh (dereference tường minh)
    fmt.Println(conTroNv.Tuoi)    // 30 (Go tự động dereference)

    conTroNv.Tuoi = 31 // Thay đổi giá trị thông qua con trỏ
    fmt.Println(nv.Tuoi) // Output: 31 (giá trị của nv đã thay đổi)
}

Tạo cấu trúc với new

Như đã đề cập, hàm new có thể được dùng để cấp phát bộ nhớ cho một cấu trúc và trả về một con trỏ tới nó. Cấu trúc được tạo sẽ có tất cả các trường được khởi tạo với giá trị zero của chúng.

package main

import "fmt"

type NhanVien struct {
    HoTen string
    Tuoi int
    GioiTinh string
    DiaChi string
}

func main() {
    svConTro := new(NhanVien) // Trả về *NhanVien với tất cả các trường là giá trị zero
    fmt.Printf("Kiểu của svConTro: %T\n", svConTro) // *main.NhanVien
    fmt.Printf("Giá trị mặc định: %+v\n", *svConTro) // {HoTen: Tuoi:0 GioiTinh: DiaChi:}

    svConTro.HoTen = "Đặng Thanh"
    svConTro.Tuoi = 22
    fmt.Printf("Giá trị sau khi gán: %+v\n", *svConTro) // {HoTen:Đặng Thanh Tuoi:22 GioiTinh: DiaChi:}
}

Cấu trúc Ẩn danh (Anonymous Structs)

Cấu trúc ẩn danh là các cấu trúc không có tên kiểu được định nghĩa trước. Chúng thường được tạo và khởi tạo trực tiếp tại chỗ khi bạn chỉ cần một cấu trúc tạm thời hoặc duy nhất mà không cần tái sử dụng kiểu.

package main

import "fmt"

func main() {
    // Định nghĩa và khởi tạo một cấu trúc ẩn danh
    thongTinTam := struct {
        TenSanPham string
        Gia float64
    }{
        TenSanPham: "Laptop Gaming",
        Gia: 25000000.0,
    }

    fmt.Println("Tên sản phẩm:", thongTinTam.TenSanPham)
    fmt.Println("Giá:", thongTinTam.Gia)
}

Trường Ẩn danh (Embedded Fields)

Go cho phép bạn nhúng một kiểu dữ liệu (có thể là một cấu trúc khác hoặc một kiểu cơ bản) trực tiếp vào một cấu trúc mà không cần cung cấp tên trường. Khi làm như vậy, tên kiểu của trường nhúng sẽ trở thành tên trường ẩn danh.

Các trường và phương thức của kiểu nhúng sẽ được "quảng bá" (promoted) lên cấu trúc chứa, cho phép bạn truy cập chúng trực tiếp như thể chúng là các trường của cấu trúc chứa.

package main

import "fmt"

type ThongTinLienHe struct {
    Email string
    DienThoai string
}

type KhachHang struct {
    string // Trường ẩn danh kiểu string (tên sẽ là "string")
    int    // Trường ẩn danh kiểu int (tên sẽ là "int")
    ThongTinLienHe // Trường ẩn danh là một cấu trúc khác
}

func main() {
    kh := KhachHang{
        "Công ty X", // Gán cho trường string
        1001,         // Gán cho trường int
        ThongTinLienHe{Email: "lienhe@congtyx.com", DienThoai: "0912345678"},
    }

    // Truy cập các trường ẩn danh
    fmt.Println("Tên khách hàng:", kh.string) // Truy cập trường string ẩn danh
    fmt.Println("Mã khách hàng:", kh.int)     // Truy cập trường int ẩn danh

    // Truy cập các trường của cấu trúc nhúng (promoted fields)
    fmt.Println("Email liên hệ:", kh.Email)       // Truy cập trực tiếp Email
    fmt.Println("Điện thoại liên hệ:", kh.DienThoai) // Truy cập trực tiếp DienThoai

    // Lưu ý: Các trường ẩn danh phải có kiểu khác nhau trong cùng một cấu trúc để tránh xung đột tên.
}

Cấu trúc Lồng nhau (Nested Structs)

Cấu trúc lồng nhau là khi một cấu trúc chứa một trường mà bản thân nó cũng là một cấu trúc khác. Điều này cho phép bạn xây dựng các mô hình dữ liệu phức tạp hơn một cách có tổ chức.

package main

import "fmt"

type Sach struct {
    TieuDe string
    Gia float64
}

type SinhVien struct {
    Ten string
    MaSo int
    MonHocYeuThich Sach // Một trường là kiểu cấu trúc Sach
}

func main() {
    sachToan := Sach{TieuDe: "Đại số tuyến tính", Gia: 150000.0}

    sv1 := SinhVien{
        Ten: "Hoàng Anh",
        MaSo: 101,
        MonHocYeuThich: sachToan, // Gán một bản sao của sachToan
    }

    fmt.Println("Tên sinh viên:", sv1.Ten)
    fmt.Println("Môn học yêu thích:", sv1.MonHocYeuThich.TieuDe) // Truy cập qua các cấp

    // Khi bạn gán một cấu trúc lồng nhau, nó là một bản sao
    sv1.MonHocYeuThich.TieuDe = "Giải tích" // Thay đổi tiêu đề trong sv1

    fmt.Println("Tiêu đề trong sv1:", sv1.MonHocYeuThich.TieuDe) // Output: Giải tích
    fmt.Println("Tiêu đề trong sachToan gốc:", sachToan.TieuDe) // Output: Đại số tuyến tính (không thay đổi)
}

Thẻ: Go golang struct Cấu Trúc Types

Đăng vào ngày 29 tháng 7 lúc 19:55