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 的内存地址输出,以证明它们相同。

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

原文地址:https://www.cnblogs.com/zuoyang/p/15156189.html