什么是结构体?


结构体是用户定义的类型,表示字段集合。它可以用于将数据分组到一个单元中,而不是将它们作为单独的类型进行维护。

例如,员工具有 firstName,lastName 和 age。将这三个属性可以组合成一个结构体 employee

声明一个结构

  1. type Employee struct {
  2. firstName string
  3. lastName string
  4. age int
  5. }

上面的代码段声明了一个 Employee 结构类型,其字段为 firstName、lastName 和 age。上面的 Employee 结构被称为 named struct(命名结构),因为它创建了一个名为 Employee 的新数据类型,使用它可以创建 Employee 结构。

这个结构也可以通过在一行中声明属于同一类型的字段,并在后面加上类型名来使结构更加紧凑。在上面的结构中,firstName 和 lastName 属于同一类型的字符串,因此该结构可以改写为

  1. type Employee struct {
  2. firstName, lastName string
  3. age, salary int
  4. }

虽然上面的语法节省了几行代码,但它并没有使字段声明变得明确。请不要使用上面这个语法。

创建命名结构体

让我们用下面的简单程序来声明一个 Employee 的结构。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Employee struct {
  6. firstName string
  7. lastName string
  8. age int
  9. salary int
  10. }
  11. func main() {
  12. //creating struct specifying field names
  13. emp1 := Employee{
  14. firstName: "Sam",
  15. age: 25,
  16. salary: 500,
  17. lastName: "Anderson",
  18. }
  19. //creating struct without specifying field names
  20. emp2 := Employee{"Thomas", "Paul", 29, 800}
  21. fmt.Println("Employee 1", emp1)
  22. fmt.Println("Employee 2", emp2)
  23. }

Run in playground

上面的 Employee 结构称为命名结构,因为它创建了一个名为 Employee 的新类型,可用于创建 Employee 类型的结构。

在上面程序的第 7 行,我们创建了一个命名为 Employee 的结构类型,在上面程序的第 17 行,通过指定每个字段名的值来定义 emp1 结构。在声明结构类型时,字段的顺序不一定要和字段名的顺序相同。在这种情况下,我们改变了 lastName 的位置,把它移到了最后。这样做就不会出现任何问题。

在上述程序的第 25 行,通过省略字段名来定义 emp2。在这种情况下,需要保持字段的顺序与结构声明中指定的相同。请不要使用这种语法,因为这会使人难以弄清哪个值是哪个字段。我们在这里指定这种格式只是为了让大家明白这也是一种有效的语法:)

上面的程序打印

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

创建匿名结构体

可以在不声明新类型的情况下声明结构,这种类型的结构称为匿名结构

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. emp3 := struct {
  7. firstName string
  8. lastName string
  9. age int
  10. salary int
  11. }{
  12. firstName: "Andreah",
  13. lastName: "Nikola",
  14. age: 31,
  15. salary: 5000,
  16. }
  17. fmt.Println("Employee 3", emp3)
  18. }

Run in playground

在上面程序的第 8 行,定义了一个匿名结构变量 emp3。正如我们已经提到的,这个结构被称为匿名结构,因为它只是创建了一个新的结构变量 emp3,并没有定义任何新的结构类型,如命名结构。

这个程序输出

  1. Employee 3 {Andreah Nikola 31 5000}

访问结构的各个字段

点 . 运算符用于访问结构体的各个字段。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Employee struct {
  6. firstName string
  7. lastName string
  8. age int
  9. salary int
  10. }
  11. func main() {
  12. emp6 := Employee{
  13. firstName: "Sam",
  14. lastName: "Anderson",
  15. age: 55,
  16. salary: 6000,
  17. }
  18. fmt.Println("First Name:", emp6.firstName)
  19. fmt.Println("Last Name:", emp6.lastName)
  20. fmt.Println("Age:", emp6.age)
  21. fmt.Printf("Salary: $%d\n", emp6.salary)
  22. emp6.salary = 6500
  23. fmt.Printf("New Salary: $%d", emp6.salary)
  24. }

Run in playground

上面程序中的 emp6.firstName 访问 emp6 结构体的 firstName 字段。在第 25 行我们修改了 employee 的 salary。该程序打印,

  1. First Name: Sam
  2. Last Name: Anderson
  3. Age: 55
  4. Salary: $6000
  5. New Salary: $6500


结构的零值


定义结构并且未使用任何值显式初始化时,默认情况下会为结构的字段分配其零值。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Employee struct {
  6. firstName string
  7. lastName string
  8. age int
  9. salary int
  10. }
  11. func main() {
  12. var emp4 Employee //zero valued struct
  13. fmt.Println("First Name:", emp4.firstName)
  14. fmt.Println("Last Name:", emp4.lastName)
  15. fmt.Println("Age:", emp4.age)
  16. fmt.Println("Salary:", emp4.salary)
  17. }

Run in playground

上面的程序定义了 emp4 ,但它没有用任何值初始化。因此,firstNamelastName 被赋予字符串的零值 “”,agesalary 被赋予 int 的零值 0。此程序输出

  1. First Name:
  2. Last Name:
  3. Age: 0
  4. Salary: 0

也可以为某些字段指定值并忽略其余字段。在这种情况下,忽略的字段名称被赋予零值。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Employee struct {
  6. firstName string
  7. lastName string
  8. age int
  9. salary int
  10. }
  11. func main() {
  12. emp5 := Employee{
  13. firstName: "John",
  14. lastName: "Paul",
  15. }
  16. fmt.Println("First Name:", emp5.firstName)
  17. fmt.Println("Last Name:", emp5.lastName)
  18. fmt.Println("Age:", emp5.age)
  19. fmt.Println("Salary:", emp5.salary)
  20. }

Run in playground

在上面的程序中第 16 和 17 行,firstNamelastName 被初始化,而 agesalary 则没有。因此,agesalary 被赋予零值。该程序输出

  1. First Name: John
  2. Last Name: Paul
  3. Age: 0
  4. Salary: 0


指向结构的指针

也可以创建指向结构的指针。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Employee struct {
  6. firstName string
  7. lastName string
  8. age int
  9. salary int
  10. }
  11. func main() {
  12. emp8 := &Employee{
  13. firstName: "Sam",
  14. lastName: "Anderson",
  15. age: 55,
  16. salary: 6000,
  17. }
  18. fmt.Println("First Name:", (*emp8).firstName)
  19. fmt.Println("Age:", (*emp8).age)
  20. }

Run in playground

上面程序中的 emp8 是指向 Employee 结构的指针。 (*emp8).firstName 是访问 emp8 结构的 firstName 字段的语法。该程序输出,

  1. First Name: Sam
  2. Age: 55

该语言为我们提供了使用 emp8.firstName 而不是显式解除引用 (*emp8).firstName 这种方式来访问 firstName 字段。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Employee struct {
  6. firstName string
  7. lastName string
  8. age int
  9. salary int
  10. }
  11. func main() {
  12. emp8 := &Employee{
  13. firstName: "Sam",
  14. lastName: "Anderson",
  15. age: 55,
  16. salary: 6000,
  17. }
  18. fmt.Println("First Name:", emp8.firstName)
  19. fmt.Println("Age:", emp8.age)
  20. }

Run in playground

我们使用 emp8.firstName 来访问上面程序中的 firstName 字段,这个程序输出,

  1. First Name: Sam
  2. Age: 55

匿名字段

可以使用只包含没有字段名的类型的字段创建结构。这些字段称为匿名字段。

下面的代码片段创建了一个结构体 Person ,它有两个匿名字段 stringint

  1. type Person struct {
  2. string
  3. int
  4. }

尽管匿名字段没有明确的名称,但默认情况下,匿名字段的名称是其类型的名称。例如在上面的 Person 结构中,虽然字段是匿名的,但默认情况下,它们采用字段类型的名称。所以 Person 结构有 2 个字段,名称分别为 string 和 int。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Person struct {
  6. string
  7. int
  8. }
  9. func main() {
  10. p1 := Person{
  11. string: "naveen",
  12. int: 50,
  13. }
  14. fmt.Println(p1.string)
  15. fmt.Println(p1.int)
  16. }

Run in playground

在上述程序的第 17 行和第 18 行中,我们使用 Person 结构的匿名字段的类型作为字段名来访问。在上述程序的第 17 行和第 18 行,我们使用 Person 结构的匿名字段的类型作为字段名,分别是 string 和 int。上述程序打印

  1. naveen
  2. 50


嵌套的结构体

结构体可能包含一个字段,而该字段又是一个结构体。这些结构体称为嵌套结构体。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Address struct {
  6. city string
  7. state string
  8. }
  9. type Person struct {
  10. name string
  11. age int
  12. address Address
  13. }
  14. func main() {
  15. p := Person{
  16. name: "Naveen",
  17. age: 50,
  18. address: Address{
  19. city: "Chicago",
  20. state: "Illinois",
  21. },
  22. }
  23. fmt.Println("Name:", p.name)
  24. fmt.Println("Age:", p.age)
  25. fmt.Println("City:", p.address.city)
  26. fmt.Println("State:", p.address.state)
  27. }

Run in playground

上述程序中的 Person 结构有一个字段 address ,而该字段又是一个结构。该程序输出

  1. Name: Naveen
  2. Age: 50
  3. City: Chicago
  4. State: Illinois

提升字段

属于结构中匿名结构字段的字段称为提升字段,因为它们可以被访问,就好像它们属于包含匿名结构字段的结构一样。我可以理解这个定义非常复杂,所以让我们直接看下面的代码来理解:)。

  1. type Address struct {
  2. city string
  3. state string
  4. }
  5. type Person struct {
  6. name string
  7. age int
  8. Address
  9. }

在上面的代码片段中,Person 结构有一个匿名字段 Address,它是一个结构体。现在,Address 结构的字段即 citystate 被称为提升字段,因为它们可以像在Person 结构本身中直接声明一样被访问。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Address struct {
  6. city string
  7. state string
  8. }
  9. type Person struct {
  10. name string
  11. age int
  12. Address
  13. }
  14. func main() {
  15. p := Person{
  16. name: "Naveen",
  17. age: 50,
  18. Address: Address{
  19. city: "Chicago",
  20. state: "Illinois",
  21. },
  22. }
  23. fmt.Println("Name:", p.name)
  24. fmt.Println("Age:", p.age)
  25. fmt.Println("City:", p.city) //city is promoted field
  26. fmt.Println("State:", p.state) //state is promoted field
  27. }

Run in playground

在上面程序的第 29 30 行中,访问提升的字段 citystate,就好像它们在结构体 p 直接声明 p.cityp.state 一样。该程序输出

  1. Name: Naveen
  2. Age: 50
  3. City: Chicago
  4. State: Illinois

导出的结构体和字段

如果结构类型以大写字母开头,则它是导出类型,可以从其他访问它。类似地,如果结构的字段以大写字母开头,则可以从其他包中访问它们。

让我们编写一个包含自定义包的程序,以便更好地理解这一点。

在你的 Documents 目录下创建一个名为 structs 的文件夹。请随意在你喜欢的地方创建。我更喜欢在我的 Documents 目录下。

  1. mkdir ~/Documents/structs

让我们创建一个名为 structs 的 go 模块

  1. cd ~/Documents/structs/
  2. go mod init structs

在 structs 里面再建立一个目录 computer。


  1. mkdir computer

computer 目录中,使用文件名 spec.go 保存下面的程序


  1. package computer
  2. type Spec struct { //exported struct
  3. Maker string //exported field
  4. model string //unexported field
  5. Price int //exported field
  6. }

上面的代码片段创建了一个包 computer ,其中包含一个导出的结构类型 Spec ,其中包含两个导出字段 MakerPrice 以及一个未导出的字段 model。让我们从 main package 导入这个包并使用 Spec 结构体。

structs 目录中创建一个名为 main.go 的文件,并在 main.go 中编写以下程序


  1. package main
  2. import (
  3. "structs/computer"
  4. "fmt"
  5. )
  6. func main() {
  7. spec := computer.Spec {
  8. Maker: "apple",
  9. Price: 50000,
  10. }
  11. fmt.Println("Maker:", spec.Maker)
  12. fmt.Println("Price:", spec.Price)
  13. }

包结构应如下所示,


  1. ├── structs
  2. ├── computer
  3. └── spec.go
  4. ├── go.mod
  5. └── main.go

在上述程序的第 4 行,我们导入了 computer 包。在第 13 行和第 14 行,我们访问结构体 Spec 的两个导出字段Maker 和 Price。这个程序可以通过执行命令 go install,然后执行 structs 命令来运行。如果你不清楚如何运行围棋程序,请访问 https://golangbot.com/hello-world-gomod/#1goinstall,了解更多。


  1. go install
  2. structs

运行上面的命令将打印,


  1. Maker: apple
  2. Price: 50000

如果我们访问未导出的字段 model,编译器会报错。用以下代码替换 main.go 的内容。

  1. package main
  2. import (
  3. "structs/computer"
  4. "fmt"
  5. )
  6. func main() {
  7. spec := computer.Spec {
  8. Maker: "apple",
  9. Price: 50000,
  10. model: "Mac Mini",
  11. }
  12. fmt.Println("Maker:", spec.Maker)
  13. fmt.Println("Price:", spec.Price)
  14. }

在程序的第 12 行中,我们尝试访问未导出的字段 model。运行此程序将导致编译错误。

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


相等的结构体

结构是值类型,如果它们的每个字段都是值类型的,则它们是可比较的。如果两个结构变量对应的字段相等,则认为它们是相等的

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type name struct {
  6. firstName string
  7. lastName string
  8. }
  9. func main() {
  10. name1 := name{
  11. firstName: "Steve",
  12. lastName: "Jobs",
  13. }
  14. name2 := name{
  15. firstName: "Steve",
  16. lastName: "Jobs",
  17. }
  18. if name1 == name2 {
  19. fmt.Println("name1 and name2 are equal")
  20. } else {
  21. fmt.Println("name1 and name2 are not equal")
  22. }
  23. name3 := name{
  24. firstName: "Steve",
  25. lastName: "Jobs",
  26. }
  27. name4 := name{
  28. firstName: "Steve",
  29. }
  30. if name3 == name4 {
  31. fmt.Println("name3 and name4 are equal")
  32. } else {
  33. fmt.Println("name3 and name4 are not equal")
  34. }
  35. }

Run in playground

在上面的程序中,name 结构体包含两个 string 字段。由于字符串是值类型的,可以比较 name 类型的两个结构体变量。

在上面的程序中,name1 和 name2 相等,而 name3 和 name4 不相等。这个程序将输出。

  1. name1 and name2 are equal
  2. name3 and name4 are not equal

如果结构变量包含无法比较的字段,则它们不具有可比性。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type image struct {
  6. data map[int]int
  7. }
  8. func main() {
  9. image1 := image{data: map[int]int{
  10. 0: 155,
  11. }}
  12. image2 := image{data: map[int]int{
  13. 0: 155,
  14. }}
  15. if image1 == image2 {
  16. fmt.Println("image1 and image2 are equal")
  17. }
  18. }

Run in playground

在上面的程序中,image 结构体包含一个 map 类型的字段 data 。字典不具有可比性,因此无法比较 image1image2。如果运行此程序,编译将失败,爆出错误

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

github 上提供了本教程的源代码

原文链接

https://golangbot.com/structs/