什么是结构体?
结构体是用户定义的类型,表示字段集合。它可以用于将数据分组到一个单元中,而不是将它们作为单独的类型进行维护。
例如,员工具有 firstName,lastName 和 age。将这三个属性可以组合成一个结构体 employee
。
声明一个结构
type Employee struct {
firstName string
lastName string
age int
}
上面的代码段声明了一个 Employee 结构类型,其字段为 firstName、lastName 和 age。上面的 Employee 结构被称为 named struct(命名结构),因为它创建了一个名为 Employee 的新数据类型,使用它可以创建 Employee 结构。
这个结构也可以通过在一行中声明属于同一类型的字段,并在后面加上类型名来使结构更加紧凑。在上面的结构中,firstName 和 lastName 属于同一类型的字符串,因此该结构可以改写为
type Employee struct {
firstName, lastName string
age, salary int
}
虽然上面的语法节省了几行代码,但它并没有使字段声明变得明确。请不要使用上面这个语法。
创建命名结构体
让我们用下面的简单程序来声明一个 Employee 的结构。
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)
}
上面的 Employee
结构称为命名结构,因为它创建了一个名为 Employee
的新类型,可用于创建 Employee
类型的结构。
在上面程序的第 7 行,我们创建了一个命名为 Employee 的结构类型,在上面程序的第 17 行,通过指定每个字段名的值来定义 emp1 结构。在声明结构类型时,字段的顺序不一定要和字段名的顺序相同。在这种情况下,我们改变了 lastName 的位置,把它移到了最后。这样做就不会出现任何问题。
在上述程序的第 25 行,通过省略字段名来定义 emp2
。在这种情况下,需要保持字段的顺序与结构声明中指定的相同。请不要使用这种语法,因为这会使人难以弄清哪个值是哪个字段。我们在这里指定这种格式只是为了让大家明白这也是一种有效的语法:)
上面的程序打印
Employee 1 {Sam Anderson 25 500}
Employee 2 {Thomas Paul 29 800}
创建匿名结构体
可以在不声明新类型的情况下声明结构,这种类型的结构称为匿名结构。
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)
}
在上面程序的第 8 行,定义了一个匿名结构变量 emp3。正如我们已经提到的,这个结构被称为匿名结构,因为它只是创建了一个新的结构变量 emp3,并没有定义任何新的结构类型,如命名结构。
这个程序输出
Employee 3 {Andreah Nikola 31 5000}
访问结构的各个字段
点 . 运算符用于访问结构体的各个字段。
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)
}
上面程序中的 emp6.firstName 访问 emp6
结构体的 firstName
字段。在第 25 行我们修改了 employee 的 salary。该程序打印,
First Name: Sam
Last Name: Anderson
Age: 55
Salary: $6000
New Salary: $6500
结构的零值
定义结构并且未使用任何值显式初始化时,默认情况下会为结构的字段分配其零值。
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)
}
上面的程序定义了 emp4
,但它没有用任何值初始化。因此,firstName
和 lastName
被赋予字符串的零值 “”,age
和 salary
被赋予 int 的零值 0。此程序输出
First Name:
Last Name:
Age: 0
Salary: 0
也可以为某些字段指定值并忽略其余字段。在这种情况下,忽略的字段名称被赋予零值。
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)
}
在上面的程序中第 16 和 17 行,firstName
和 lastName
被初始化,而 age
和 salary
则没有。因此,age
和 salary
被赋予零值。该程序输出
First Name: John
Last Name: Paul
Age: 0
Salary: 0
指向结构的指针
也可以创建指向结构的指针。
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)
}
上面程序中的 emp8 是指向 Employee
结构的指针。 (*emp8).firstName
是访问 emp8
结构的 firstName
字段的语法。该程序输出,
First Name: Sam
Age: 55
该语言为我们提供了使用 emp8.firstName
而不是显式解除引用 (*emp8).firstName
这种方式来访问 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)
}
我们使用 emp8.firstName
来访问上面程序中的 firstName
字段,这个程序输出,
First Name: Sam
Age: 55
匿名字段
可以使用只包含没有字段名的类型的字段创建结构。这些字段称为匿名字段。
下面的代码片段创建了一个结构体 Person
,它有两个匿名字段 string
和 int
type Person struct {
string
int
}
尽管匿名字段没有明确的名称,但默认情况下,匿名字段的名称是其类型的名称。例如在上面的 Person 结构中,虽然字段是匿名的,但默认情况下,它们采用字段类型的名称。所以 Person 结构有 2 个字段,名称分别为 string 和 int。
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)
}
在上述程序的第 17 行和第 18 行中,我们使用 Person 结构的匿名字段的类型作为字段名来访问。在上述程序的第 17 行和第 18 行,我们使用 Person 结构的匿名字段的类型作为字段名,分别是 string 和 int。上述程序打印
naveen
50
嵌套的结构体
结构体可能包含一个字段,而该字段又是一个结构体。这些结构体称为嵌套结构体。
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)
}
上述程序中的 Person
结构有一个字段 address
,而该字段又是一个结构。该程序输出
Name: Naveen
Age: 50
City: Chicago
State: Illinois
提升字段
属于结构中匿名结构字段的字段称为提升字段,因为它们可以被访问,就好像它们属于包含匿名结构字段的结构一样。我可以理解这个定义非常复杂,所以让我们直接看下面的代码来理解:)。
type Address struct {
city string
state string
}
type Person struct {
name string
age int
Address
}
在上面的代码片段中,Person
结构有一个匿名字段 Address
,它是一个结构体。现在,Address
结构的字段即 city
和 state
被称为提升字段,因为它们可以像在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
}
在上面程序的第 29 30 行中,访问提升的字段 city
和 state
,就好像它们在结构体 p
直接声明 p.city
和 p.state
一样。该程序输出
Name: Naveen
Age: 50
City: Chicago
State: Illinois
导出的结构体和字段
如果结构类型以大写字母开头,则它是导出类型,可以从其他包访问它。类似地,如果结构的字段以大写字母开头,则可以从其他包中访问它们。
让我们编写一个包含自定义包的程序,以便更好地理解这一点。
在你的 Documents 目录下创建一个名为 structs 的文件夹。请随意在你喜欢的地方创建。我更喜欢在我的 Documents 目录下。
mkdir ~/Documents/structs
让我们创建一个名为 structs 的 go 模块。
cd ~/Documents/structs/
go mod init structs
在 structs 里面再建立一个目录 computer。
mkdir computer
在 computer
目录中,使用文件名 spec.go
保存下面的程序
package computer
type Spec struct { //exported struct
Maker string //exported field
model string //unexported field
Price int //exported field
}
上面的代码片段创建了一个包 computer
,其中包含一个导出的结构类型 Spec
,其中包含两个导出字段 Maker
和 Price
以及一个未导出的字段 model
。让我们从 main package 导入这个包并使用 Spec
结构体。
在 structs
目录中创建一个名为 main.go
的文件,并在 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)
}
包结构应如下所示,
├── structs
│ ├── computer
│ │ └── spec.go
│ ├── go.mod
│ └── main.go
在上述程序的第 4 行,我们导入了 computer 包。在第 13 行和第 14 行,我们访问结构体 Spec 的两个导出字段Maker 和 Price。这个程序可以通过执行命令 go install,然后执行 structs 命令来运行。如果你不清楚如何运行围棋程序,请访问 https://golangbot.com/hello-world-gomod/#1goinstall,了解更多。
go install
structs
运行上面的命令将打印,
Maker: apple
Price: 50000
如果我们访问未导出的字段 model
,编译器会报错。用以下代码替换 main.go
的内容。
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)
}
在程序的第 12 行中,我们尝试访问未导出的字段 model
。运行此程序将导致编译错误。
# structs
./main.go:12:13: unknown field 'model' in struct literal of type computer.Spec
相等的结构体
结构是值类型,如果它们的每个字段都是值类型的,则它们是可比较的。如果两个结构变量对应的字段相等,则认为它们是相等的
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")
}
}
在上面的程序中,name
结构体包含两个 string 字段。由于字符串是值类型的,可以比较 name
类型的两个结构体变量。
在上面的程序中,name1 和 name2 相等,而 name3 和 name4 不相等。这个程序将输出。
name1 and name2 are equal
name3 and name4 are not equal
如果结构变量包含无法比较的字段,则它们不具有可比性。
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")
}
}
在上面的程序中,image
结构体包含一个 map
类型的字段 data
。字典不具有可比性,因此无法比较 image1
和 image2
。如果运行此程序,编译将失败,爆出错误
./prog.go:20:12: invalid operation: image1 == image2 (struct containing map[int]int cannot be compared)
github 上提供了本教程的源代码