Go 基础 —— (六)结构体

Go基础 —— 目录

(一)Go 起步
(二)变量
(三)运算符、流程控制
(四)函数
(五)容器
(六)结构体



前言

Go 语言通过用自定义的方式形成新的类型,结构体是类型中带有成员的复合类型。Go 语言使用结构体和结构体成员来描述真实世界的实体和实体对应的各种属性。

Go 语言中的类型可以被实例化,使用new或&构造的类型实例的类型是类型的指针。

结构体成员是由一系列的成员变量构成,这些成员变量也被称为“字段”。字段有以下特性:

  • 字段拥有自己的类型和值。
  • 字段名必须唯一。
  • 字段的类型也可以是结构体,甚至是字段所在结构体的类型。

关于 Go 语言的类(class)

  • Go 语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。
  • Go 语言的结构体与“类”都是复合结构体,但 Go 语言中结构体的内嵌配合接口比面向对象具有更高的扩展性和灵活性。
  • Go 语言不仅认为结构体能拥有方法,且每种自定义类型也可以拥有自己的方法。

一、结构体定义

使用关键字type可以将各种基本类型定义为自定义类型,基本类型包括整型、字符串、布尔等。结构体是一种复合的基本类型,通过type定义为自定义类型后,使结构体更便于使用。

type 类型名 struct {
    字段1 字段1类型
    字段2 字段2类型
    …
}
// 使用结构体可以表示一个包含 X 和 Y 整型分量的点结构,代码如下:
type Point struct {
    X int
    Y int
}
// 同类型的变量也可以写在一行,颜色的红、绿、蓝 3 个分量可以使用 byte 类型表示,定义的颜色结构体如下:
type Color struct {
    R, G, B byte
}

二、实例化结构体

结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存,因此必须在定义结构体并实例化后才能使用结构体的字段。

实例化就是根据结构体定义的格式创建一份与格式一致的内存区域,结构体实例与实例间的内存是完全独立的。

Go语言可以通过多种方式实例化结构体,根据实际需要可以选用不同的写法。

2.1 基本的实例化形式

var ins T
// T 为结构体类型
// ins 为结构体的实例
type Point struct {
    X int
    Y int
}
var p Point
p.X = 10
p.Y = 20

2.2 创建指针类型的结构体

Go语言中,还可以使用new关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体。

ins := new(T)
// T 为类型,可以是结构体、整型、字符串等
// ins T类型被实例化后保存到 ins 变量中,ins 的类型为 *T,属于指针

Go语言可以像访问普通结构体一样使用.来访问结构体指针的成员。

type Player struct{
    Name string
    HealthPoint int
    MagicPoint int
}
tank := new(Player)
tank.Name = "Canon"
tank.HealthPoint = 300

经过 new 实例化的结构体实例在成员赋值上与基本实例化的写法一致。

Go语言和 C/C++
在 C/C++ 语言中,使用 new 实例化类型后,访问其成员变量时必须使用->操作符。

在Go语言中,访问结构体指针的成员变量时可以继续使用.,这是因为Go语言为了方便开发者访问结构体指针的成员变量,使用了语法糖(Syntactic sugar)技术,将ins.Name形式转换为(*ins).Name

2.3 取结构体的地址实例化

在Go语言中,对结构体进行&取地址操作时,视为对该类型进行一次new的实例化操作,取地址格式如下:

ins := &T{}
// T 结构体类型
// ins 为结构体的实例,类型为 *T,是指针类型
// 使用结构体定义一个命令行指令(Command),指令中包含名称、变量关联和注释等,对 Command 进行指针地址的实例化,并完成赋值过程
type Command struct {
    Name    string    // 指令名称
    Var     *int      // 指令绑定的变量
    Comment string    // 指令的注释
}
var version int = 1
cmd := &Command{}
cmd.Name = "version"
cmd.Var = &version
cmd.Comment = "show version"
// 取地址实例化是最广泛的一种结构体实例化方式,可以使用函数封装上面的初始化过程
func newCommand(name string, varref *int, comment string) *Command {
    return &Command{
        Name:    name,
        Var:     varref,
        Comment: comment,
    }
}
cmd = newCommand(
    "version",
    &version,
    "show version",
)

三、初始化结构体的成员变量

结构体在实例化时可以直接对成员变量进行初始化,初始化有两种形式分别是以字段“键值对”形式和多个值的列表形式,键值对形式的初始化适合选择性填充字段较多的结构体,多个值的列表形式适合填充字段较少的结构体。

3.1 使用“键值对”初始化结构体

ins := 结构体类型名{
    字段1: 字段1的值,
    字段2: 字段2的值,}
type People struct {
    name  string
    child *People
}
relation := &People{
    name: "爷爷",
    child: &People{
        name: "爸爸",
        child: &People{
                name: "我",
        },
    },
}

注意:结构体成员中只能包含结构体的指针类型,包含非指针类型会引起编译错误。

3.2 使用多个值的列表初始化结构体

Go语言可以在“键值对”初始化的基础上忽略“键”,也就是说,可以使用多个值的列表初始化结构体的字段。

ins := 结构体类型名{
    字段1的值,
    字段2的值,}

使用这种格式初始化时,需要注意:

  • 必须初始化结构体的所有字段。
  • 每一个初始值的填充顺序必须与字段在结构体中的声明顺序一致。
  • 键值对与值列表的初始化形式不能混用。
type Address struct {
    Province    string
    City        string
    ZipCode     int
    PhoneNumber string
}
addr := Address{
    "四川",
    "成都",
    610000,
    "0",
}
fmt.Println(addr)

3.3 初始化匿名结构体

ins := struct {
    // 匿名结构体字段定义
    字段1 字段类型1
    字段2 字段类型2}{
    // 字段值初始化
    初始化字段1: 字段1的值,
    初始化字段2: 字段2的值,}
package main
import (
    "fmt"
)
// 打印消息类型, 传入匿名结构体
func printMsgType(msg *struct {
    id   int
    data string
}) {
    // 使用动词%T打印msg的类型
    fmt.Printf("%T\n", msg)
}
func main() {
    // 实例化一个匿名结构体
    msg := &struct {  // 定义部分
        id   int
        data string
    }{  // 值初始化部分
        1024,
        "hello",
    }
    printMsgType(msg)
}

四、“构造函数”

Go语言的类型或结构体没有构造函数的功能,但是我们可以使用结构体初始化的过程来模拟实现构造函数。

4.1 多种方式创建和初始化结构体(模拟构造函数重载)

type Cat struct {
    Color string
    Name  string
}
// 定义用名字构造猫结构的函数,返回 Cat 指针
func NewCatByName(name string) *Cat {
	// 取地址实例化猫的结构体
    return &Cat{
    	// 初始化猫的名字字段,忽略颜色字段
        Name: name,
    }
}
// 定义用颜色构造猫结构的函数,返回 Cat 指针
func NewCatByColor(color string) *Cat {
    return &Cat{
        Color: color,
    }
}

4.2 带有父子关系的结构体的构造和初始化(模拟父级构造调用)

type Cat struct {
    Color string
    Name  string
}
type BlackCat struct {
    Cat  // 嵌入Cat, 类似于派生
}
// “构造基类”
func NewCat(name string) *Cat {
    return &Cat{
        Name: name,
    }
}
// “构造子类”
func NewBlackCat(color string) *BlackCat {
    cat := &BlackCat{}
    cat.Color = color
    return cat
}

五、类型内嵌和结构体内嵌

结构体可以包含一个或多个匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型也就是字段的名字。匿名字段本身可以是一个结构体类型,即结构体可以包含内嵌结构体。

可以粗略地将这个和面向对象语言中的继承概念相比较,随后将会看到它被用来模拟类似继承的行为。Go语言中的继承是通过内嵌或组合来实现的,所以可以说,在Go语言中,相比较于继承,组合更受青睐。

package main
import "fmt"
type innerS struct {
    in1 int
    in2 int
}
type outerS struct {
    b int
    c float32
    int // anonymous field
    innerS //anonymous field
}
func main() {
    outer := new(outerS)
    outer.b = 6
    outer.c = 7.5
    outer.int = 60
    outer.in1 = 5
    outer.in2 = 10
    fmt.Printf("outer.b is: %d\n", outer.b)
    fmt.Printf("outer.c is: %f\n", outer.c)
    fmt.Printf("outer.int is: %d\n", outer.int)
    fmt.Printf("outer.in1 is: %d\n", outer.in1)
    fmt.Printf("outer.in2 is: %d\n", outer.in2)
    // 使用结构体字面量
    outer2 := outerS{6, 7.5, 60, innerS{5, 10}}
    fmt.Printf("outer2 is:", outer2)
}

5.1 内嵌结构体

package main
import "fmt"
type A struct {
    ax, ay int
}
type B struct {
    A
    bx, by float32
}
func main() {
    b := B{A{1, 2}, 3.0, 4.0}
    fmt.Println(b.ax, b.ay, b.bx, b.by)
    fmt.Println(b.A)
}

5.2 结构体内嵌特性

  • 内嵌结构体可以直接访问其成员变量
  • 内嵌结构体的字段名是它的类型名

六、初始化内嵌结构体

package main
import "fmt"
// 车轮
type Wheel struct {
    Size int
}
// 引擎
type Engine struct {
    Power int    // 功率
    Type  string // 类型
}
// 车
type Car struct {
    Wheel
    Engine
}
func main() {
    c := Car{
        // 初始化轮子
        Wheel: Wheel{
            Size: 18,
        },
        // 初始化引擎
        Engine: Engine{
            Type:  "1.4T",
            Power: 143,
        },
    }
    fmt.Printf("%+v\n", c)
}
// 初始化内嵌匿名结构体
package main
import "fmt"
// 车轮
type Wheel struct {
    Size int
}
// 车
type Car struct {
    Wheel
    // 引擎
    Engine struct {
        Power int    // 功率
        Type  string // 类型
    }
}
func main() {
    c := Car{
        // 初始化轮子
        Wheel: Wheel{
            Size: 18,
        },
        // 初始化引擎
        Engine: struct {
            Power int
            Type  string
        }{
            Type:  "1.4T",
            Power: 143,
        },
    }
    fmt.Printf("%+v\n", c)
}

七、内嵌结构体成员名字冲突

package main
import (
    "fmt"
)
type A struct {
    a int
}
type B struct {
    a int
}
type C struct {
    A
    B
}
func main() {
    c := &C{}
    c.A.a = 1
    fmt.Println(c)
}

八、垃圾回收和SetFinalizer

Go语言自带垃圾回收机制(GC)。GC 通过独立的进程执行,它会搜索不再使用的变量,并将其释放。需要注意的是,GC 在运行时会占用机器资源。

GC 是自动进行的,如果要手动进行 GC,可以使用runtime.GC()函数,显式的执行 GC。显式的进行 GC 只在某些特殊的情况下才有用,比如当内存资源不足时调用runtime.GC() ,这样会立即释放一大片内存,但是会造成程序短时间的性能下降。

finalizer(终止器)是与对象关联的一个函数,通过runtime.SetFinalizer来设置,如果某个对象定义了 finalizer,当它被 GC 时候,这个finalizer就会被调用,以完成一些特定的任务,例如发信号或者写日志等。

func SetFinalizer(x, f interface{})
// 参数 x 必须是一个指向通过 new 申请的对象的指针,或者通过对复合字面值取址得到的指针。
// 参数 f 必须是一个函数,它接受单个可以直接用 x 类型值赋值的参数,也可以有任意个被忽略的返回值。

SetFinalizer函数可以将 x 的终止器设置为f,当垃圾收集器发现x不能再直接或间接访问时,它会清理x并调用f(x)

x的终止器会在 x 不能直接或间接访问后的任意时间被调用执行,不保证终止器会在程序退出前执行,因此一般终止器只用于在长期运行的程序中释放关联到某对象的非内存资源。例如,当一个程序丢弃一个os.File对象时没有调用其Close方法,该os.File对象可以使用终止器去关闭对应的操作系统文件描述符。

终止器会按依赖顺序执行:如果 A 指向 B,两者都有终止器,且 A 和 B 没有其它关联,那么只有 A 的终止器执行完成,并且 A 被释放后,B 的终止器才可以执行。

如果*x的大小为0字节,也不保证终止器会执行。

此外,我们也可以使用SetFinalizer(x, nil)来清理绑定到x上的终止器。

注意:终止器只有在对象被 GC 时,才会被执行。其他情况下,都不会被执行,即使程序正常结束或者发生错误。

// 在函数 entry() 中定义局部变量并设置 finalizer,当函数 entry() 执行完成后,在 main 函数中手动触发 GC,查看 finalizer 的执行情况。
package main
import (
    "log"
    "runtime"
    "time"
)
type Road int
func findRoad(r *Road) {
    log.Println("road:", *r)
}
func entry() {
    var rd Road = Road(999)
    r := &rd
    runtime.SetFinalizer(r, findRoad)
}
func main() {
    entry()
    for i := 0; i < 10; i++ {
        time.Sleep(time.Second)
        runtime.GC()
    }
}

九、链表操作

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。

使用链表结构可以避免在使用数组时需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

链表允许插入和移除表上任意位置上的结点,但是不允许随机存取。链表有三种类型:单向链表、双向链表以及循环链表。

9.1 单向链表

单向链表中每个结点包含两部分,分别是数据域和指针域,上一个结点的指针指向下一结点,依次相连,形成链表。

  • 首元结点:就是链表中存储第一个元素的结点,如下图中 a1 的位置。
  • 头结点:它是在首元结点之前附设的一个结点,其指针域指向首元结点。头结点的数据域可以存储链表的长度或者其它的信息,也可以为空不存储任何信息。
  • 头指针:它是指向链表中第一个结点的指针。若链表中有头结点,则头指针指向头结点;若链表中没有头结点,则头指针指向首元结点。

头结点在链表中不是必须的,但增加头结点有以下几点好处:

  • 增加了头结点后,首元结点的地址保存在头结点的指针域中,对链表的第一个数据元素的操作与其他数据元素相同,无需进行特殊处理。
  • 增加头结点后,无论链表是否为空,头指针都是指向头结点的非空指针,若链表为空的话,那么头结点的指针域为空。

9.2 使用struct定义单链表

type Node struct {
    Data  int
    Next  *node
}
// Data 用来存放结点中的有用数据,Next 是指针类型的成员,它指向 Node struct 类型数据,也就是下一个结点的数据类型
package main
import "fmt"
type Node struct {
    data int
    next *Node
}
func Shownode(p *Node) { //遍历
    for p != nil {
        fmt.Println(*p)
        p = p.next //移动指针
    }
}
func main() {
    var head = new(Node)
    head.data = 1
    var node1 = new(Node)
    node1.data = 2
    head.next = node1
    var node2 = new(Node)
    node2.data = 3
    node1.next = node2
    Shownode(head)
}

9.3 插入结点

9.3.1 头插法

package main
import "fmt"
type Node struct {
    data  int
    next  *Node
}
func Shownode(p *Node){   //遍历
    for p != nil{
        fmt.Println(*p)
        p=p.next  //移动指针
    }
}
func main() {
    var head = new(Node)
    head.data = 0
    var tail *Node
    tail = head   //tail用于记录头结点的地址,刚开始tail的的指针指向头结点
    for i :=1 ;i<10;i++{
        var node = Node{data:i}
        node.next = tail   //将新插入的node的next指向头结点
        tail = &node      //重新赋值头结点
    }
    Shownode(tail) //遍历结果
}

9.3.2 尾插发

package main
import "fmt"
type Node struct {
    data  int
    next  *Node
}
func Shownode(p *Node){   //遍历
    for p != nil{
        fmt.Println(*p)
        p=p.next  //移动指针
    }
}
func main() {
    var head = new(Node)
    head.data = 0
    var tail *Node
    tail = head   //tail用于记录最末尾的结点的地址,刚开始tail的的指针指向头结点
    for i :=1 ;i<10;i++{
        var node = Node{data:i}
        (*tail).next = &node
        tail = &node
    }
    Shownode(head) //遍历结果
}

在进行数组的插入、删除操作时,为了保持内存数据的连续性,需要做大量的数据搬移,所以速度较慢。而在链表中插入或者删除一个数据,我们并不需要为了保持内存的连续性而搬移结点,因为链表的存储空间本身就不是连续的。所以,在链表中插入和删除一个数据是非常快速的。

但是,有利就有弊。链表要想随机访问第 k 个元素,就没有数组那么高效了。因为链表中的数据并非连续存储的,所以无法像数组那样,根据首地址和下标,通过寻址公式就能直接计算出对应的内存地址,而是需要根据指针一个结点一个结点地依次遍历,直到找到相应的结点。

9.4 循环链表

和单链表相比,循环链表的优点是从链尾到链头比较方便。当要处理的数据具有环型结构特点时,就特别适合采用循环链表。比如著名的约瑟夫问题,尽管用单链表也可以实现,但是用循环链表实现的话,代码就会简洁很多。

9.5 双向链表

双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值