Struct trong Go

Trong hướng dẫn này, chúng ta sẽ tìm hiểu struct là gì, cách khai báo và sử dụng một struct trong Go, struct ẩn danh, so sanh hai struct, ...

Struct là gì?

Struct là một kiểu do người dùng định nghĩa đại diện cho một tập hợp các trường. Nó có thể được sử dụng ở những nơi có ý nghĩa khi nhóm dữ liệu thành một đơn vị duy nhất thay vì lưu trữ chúng dưới dạng các giá trị riêng biệt.

Ví dụ: một nhân viên có các thuộc tính như họ, tên và tuổi. Sẽ hợp lý khi nhóm ba thuộc tính này thành một cấu trúc duy nhất được đặt tên là Employee.

Khai báo một struct

type Employee struct {  
    firstName string
    lastName  string
    age       int
}

Đoạn mã trên khai báo một struct kiểu Employee với các trường firstName, lastNameage. Struct Employee trên được gọi struct được đặt tên vì nó tạo ra một kiểu dữ liệu mới được đặt tên là Employee bằng cách sử dụng struct Employee có thể được tạo.

Struct này cũng có thể được làm nhỏ gọn hơn bằng cách khai báo các trường thuộc cùng một kiểu trong một dòng duy nhất theo sau là tên kiểu. Trong struct trên firstNamelastName thuộc cùng một kiểu dữ liệu string và do đó struct có thể được viết lại thành

type Employee struct {  
    firstName, lastName string
    age                 int
}
Mặc dù cú pháp trên tiết kiệm một vài dòng mã, nó không làm cho khai báo trường rõ ràng. Vui lòng không sử dụng cách này để khai báo struct.

Tạo struct được đặt tên

Hãy khai báo một struct được đặt tên Employee bằng chương trình đơn giản sau.

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName string
    lastName  string
    age       int
    salary    int
}

func main() {

    //creating struct specifying field names
    emp1 := Employee{
        firstName: "Sam",
        age:       25,
        salary:    500,
        lastName:  "Anderson",
    }

    //creating struct without specifying field names
    emp2 := Employee{"Thomas", "Paul", 29, 800}

    fmt.Println("Employee 1", emp1)
    fmt.Println("Employee 2", emp2)
}

Chạy chương trình trong playground

Ở dòng số 7 của chương trình trên, chúng ta tạo một kiểu struct được đặt tên là Employee.  dòng số 17 của chương trình trên, biến emp1 được định nghĩa bằng cách chỉ định giá trị cho mỗi trường.

Thứ tự của các trường không nhất thiết phải giống thứ tự của tên trường trong khi khai báo kiểu struct. Trong trường hợp này, chúng ta đã thay đổi vị trí của trường lastName và chuyển nó xuống cuối. Điều này sẽ hoạt động mà không có bất kỳ vấn đề nào.

dòng số 25 của chương trình trên, biến emp2 được định nghĩa bằng cách bỏ qua các tên trường. Trong trường hợp này, cần phải duy trì thứ tự của các trường giống như được chỉ định trong khai báo struct.

Vui lòng không sử dụng cú pháp này vì nó gây khó khăn cho việc tìm ra giá trị nào dành cho trường nào.

Chúng tôi đã chỉ định định dạng này ở đây chỉ để hiểu rằng đây cũng là một cú pháp hợp lệ :)

Chương trình trên in ra kết quả sau:

Employee 1 {Sam Anderson 25 500}  
Employee 2 {Thomas Paul 29 800}  

Tạo struct ẩn danh

Có thể khai báo struct mà không cần tạo kiểu dữ liệu mới. Những kiểu struct này được gọi là struct ẩn danh.

package main

import (  
    "fmt"
)

func main() {  
    emp3 := struct {
        firstName string
        lastName  string
        age       int
        salary    int
    }{
        firstName: "Andreah",
        lastName:  "Nikola",
        age:       31,
        salary:    5000,
    }

    fmt.Println("Employee 3", emp3)
}

Chạy chương trình trong playground

Ở dòng số 8 của chương trình trên, một biến struct ẩn danhemp3 được định nghĩa. Như chúng ta đã đề cập, struct này được gọi là ẩn danh vì nó chỉ tạo một biến struct mới là emp3 và không xác định bất kỳ kiểu struct mới nào như struct được đặt tên.

Chương trình này xuất ra kết quả sau:

Employee 3 {Andreah Nikola 31 5000}  

Truy cập các trường riêng lẻ của một struct

Toán tử dấu chấm . được sử dụng để truy cập các trường riêng lẻ của một struct.

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName string
    lastName  string
    age       int
    salary    int
}

func main() {  
    emp6 := Employee{
        firstName: "Sam",
        lastName:  "Anderson",
        age:       55,
        salary:    6000,
    }
    fmt.Println("First Name:", emp6.firstName)
    fmt.Println("Last Name:", emp6.lastName)
    fmt.Println("Age:", emp6.age)
    fmt.Printf("Salary: $%d\n", emp6.salary)
    emp6.salary = 6500
    fmt.Printf("New Salary: $%d", emp6.salary)
}

Chạy chương trình trong playground

emp6.firstName trong chương trình trên truy cập vào trường firstName của struct emp6. Ở dòng số 25 chúng ta sửa đổi mức lương của nhân viên. Chương trình này in ra kết quả sau:

First Name: Sam  
Last Name: Anderson  
Age: 55  
Salary: $6000  
New Salary: $6500  

Giá trị mặc định của một struct

Khi một struct được định nghĩa và nó không được khởi tạo rõ ràng với bất kỳ giá trị nào, các trường của struct được gán giá trị mặc định theo mặc định.

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName string
    lastName  string
    age       int
    salary    int
}

func main() {  
    var emp4 Employee //zero valued struct
    fmt.Println("First Name:", emp4.firstName)
    fmt.Println("Last Name:", emp4.lastName)
    fmt.Println("Age:", emp4.age)
    fmt.Println("Salary:", emp4.salary)
}

Chạy chương trình trong playground

Chương trình trên định nghĩa biến emp4 nhưng nó không được khởi tạo với bất kỳ giá trị nào. Do đó firstNamelastName được giao các giá trị mặc định của kiểu string là một chuỗi rỗng "" và các trường age, salary được giao các giá trị mặc định của kiểu int đó là 0. Chương trình này in ra kết quả sau:

First Name:  
Last Name:  
Age: 0  
Salary: 0  

Cũng có thể chỉ định giá trị cho một số trường và bỏ qua phần còn lại. Trong trường hợp này, các trường bị bỏ qua được gán giá trị mặc định theo kiểu dữ liệu của trường.

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName string
    lastName  string
    age       int
    salary    int
}

func main() {  
    emp5 := Employee{
        firstName: "John",
        lastName:  "Paul",
    }
    fmt.Println("First Name:", emp5.firstName)
    fmt.Println("Last Name:", emp5.lastName)
    fmt.Println("Age:", emp5.age)
    fmt.Println("Salary:", emp5.salary)
}

Chạy chương trình trong playground

Trong chương trình trên ở dòng số 16 và 17, trường firstNamelastName được khởi tạo, ngược lại trường agesalary không được khởi tạo. Do đó agesalary được gán giá trị mặc định của kiểu int là 0. Chương trình này xuất ra:

First Name: John  
Last Name: Paul  
Age: 0  
Salary: 0  

Con trỏ đến một struct

Cũng có thể tạo con trỏ trỏ đến một struct.

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName string
    lastName  string
    age       int
    salary    int
}

func main() {  
    emp8 := &Employee{
        firstName: "Sam",
        lastName:  "Anderson",
        age:       55,
        salary:    6000,
    }
    fmt.Println("First Name:", (*emp8).firstName)
    fmt.Println("Age:", (*emp8).age)
}

Chạy chương trình trong playground

Biến emp8 trong chương trình trên là một con trỏ trỏ đến struct Employee. (*emp8).firstName là cú pháp để truy cập trường firstName của struct emp8. Chương trình này in ra:

First Name: Sam  
Age: 55  

Go cung cấp cho chúng ta tùy chọn sử dụng emp8.firstName thay vì tham chiếu rõ ràng (*emp8).firstName để truy cập trường firstName.

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName string
    lastName  string
    age       int
    salary    int
}

func main() {  
    emp8 := &Employee{
        firstName: "Sam",
        lastName:  "Anderson",
        age:       55,
        salary:    6000,
    }
    fmt.Println("First Name:", emp8.firstName)
    fmt.Println("Age:", emp8.age)
}

Chạy chương trình trong playground

Chúng ta đã sử dụng emp8.firstName để truy cập trường firstName trong chương trình trên và chương trình này cũng xuất ra kết quả:

First Name: Sam  
Age: 55  

Các trường ẩn danh

Có thể tạo struct với các trường chỉ chứa kiểu mà không có tên trường. Loại trường này được gọi là trường ẩn danh.

Đoạn mã dưới đây tạo một struct là Person có hai trường ẩn danh stringint

type Person struct {  
    string
    int
}

Mặc dù các trường ẩn danh không có tên rõ ràng, theo mặc định, tên của trường ẩn danh là tên kiểu dữ liệu của nó.

Ví dụ trong trường hợp struct Person ở trên, mặc dù các trường là ẩn danh, theo mặc định, chúng lấy tên của kiểu dữ liệu của trường. Vì vậy, struct Person có 2 trường tên là stringint.

package main

import (  
    "fmt"
)

type Person struct {  
    string
    int
}

func main() {  
    p1 := Person{
        string: "naveen",
        int:    50,
    }
    fmt.Println(p1.string)
    fmt.Println(p1.int)
}

Chạy chương trình trong playground

Ở dòng số 17 và 18 của chương trình trên, chúng ta truy cập vào các trường ẩn danh của struct Person sử dụng tên của kiểu dữ liệu thay cho tên trường là stringint tương ứng. Đầu ra của chương trình trên là:

naveen  
50  

Struct lồng nhau

Struct có thể  chứa một trường có kiểu dữ liệu là một struct. Loại struct này được gọi là struct lồng nhau.

package main

import (  
    "fmt"
)

type Address struct {  
    city  string
    state string
}

type Person struct {  
    name    string
    age     int
    address Address
}

func main() {  
    p := Person{
        name: "Naveen",
        age:  50,
        address: Address{
            city:  "Chicago",
            state: "Illinois",
        },
    }

    fmt.Println("Name:", p.name)
    fmt.Println("Age:", p.age)
    fmt.Println("City:", p.address.city)
    fmt.Println("State:", p.address.state)
}

Chạy chương trình trong playground

Struct Person trong chương trình trên có một trường address cũng là một struct. Chương trình này in ra kết quả sau:

Name: Naveen  
Age: 50  
City: Chicago  
State: Illinois  

Các trường được thăng hạng

Các trường thuộc về trường struct ẩn danh trong struct được gọi là trường được thăng hạng vì chúng có thể được truy cập như thể chúng thuộc về struct chứa trường struct ẩn danh.

Tôi có thể hiểu rằng định nghĩa này khá phức tạp vì vậy chúng ta hãy đi sâu vào một số đoạn mã để hiểu điều này :).

type Address struct {  
    city string
    state string
}
type Person struct {  
    name string
    age  int
    Address
}

Trong đoạn mã trên, struct Person có một trường ẩn danh Address là một struct. Bây giờ các trường của Address cụ thể citystate được gọi là các trường được thăng hạng vì chúng có thể được truy cập như thể chúng được khai báo trực tiếp trong chính struct Person.

package main

import (  
    "fmt"
)

type Address struct {  
    city  string
    state string
}
type Person struct {  
    name string
    age  int
    Address
}

func main() {  
    p := Person{
        name: "Naveen",
        age:  50,
        Address: Address{
            city:  "Chicago",
            state: "Illinois",
        },
    }

    fmt.Println("Name:", p.name)
    fmt.Println("Age:", p.age)
    fmt.Println("City:", p.city)   //city is promoted field
    fmt.Println("State:", p.state) //state is promoted field
}

Chạy chương trình trong playground

Ở dòng số 29 và 30 của chương trình ở trên, các trường được thăng hạng là citystate được truy cập như thể chúng được khai báo trong chính struct p bằng cách sử dụng cú pháp p.cityp.state. Chương trình này in ra kết quả sau:

Name: Naveen  
Age: 50  
City: Chicago  
State: Illinois  

Struct và trường đã xuất

Nếu một kiểu struct bắt đầu bằng một chữ cái viết hoa, thì đó là kiểu được export và nó có thể được truy cập từ các package khác. Tương tự, nếu các trường của một struct bắt đầu bằng chữ hoa, chúng có thể được truy cập từ các package khác.

Hãy viết một chương trình có các package tùy chỉnh để hiểu rõ hơn điều này.

Tạo một thư mục có tên structs trong thư mục Documents của bạn. Hãy thoải mái tạo nó ở bất cứ đâu bạn thích. Tôi thích nó nằm trong thư mục Documents của mình hơn .

mkdir ~/Documents/structs  

Hãy tạo một module Go có tên structs.

cd ~/Documents/structs/  
go mod init structs  

Tạo một thư mục computer khác bên trong structs.

mkdir computer  

Bên trong thư mục computer, tạo một tệp spec.go với nội dung sau.

package computer

type Spec struct { //exported struct  
    Maker string //exported field
    Price int //exported field
    model string //unexported field

}

Đoạn trên tạo ra một package computer có chứa một struct được export là Spec với hai trường được export là MakerPrice và một trường không được exported là model. Hãy import package này từ package main và sử dụng struct Spec.

Tạo một tập tin có tên main.go bên trong thư mục structs và viết chương trình sau vào main.go

package main

import (  
    "structs/computer"
    "fmt"
)

func main() {  
    spec := computer.Spec {
            Maker: "apple",
            Price: 50000,
        }
    fmt.Println("Maker:", spec.Maker)
    fmt.Println("Price:", spec.Price)
}

Thư mục structs nên có cơ cấu như sau,

├── structs
│   ├── computer
│   │   └── spec.go
│   ├── go.mod
│   └── main.go

Ở dòng số 4 của chương trình trên, chúng ta đã import package computer. Ở dòng số 13 và 14, chúng ta truy cập vào hai trường đã export là MakerPrice của struct Spec.

Chương trình này có thể được chạy bằng cách thực thi lệnh go install theo sau lệnh structs. Nếu bạn không chắc chắn về cách chạy chương trình Go, vui lòng truy cập bài viết này để biết thêm.

go install  
structs  

Chạy các lệnh trên sẽ in ra kết quả sau:

Maker: apple  
Price: 50000  

Nếu chúng ta cố gắng truy cập vào trường chưa được export model, trình biên dịch sẽ báo lỗi. Thay thế nội dung của main.go bằng mã sau:

package main

import (  
    "structs/computer"
    "fmt"
)

func main() {  
    spec := computer.Spec {
            Maker: "apple",
            Price: 50000,
            model: "Mac Mini",
        }
    fmt.Println("Maker:", spec.Maker)
    fmt.Println("Price:", spec.Price)
}

Ở dòng số 12 của chương trình trên, chúng ta cố gắng truy cập vào trường chưa được export là model. Chạy chương trình này sẽ dẫn đến lỗi biên dịch.

# structs
./main.go:12:13: unknown field 'model' in struct literal of type computer.Spec

Vì trường model chưa được export nên không thể truy cập trường từ các package khác.

So sánh hai struct

Struct là các kiểu giá trị và có thể so sánh được nếu từng trường của chúng có thể so sánh được. Hai biến struct được coi là bằng nhau nếu các trường tương ứng của chúng bằng nhau.

package main

import (  
    "fmt"
)

type name struct {  
    firstName string
    lastName  string
}

func main() {  
    name1 := name{
        firstName: "Steve",
        lastName:  "Jobs",
    }
    name2 := name{
        firstName: "Steve",
        lastName:  "Jobs",
    }
    
    if name1 == name2 {
        fmt.Println("name1 and name2 are equal")
    } else {
        fmt.Println("name1 and name2 are not equal")
    }

    name3 := name{
        firstName: "Steve",
        lastName:  "Jobs",
    }
    name4 := name{
        firstName: "Steve",
    }

    if name3 == name4 {
        fmt.Println("name3 and name4 are equal")
    } else {
        fmt.Println("name3 and name4 are not equal")
    }
}

Chạy chương trình trong playground

Trong chương trình trên, struct name chứa hai trường kiểu string. Vì các chuỗi có thể so sánh được, nên có thể so sánh hai biến kiểu struct name.

Trong chương trình trên, hai biến name1name2 bằng nhau trong khi name3name4 thì không. Chương trình này sẽ xuất ra:

name1 and name2 are equal  
name3 and name4 are not equal  

Các biến kiểu struct không thể so sánh được nếu chúng chứa các trường không thể so sánh được.

package main

import (  
    "fmt"
)

type image struct {  
    data map[int]int
}

func main() {  
    image1 := image{
        data: map[int]int{
            0: 155,
        }}
    image2 := image{
        data: map[int]int{
            0: 155,
        }}
    if image1 == image2 {
        fmt.Println("image1 and image2 are equal")
    }
}

Chạy chương trình trong playground

Trong chương trình trên, struct image chứa một trường data thuộc kiểu map. Kiểu map không thể so sánh, do đó image1image2 không thể so sánh được. Nếu bạn chạy chương trình này, trình biên dịch sẽ báo lỗi

./prog.go:20:12: invalid operation: image1 == image2 (struct containing map[int]int cannot be compared)

Cảm ơn bạn đã đọc. Vui lòng để lại ý kiến ​​và phản hồi của bạn.

Go
Bài Viết Liên Quan:
Phương thức (method) trong Go
Trung Nguyen 02/12/2021
Phương thức (method) trong Go

Trong hướng dẫn này, chúng ta sẽ tìm hiểu phương thức (method) trong Go là gì? Cú pháp khai báo phương thức, so sánh phương thức với hàm, ... trong Go.

Con trỏ trong Go
Trung Nguyen 28/11/2021
Con trỏ trong Go

Trong hướng dẫn này, chúng ta sẽ tìm hiểu cách con trỏ (pointer) hoạt động trong Go và nó khác với con trỏ trong các ngôn ngữ khác như C và C++ như thế nào.

Chuỗi trong Go
Trung Nguyen 28/11/2021
Chuỗi trong Go

Chuỗi (string) xứng đáng được đề cập đặc biệt trong Go vì chúng khác biệt trong cách triển khai khi so sánh với các ngôn ngữ khác.

Map trong Go
Trung Nguyen 28/11/2021
Map trong Go

Trong hướng dẫn này, chúng ta sẽ tìm hiểu map là gì, cú pháp khai báo map, thêm phần tử, xóa phần tử, duyệt các phần tử, ... của map trong Go.