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


    使用结构体和指针

    本章节介绍如下内容

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

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

    1.1 结构体是什么?

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

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

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

    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
    ", m)  //打印结构体

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

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

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

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

    package main
    
    import "fmt"
    
    type Movie struct {
    	Name   string
    	Rating float32
    }
    
    func main() {
    
    	var m Movie
    
    	fmt.Printf("%+v
    ", m)
    
    	m.Name = "Metropolis"
    	m.Rating = 0.9918
    	fmt.Printf("%+v
    ", m)
    
    }
    

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

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

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

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

    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
    ", m)
    
    }
    

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

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

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

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

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

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

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

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

    package main
    
    import "fmt"
    
    type Movie struct {
    	Name   string
    	Rating float32
    }
    
    func main() {
    	m := Movie{
    		Name:   "Metropolis",
    		Rating: 0.99,
    	}
    	fmt.Printf("%+v
    ", m)
    
    }
    

    1.3 嵌套结构体

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

    type Superhero struct{
        Name    string
        Age       int
        Address  Address
    }
    
    type Address struct{
       Name int
       street string
       City    string
    }

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

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

    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 的值,因此这种方法创建的是初始默认值,而不是常量值。

    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
    ", NewAlarm("07:00"))
    }
    

    1.5 比较结构体

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

    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
    ", a)
    	fmt.Printf("%+v
    ", a)
    }
    

    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
    ", a)
    	fmt.Printf("%+v
    ", a)
    }
    

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

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

    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,反之亦然。

    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
    ", b)
    	fmt.Printf("%+v
    ", a)
    	fmt.Printf("%p
    ", &a)
    	fmt.Printf("%p
    ", &b)
    }
    • 声明结构体类型 Drink。
    • 创建结构体 Drink 的一个实例,并将其赋给变量 a。
    • 修改 b 的数据字段 Ice。
    • 将 b 的值打印到终端。
    • 将 a 的值打印到终端,以证明修改 b 不会影响 a。
    • 使用 fmt.Printf 将 a 和 b 的内存地址打印到终端,以证明它们的内存地址不同。

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

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

    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
    ", b)
    	fmt.Printf("%+v
    ", a)
    	fmt.Printf("%p
    ", b)
    	fmt.Printf("%p
    ", &a)
    }
    
    • 将指向 a 的指针(而不是 a 本身)赋给 b,这是使用 & 字符表示的。
    • 修改 b 时,将修改分配给 a 的内存,因为 a 和 b 指向相同的内存。
    • 打印 a 和 b 的值时,将发现它们的值相同。请注意,由于 b 是指针,因此必须使用星号符对其进行引用。
    • 将 b 和 a 的内存地址输出,以证明它们相同。

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

  • 相关阅读:
    [cf 599A]Patrick and Shopping
    [APIO2014] [Uoj103] [Bzoj3676] Palindromes回文串 [Manacher,后缀数组]
    [Hdu3068]最长回文[Manacher]
    [hdu2222] [AC自动机模板] Keywords Search [AC自动机]
    [Bzoj3940] [AC自动机,USACO 2015 February Gold] Censor [AC自动机模板题]
    [Poj3261] [Bzoj1717] [后缀数组论文例题,USACO 2006 December Gold] Milk Patterns [后缀数组可重叠的k次最长重复子串]
    [Poj1743] [后缀数组论文例题] Musical Theme [后缀数组不可重叠最长重复子串]
    [UOJ#35] [UOJ后缀数组模板题] 后缀排序 [后缀数组模板]
    [Bzoj4196] [NOI2015] 软件包管理器 [树链剖分,线段树]
    [Bzoj4195] [NOI2015] 程序自动分析 [并查集,哈希,map] 题解
  • 原文地址:https://www.cnblogs.com/zuoyang/p/15156189.html
Copyright © 2020-2023  润新知