Go从入门到精通——使用结构体和指针

使用结构体和指针

本章节介绍如下内容

  • 结构体是什么?
  • 创建结构体
  • 嵌套结构体
  • 自定义结构体数据结字段的默认值
  • 比较结构体
  • 理解共有和私有值
  • 区分指针引用和值引用

  结构体是由数据元素组成的结构,它是一个很有用的编程构件。

1.1 结构体是什么?

  结构体是一系列具有指定数据类型的数据字段,它能够让你通过单个变量引用一系列相关的值。通过使用结构体,可在单个变量中存储众多类型不同的数据字段。存储在结构体中的值可轻松地访问和修改,这提供了一种灵活的数据结构创建方式。通过使用结构体,可提高模块化程度,还能够让你创建并传递复杂的数据结构。

  还可将结构体视为用于创建数据记录(如员工记录和机票预订)的模版。

  程序清单:声明并创建一个简单的结构体.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
 
import (
    "fmt"
)
 
// 声明结构类型
type Movie struct {
    Name   string
    Rating float32
}
 
func main() {
    m := Movie{
        Name:   "Citizen Kane",
        Rating: 10,
    }
    fmt.Println(m.Name, m.Rating)
}

  • 关键字 type 定义一种新类型。
  • 将新类型的名称指定为 Movie。
  • 类型名右边是数据类型,这里为结构体。
  • 在大括号内,使用名称和类型指定了一系列数据字段。请注意,此时没有给数据字段赋值。可将结构体视为模板。
  • 在 main 函数中,使用简短变量赋值声明并初始化了变量 m,给数据字段指定的值为相应的数据类型。
  • 使用点表示法访问数据字段并将其打印到控制台。

  要访问结构体的数据字段,可使用点表示法:结构体变量名、圆点和要访问的数据字段的名称。

 1.2 创建结构体

  声明结构体后,就可以通过多种方式创建它。假设你已经声明了一个结构体,那么就可直接声明这种类型的变量。

type Movie struct{
    Name string
    Rating float32
}

var m Movie

  注意:创建一个结构体实例后,各个数据字段的值为相应类型的零值。如果想要调试或查看结构体的零值,可使用 fmt 包将结构体的字段名和值打印出来。

fmt.Printf("%+v\n", m)  //打印结构体

  以这种方式创建结构体实例后,可使用点表示法给其字段赋值:

var m Movie

m.Name = "Metropolis"
m.Rating = 0.99

  结构体数据字段是可变的,这意味着可动态地修改它们。例如,你可以修改电影的名称。然而,一旦结构体被声明或者实例被创建,就不能修改其字段的数据类型了,否则会引发编译错误。

  程序清单:声明并创建一个结构体并将其赋给一个变量,然后再给这个结构体的数据字段赋值.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
 
import "fmt"
 
type Movie struct {
    Name   string
    Rating float32
}
 
func main() {
 
    var m Movie
 
    fmt.Printf("%+v\n", m)
 
    m.Name = "Metropolis"
    m.Rating = 0.9918
    fmt.Printf("%+v\n", m)
 
}

  • 关键字 var 声明变量 m。
  • 没有给字段赋值,所以他们默认为零值。对于字符串,零值为空字符串"",对于 float32,零值为 0。
  • 将字段的值打印到终端。
  • 使用点表示法给结构体的数据字段赋值。
  • 再次将结构体打印,以证明数据发字段的值发生了变化。

  也可使用关键字 new 来创建结构体实例。

m := new(Movie)
m.Name = "Metropolis"
m.Rating = 0.99

  程序清单:使用关键字 new 创建结构体实例.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
 
import (
    "fmt"
)
 
type Movie struct {
    Name   string
    Rating float32
}
 
func main() {
    m := new(Movie)
    m.Name = "Metropolis"
    m.Rating = 0.99
    fmt.Printf("%+v\n", m)
 
}

  还可使用简短变量赋值来创建结构体实例,此时可省略关键 new。创建结构体实例时,可同时给字段赋值,方法是使用字段名、冒号和字段值。

c := Movie{Name: "Citizen Kane", Rating: 10}

  也可省略字段名,按字段声明顺序来给它们赋值,但出于可维护性考虑,不推荐这么做。

c  := Movie{"Citizen Kane", 10}

  字段有很多时,让每个字段独占一行能够提高代码的可维护性和可读性。请注意,每行必须以逗号结尾。

c := Movie{
    Name: "Citizen Kane",
    Rating: 10,
}

  使用简短变量赋值是最常用的结构体创建方式,也是推荐的方式。

  程序清单:使用简短变量赋值创建结构体实例.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
 
import "fmt"
 
type Movie struct {
    Name   string
    Rating float32
}
 
func main() {
    m := Movie{
        Name:   "Metropolis",
        Rating: 0.99,
    }
    fmt.Printf("%+v\n", m)
 
}

1.3 嵌套结构体

  有时候,数据结构需要包含多个层级。此时,虽然可选择使用诸如切片等数据类型,但有时候需要的数据结构更复杂。为建立复杂的数据解雇,在一个结构体中嵌套另一个结构体的方式很有用。一个这样的例子是超级英雄列表:对于每个超级英雄,都需要存储其住址,而住址本身也是一个数据结构,非常适合使用结构体表示。

复制代码
type Superhero struct{
    Name    string
    Age       int
    Address  Address
}

type Address struct{
   Name int
   street string
   City    string
}
复制代码

  创建结构体 Superhero 的实例时,其中将包含一个数据字段为默认值的 Address 结构体。这可改善代码的灵活性和模块性,因为结构体 Address 也可用于其他地方。

  程序清单:使用简短变量赋值创建嵌套结构体实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main
 
import (
    "fmt"
)
 
type Superhero struct {
    Name    string
    Age     int
    Address Address
}
 
type Address struct {
    Number int
    Street string
    City   string
}
 
func main() {
    e := Superhero{
        Name: "Batman",
        Age:  32,
        Address: Address{
            Number: 1007,
            Street: "Mountain Drive",
            City:   "Gotham",
        },
    }
    fmt.Printf("%+v", e) // %+v 打印结构体时,会添加字段名; %v 打印相应值的默认格式
}

  要访问内嵌结构体的数据字段,可使用点表示法,这意味着使用结构体变量名、圆点、数据字段名、圆点和内嵌数据字段名,如下所示:

fmt.Println(e.Address.Street)

1.4 自定义结构体数据字段的默认值

  创建数据结构时,自定义数据字段的默认值是很有必要的。默认情况下,Go 给数据字段指定相应数据类型的零值。

Go 语言中的零值
类型 零值
布尔型(Boolean) false
整型(Integer) 0
浮点型(Float) 0.0
字符串(String) ""
指针(Pointer) nil
函数(Function) nil
接口(Interface) nil
切片(Slice) nil
通道(Channel) nil
映射(Map) nil

  创建结构体时,如果没有给其数据字段指定值,它们将为 Go 语言中对应类型的零值。Go 语言没有提供自定义默认值的内置方法,但可使用构造函数来实现这个目标。构造函数创建结构体,并将没有指定值的数据字段设置为默认值。 

复制代码
type Alarm struct{
   Time string
   Sound string
}

func NewAlarm(time string) Alarm{
    a := Alarm{
        Time: time,
        Sound: "Klaxon"
    }
    return a
}
复制代码

  这里不直接创建结构体 Alarm,而是使用函数 NewAlarm 来创建,从而让字段 Sound 包含自定义的默认值。请注意,这只是一种技巧,而并非 Go 语言规范的组成部分。如果你直接创建结构体 Alarm 的实例,且没有给 Sound 赋值,它将包含默认值 ""。

  通过使用构造函数来创建这种结构体实例时,字段 Sound 的默认值将为 Klaxon。请注意,可轻松地修改字段 Sound 的值,因此这种方法创建的是初始默认值,而不是常量值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
 
import (
    "fmt"
)
 
type Alarm struct{
    Time string
    Sound: bool
}
 
func NewAlarm(time string) Alarm {
    a := Alarm{
        Time:  time,
        Sound: "Klaxon",
    }
    return a
}
 
func main() {
    fmt.Printf("%+v\n", NewAlarm("07:00"))
}

1.5 比较结构体

  对结构体进行比较,要先看它们的类型是否相同。对于类型相同的结构体,可使用相等性运算符来比较。要判断两个结构体是否相等,可使用 ==;要判断它们是否不等,可使用 != 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main
 
import "fmt"
 
type Drink struct {
    Name string
    Ice  bool
}
 
func main() {
    a := Drink{
        Name: "Lemonade",
        Ice:  true,
    }
    b := Drink{
        Name: "Lemonade",
        Ice:  true,
    }
    if a == b {
        fmt.Println("a and b are the same")
    }
    fmt.Printf("%+v\n", a)
    fmt.Printf("%+v\n", a)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main
 
import "fmt"
 
type Drink struct {
    Name string
    Ice  bool
}
 
func main() {
    a := Drink{
        Name: "Lemonad",
        Ice:  true,
    }
    b := Drink{
        Name: "Lemonade",
        Ice:  true,
    }
    if a == b {
        fmt.Println("a and b are the same")
    }
    fmt.Printf("%+v\n", a)
    fmt.Printf("%+v\n", a)
}

  不能对两个类型不同的结构体进行比较,否则将导致编译错误。因此,试图比较两个结构体之前,必须确定它们的类型相同。要检查结构体的类型,可使用 Go 语言包 reflect。

  程序清单:使用 reflect 包检查结构体的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main
 
import (
    "fmt"
    "reflect"
)
 
type Drink struct {
    Name string
    Ice  bool
}
 
func main() {
    a := Drink{
        Name: "Lemonade",
        Ice:  true,
    }
    b := Drink{
        Name: "Lemonade",
        Ice:  true,
    }
    fmt.Println(reflect.TypeOf(a))
    fmt.Println(reflect.TypeOf(b))
}

1.6 理解公有和私有值

  如果一个值被导出,可在函数、方法或包外面使用,那么它就是公有的;如果一个值只能在其所属上下文中使用,那么它就是私有的。

  根据 Go 语言约定,结构体及其数据字段都可能被导出,也可能不导出。如果一个标识符的首字母是大写的,那么它将被导出;否则不会导出。

  要导出结构体及其字段,结构体及其字段的名称都必须以大写字母开头。

1.7 区分指针引用和值引用

  使用结构体时,明确指针引用和值引用的区别很重要。

  数据值存储在计算机内存中红。指针包含值的内存地址,这意味着使用指针可读写存储的值。创建结构体实例时,给数据字段分配内存并给它们指定默认值;然后返回指向内存的指针,并将其赋给一个变量。使用简短变量赋值时,将分配内存并指定默认值。

a := Drink{}

  赋值结构体时,明确内存方面的差别很重要。将指向结构体的变量赋值给另一个变量时,被称为赋值。

a := b

  赋值后,a 与 b 相同,但它是 b 的副本,而不是指向 b 的引用。修改 b 不会影响 a,反之亦然。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
 
import (
    "fmt"
)
 
type Drink struct {
    Name string
    Ice  bool
}
 
func main() {
    a := Drink{
        Name: "Lemonade",
        Ice:  true,
    }
    b := a
    b.Ice = false
    fmt.Printf("%+v\n", b)
    fmt.Printf("%+v\n", a)
    fmt.Printf("%p\n", &a)
    fmt.Printf("%p\n", &b)
}
  • 声明结构体类型 Drink。
  • 创建结构体 Drink 的一个实例,并将其赋给变量 a。
  • 修改 b 的数据字段 Ice。
  • 将 b 的值打印到终端。
  • 将 a 的值打印到终端,以证明修改 b 不会影响 a。
  • 使用 fmt.Printf 将 a 和 b 的内存地址打印到终端,以证明它们的内存地址不同。

  要修改原始结构体实例包含的值,必须使用指针。指针是指向内存地址的引用。因此使用它操作的不是结构体的副本而是其本身。要获得指针,可在变量前加上和号。、

  程序清单:以指针引用的方式赋值结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main
 
import (
    "fmt"
)
 
type Drink struct {
    Name string
    Ice  bool
}
 
func main() {
    a := Drink{
        Name: "Lemonade",
        Ice:  true,
    }
    b := &a
    b.Ice = false
 
    fmt.Printf("%+v\n", b)
    fmt.Printf("%+v\n", a)
    fmt.Printf("%p\n", b)
    fmt.Printf("%p\n", &a)
}
  • 将指向 a 的指针(而不是 a 本身)赋给 b,这是使用 & 字符表示的。
  • 修改 b 时,将修改分配给 a 的内存,因为 a 和 b 指向相同的内存。
  • 打印 a 和 b 的值时,将发现它们的值相同。请注意,由于 b 是指针,因此必须使用星号符对其进行引用。
  • 将 b 和 a 的内存地址输出,以证明它们相同。

  指针和值的差别很微妙,但选择使用指针还是值很容易区分;如果需要修改原始结构体实例,就使用指针;如果要操作一个结构体,但不想修改。

posted @   左扬  阅读(348)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
levels of contents
点击右上角即可分享
微信分享提示