Struct trong Go
Struct có thể lưu trữ một nhóm dữ liệu các kiểu khác nhau, là một kiểu phức hợp. Go đã loại bỏ lớp và kế thừa, đồng thời cũng loại bỏ phương thức khởi tạo, cố ý làm yếu chức năng hướng đối tượng, Go không phải là một ngôn ngữ OOP truyền thống, nhưng Go vẫn có bóng dáng của OOP, thông qua struct và phương thức cũng có thể mô phỏng ra một lớp. Dưới đây là một ví dụ đơn giản về struct:
type Programmer struct {
Name string
Age int
Job string
Language []string
}Khai báo
Việc khai báo struct rất đơn giản, ví dụ như sau:
type Person struct {
name string
age int
}Bản thân struct và các trường bên trong nó đều tuân theo cách đặt tên viết hoa viết thường để phơi bày. Đối với một số trường liền kề có kiểu giống nhau, có thể không cần lặp lại khai báo kiểu, như sau:
type Rectangle struct {
height, width, area int
color string
}TIP
Khi khai báo trường struct, tên trường không được trùng với tên phương thức
Khởi tạo
Go không tồn tại phương thức khởi tạo, trong hầu hết các trường hợp áp dụng cách sau để khởi tạo struct, khi khởi tạo giống như map chỉ định tên trường rồi khởi tạo giá trị trường
programmer := Programmer{
Name: "jack",
Age: 19,
Job: "coder",
Language: []string{"Go", "C++"},
}Nhưng cũng có thể bỏ qua tên trường, khi bỏ qua tên trường, phải khởi tạo tất cả các trường, thường không khuyến nghị sử dụng cách này, vì khả năng đọc rất kém.
programmer := Programmer{
"jack",
19,
"coder",
[]string{"Go", "C++"}}Nếu quá trình khởi tạo phức tạp, bạn cũng có thể viết một hàm để khởi tạo struct, giống như dưới đây, bạn cũng có thể hiểu nó như một hàm khởi tạo
type Person struct {
Name string
Age int
Address string
Salary float64
}
func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}Nhưng Go không hỗ trợ quá tải hàm và phương thức, nên bạn không thể định nghĩa các tham số khác nhau cho cùng một hàm hoặc phương thức. Nếu muốn khởi tạo struct bằng nhiều cách, hoặc là tạo nhiều hàm khởi tạo, hoặc khuyến nghị sử dụng chế độ options.
Chế độ options
Chế độ options là một chế độ thiết kế rất phổ biến trong ngôn ngữ Go, có thể khởi tạo struct linh hoạt hơn, khả năng mở rộng mạnh, và không cần thay đổi chữ ký hàm của hàm khởi tạo. Giả sử có một struct như sau
type Person struct {
Name string
Age int
Address string
Salary float64
Birthday string
}Khai báo một kiểu PersonOptions, nó nhận một tham số kiểu *Person, nó phải là con trỏ, vì chúng ta cần gán giá trị cho Person trong closure.
type PersonOptions func(p *Person)Tiếp theo tạo các hàm options, chúng thường bắt đầu bằng With, giá trị trả về của chúng là một hàm closure.
func WithName(name string) PersonOptions {
return func(p *Person) {
p.Name = name
}
}
func WithAge(age int) PersonOptions {
return func(p *Person) {
p.Age = age
}
}
func WithAddress(address string) PersonOptions {
return func(p *Person) {
p.Address = address
}
}
func WithSalary(salary float64) PersonOptions {
return func(p *Person) {
p.Salary = salary
}
}Chữ ký hàm khởi tạo thực tế như sau, nó nhận một tham số kiểu PersonOptions độ dài biến đổi.
func NewPerson(options ...PersonOptions) *Person {
// Ưu tiên áp dụng options
p := &Person{}
for _, option := range options {
option(p)
}
// Xử lý giá trị mặc định
if p.Age < 0 {
p.Age = 0
}
......
return p
}Như vậy đối với nhu cầu khởi tạo khác nhau chỉ cần một hàm khởi tạo là có thể hoàn thành, chỉ cần truyền vào các hàm Options khác nhau là được
func main() {
pl := NewPerson(
WithName("John Doe"),
WithAge(25),
WithAddress("123 Main St"),
WithSalary(10000.00),
)
p2 := NewPerson(
WithName("Mike jane"),
WithAge(30),
)
}Chế độ options hàm có thể thấy trong rất nhiều dự án mã nguồn mở, cách khởi tạo gRPC Server cũng áp dụng chế độ thiết kế này. Chế độ options hàm chỉ phù hợp với khởi tạo phức tạp, nếu tham số chỉ có vài cái đơn giản, khuyến nghị vẫn dùng hàm khởi tạo thông thường để giải quyết.
Tổ hợp
Trong Go, quan hệ giữa các struct được biểu thị thông qua tổ hợp, có thể tổ hợp rõ ràng, cũng có thể tổ hợp ẩn danh, cách sau sử dụng càng giống kế thừa, nhưng về bản chất không có bất kỳ thay đổi nào. Ví dụ:
Cách tổ hợp rõ ràng
type Person struct {
name string
age int
}
type Student struct {
p Person
school string
}
type Employee struct {
p Person
job string
}Khi sử dụng cần chỉ định rõ ràng trường p
student := Student{
p: Person{name: "jack", age: 18},
school: "lili school",
}
fmt.Println(student.p.name)Còn tổ hợp ẩn danh có thể không cần chỉ định rõ ràng trường
type Person struct {
name string
age int
}
type Student struct {
Person
school string
}
type Employee struct {
Person
job string
}Tên trường ẩn danh mặc định là tên kiểu, người gọi có thể trực tiếp truy cập các trường và phương thức của kiểu đó, nhưng ngoài tiện lợi hơn ra thì không có bất kỳ khác biệt nào so với cách đầu tiên.
student := Student{
Person: Person{name: "jack",age: 18},
school: "lili school",
}
fmt.Println(student.name)Con trỏ
Đối với con trỏ struct而言, không cần giải tham chiếu là có thể trực tiếp truy cập nội dung của struct, ví dụ như sau:
p := &Person{
name: "jack",
age: 18,
}
fmt.Println(p.age,p.name)Khi biên dịch sẽ chuyển đổi thành (*p).name, (*p).age, thực ra vẫn cần giải tham chiếu, nhưng khi mã hóa có thể省去, coi như một đường cú pháp.
Nhãn
Nhãn struct là một hình thức siêu lập trình, kết hợp với phản xạ có thể tạo ra rất nhiều chức năng kỳ diệu, định dạng như sau
`key1:"val1" key2:"val2"`Nhãn là một dạng cặp khóa-giá trị, sử dụng khoảng trắng để phân cách. Khả năng chịu lỗi của nhãn struct rất thấp, nếu không viết nhãn struct theo định dạng chính xác, thì sẽ dẫn đến không thể đọc bình thường, nhưng khi biên dịch sẽ không có bất kỳ báo lỗi nào, dưới đây là một ví dụ sử dụng.
type Programmer struct {
Name string `json:"name"`
Age int `yaml:"age"`
Job string `toml:"job"`
Language []string `properties:"language"`
}Việc sử dụng nhãn struct rộng rãi nhất là định nghĩa bí danh trong các định dạng tuần tự hóa khác nhau, việc sử dụng nhãn cần kết hợp với phản xạ mới có thể phát huy đầy đủ chức năng của nó.
Căn chỉnh bộ nhớ
Phân bố bộ nhớ của các trường struct trong Go tuân theo quy tắc căn chỉnh bộ nhớ, làm như vậy có thể giảm số lần CPU truy cập bộ nhớ, tương ứng chiếm bộ nhớ nhiều hơn, thuộc về một loại thủ đoạn đổi không gian lấy thời gian. Giả sử có struct như sau
type Num struct {
A int64
B int32
C int16
D int8
E int32
}Biết số byte chiếm dụng của các kiểu này
int64chiếm 8 byteint32chiếm 4 byteint16chiếm 2 byteint8chiếm 1 byte
Toàn bộ struct chiếm bộ nhớ dường như là 8+4+2+1+4=19 byte sao, đương nhiên không phải như vậy, theo quy tắc căn chỉnh bộ nhớ而言, độ dài chiếm bộ nhớ của struct ít nhất là bội số nguyên của trường lớn nhất, phần thiếu thì bù vào. Trong struct này lớn nhất là int64 chiếm 8 byte, nên phân bố bộ nhớ như hình dưới đây

Nên thực tế là chiếm 24 byte, trong đó có 5 byte vô dụng.
Xem struct dưới đây
type Num struct {
A int8
B int64
C int8
}Sau khi hiểu quy tắc trên, có thể rất nhanh hiểu được bộ nhớ chiếm dụng của nó cũng là 24 byte, tuy nó chỉ có ba trường, lãng phí tới 14 byte.

Nhưng chúng ta có thể điều chỉnh các trường, sửa thành thứ tự như sau
type Num struct {
A int8
C int8
B int64
}Như vậy bộ nhớ chiếm dụng biến thành 16 byte, lãng phí 6 byte, giảm lãng phí bộ nhớ 8 byte.

Về lý thuyết mà nói, để các trường trong struct phân bố theo thứ tự hợp lý, có thể giảm bộ nhớ chiếm dụng của nó. Nhưng trong quá trình mã hóa thực tế, không có lý do cần thiết để làm như vậy, nó không nhất định có thể mang lại nâng cao thực chất về mặt giảm bộ nhớ chiếm dụng, nhưng nhất định sẽ tăng huyết áp và gánh nặng tinh thần của nhân viên phát triển, đặc biệt là trong nghiệp vụ một số trường của struct có thể lớn đến vài chục hoặc vài trăm cái, nên chỉ cần hiểu là được.
TIP
Nếu bạn thực sự muốn tiết kiệm bộ nhớ thông qua phương pháp này, có thể xem hai thư viện này
Họ sẽ kiểm tra struct trong mã nguồn của bạn, tính toán và sắp xếp lại các trường struct để tối thiểu hóa bộ nhớ struct chiếm dụng.
Struct rỗng
Struct rỗng không có trường, không chiếm không gian bộ nhớ, chúng ta có thể thông qua hàm unsafe.SizeOf để tính toán kích thước byte chiếm dụng
func main() {
type Empty struct {}
fmt.Println(unsafe.Sizeof(Empty{}))
}Xuất ra
0Có rất nhiều tình huống sử dụng của struct rỗng, ví dụ như đã đề cập trước đó, làm kiểu giá trị của map, có thể sử dụng map như set, hoặc là làm kiểu của kênh, biểu thị chỉ làm thông báo loại kênh.
