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 make và new
make và new 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
Tvà trả về một con trỏ kiểu*Ttrỏ đế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ụ:0cho số nguyên,""cho chuỗi,nilcho con trỏ/slice/map/channel). newcó 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:} } - Cấp phát bộ nhớ cho một giá trị kiểu
-
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. makekhô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ểuT.- Mục đích của
makelà 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 } - 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ư
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)
}