Golang学习-CH6 Go语言结构体

Go 语言通过用自定义的方式形成新的类型,结构体是类型中带有成员的复合类型

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

Go语言中结构体与其他语言中类的概念对比:

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

6.1 Go语言结构体定义

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

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

使用关键字 type 可以将各种基本类型定义为自定义类型。结构体是一种复合的基本类型,通过 type 定义为自定义类型后,使结构体更便于使用。

type Point struct {
    X int
    Y int
}
//同类型变量写在同一行
type Color struct {
    R, G, B byte
}

结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存。

6.2 Go语言实例化结构体

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

基本的实例化形式

结构体本身是一种类型,以 var 的方式声明结构体即可完成实例化。

type Point struct {
    X int
    Y int
}
var p Point
p.X = 10
p.Y = 20
  • 使用.来访问结构体的成员变量,如p.Xp.Y等,结构体成员变量的赋值方法与普通变量一致。

new创建指针类型的结构体

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

type Player struct{
    Name string
    HealthPoint int
    MagicPoint int
}
tank := new(Player)
tank.Name = "Canon"
tank.HealthPoint = 300
  • 实际上tank为指针类型,但是在Go语言中仍然可以使用.来直接访问结构体指针的成员
  • 因为Go语言为了方便开发者访问结构体指针的成员变量,使用了语法糖(Syntactic sugar)技术,将 ins.Name 形式转换为 (*ins).Name

取结构体的地址实例化

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

ins := &T{} //ins 为结构体的实例,类型为 *T,是指针类型。
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,
    }
}

6.3 初始化结构体的成员变量

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

键值对形式的初始化适合选择性填充字段较多的结构体,多个值的列表形式适合填充字段较少的结构体。

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

键值对的填充是可选的,不需要初始化的字段可以不填入初始化列表中。字段的默认值是字段类型的默认值。

格式如下:

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

示例如下:

type People struct {
    name  string
    child *People
}
relation := &People{
    name: "爷爷",
    child: &People{
        name: "爸爸",
        child: &People{
                name: "我",
        },
    },
}

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

类似C++的情况,不知道内存分布时无法声明实体。

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

Go语言可以在“键值对”初始化的基础上忽略“键”,但是:

  • 必须初始化结构体的所有字段。
  • 每一个初始值的填充顺序必须与字段在结构体中的声明顺序一致。
  • 键值对与值列表的初始化形式不能混用。

格式如下:

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

示例如下:

type Address struct {
    Province    string
    City        string
    ZipCode     int
    PhoneNumber string
}
addr := Address{
    "四川",
    "成都",
    610000,
    "0",
}
fmt.Println(addr)

初始化匿名结构体

匿名结构体没有类型名称,无须通过 type 关键字定义就可以直接使用。

// 打印消息类型, 传入匿名结构体
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)
}

匿名结构体的类型名是结构体包含字段成员的详细描述,匿名结构体在使用时需要重新定义,造成大量重复的代码,因此开发中较少使用。

6.4 Go语言构造函数

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

其他编程语言构造函数的一些常见功能及特性如下:

  • 每个类可以添加构造函数,多个构造函数使用函数重载实现。Go语言没有重载
  • 构造函数一般与类名同名,且没有返回值。
  • 构造函数有一个静态构造函数,一般用这个特性来调用父类的构造函数。
  • 对于 C++ 来说,还有默认构造函数、拷贝构造函数等。

模拟构造函数重载

type Cat struct {
    Color string
    Name  string
}
func NewCatByName(name string) *Cat {
    return &Cat{
        Name: name,
    }
}
func NewCatByColor(color string) *Cat {
    return &Cat{
        Color: color,
    }
}

模拟父子构造函数调用

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语言中没有提供构造函数相关的特殊机制,用户根据自己的需求,将参数使用函数传递到结构体构造参数中即可完成构造函数的任务。

6.5 Go语言方法和接收器

  • 在Go语言中有一个概念,它和方法有着同样的名字,并且大体上意思相同,Go 方法是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量,因此方法是一种特殊类型的函数。
  • 接收器类型可以是(几乎)任何类型,不仅仅是结构体类型,任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型,但是接收器不能是一个接口类型,因为接口是一个抽象定义,而方法却是具体实现,如果这样做了就会引发编译错误。
  • 一个类型加上它的方法等价于面向对象中的一个类,一个重要的区别是,在Go语言中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在不同的源文件中,唯一的要求是它们必须是同一个包的。
  • 类型 T(或 T)上的所有方法的集合叫做类型 T(或 T)的方法集。
  • 因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法,但是如果基于接收器类型,是有重载的:具有同样名字的方法可以在 2 个或多个不同的接收器类型上存在,比如在同一个包里这么做是允许的。

为结构体添加方法

type Bag struct {
    items []int
}
func (b *Bag) Insert(itemid int) {
    b.items = append(b.items, itemid)
}
func main() {
    b := new(Bag)
    b.Insert(1001)
}

接收器说明

  • 接收器变量:接收器中的参数变量名在命名时,官方建议使用接收器类型名的第一个小写字母,而不是 self、this 之类的命名。例如,Socket 类型的接收器变量应该命名为 s,Connector 类型的接收器变量应该命名为 c 等。
  • 接收器类型:接收器类型和参数类似,可以是指针类型和非指针类型。两种接收器在使用时会产生不同的效果,根据效果的不同,两种接收器会被用于不同性能和功能要求的代码中。

指针类型的接收器:

  • 指针类型的接收器由一个结构体的指针组成,更接近于面向对象中的 this 或者 self。由于指针的特性,调用方法时,修改接收器指针的任意成员变量,在方法结束后,修改都是有效的。

非指针类型的接收器:

当方法作用于非指针接收器时,Go语言会在代码运行时将接收器的值复制一份,在非指针接收器的方法中可以获取接收器的成员值,但修改后无效。

// 定义点结构
type Point struct {
    X int
    Y int
}
// 非指针接收器的加方法
func (p Point) Add(other Point) Point {
    // 成员值与参数相加后返回新的结构
    return Point{p.X + other.X, p.Y + other.Y}
}
func main() {
    // 初始化点
    p1 := Point{1, 1}
    p2 := Point{2, 2}
    // 与另外一个点相加
    result := p1.Add(p2)
    // 输出结果
    fmt.Println(result)
}

在计算机中,小对象由于值复制时的速度较快,所以适合使用非指针接收器,大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制,只是传递指针。

6.6 Go语言为任意类型添加方法

Go语言可以对任何类型添加方法,结构体也是一种类型。

  • 在Go语言中,使用 type 关键字可以定义出新的自定义类型,之后就可以为自定义类型添加各种方法了。
// 将int定义为MyInt类型
type MyInt int
// 为MyInt添加IsZero()方法
func (m MyInt) IsZero() bool {
    return m == 0
}
// 为MyInt添加Add()方法
func (m MyInt) Add(other int) int {
    return other + int(m)
}
func main() {
    var b MyInt
    fmt.Println(b.IsZero())
    b = 1
    fmt.Println(b.Add(2))
}
  • 但是不能直接基于本地类型定义方法,编译错误

6.7 Go语言使用事件系统实现事件的响应和处理

Go语言可以将类型的方法与普通函数视为一个概念,从而简化方法和函数混合作为回调类型时的复杂性。这个特性和 C# 中的代理(delegate)类似调用者无须关心谁来支持调用,系统会自动处理是否调用普通函数或类型的方法。

方法和函数的统一调用

// 声明一个结构体
type class struct {
}
// 给结构体添加Do方法
func (c *class) Do(v int) {
    fmt.Println("1 call method do:", v)
}
// 普通函数的Do
func funcDo(v int) {
    fmt.Println("2 call function do:", v)
}
func main() {
    // 声明一个函数回调
    var delegate func(int)
    // 创建结构体实例
    c := new(class)
    // 将回调设为c的Do方法
    delegate = c.Do
    // 调用
    delegate(100)
    // 将回调设为普通函数
    delegate = funcDo
    // 调用
    delegate(100)
}

无论是普通函数还是结构体的方法,只要它们的签名一致,与它们签名一致的函数变量就可以保存普通函数或是结构体方法。实现了接口统一。

签名一致:函数类型一致

事件系统基本原理*

事件系统可以将事件派发者与事件处理者解耦。

例如,网络底层可以生成各种事件,在网络连接上后,网络底层只需将事件派发出去,而不需要关心到底哪些代码来响应连接上的逻辑。或者再比如,你注册、关注或者订阅某“大V”的社交消息后,“大V”发生的任何事件都会通知你,但他并不用了解粉丝们是如何为她喝彩或者疯狂的。

一个事件系统拥有如下特性:

  • 能够实现事件的一方,可以根据事件 ID 或名字注册对应的事件。
  • 事件发起者,会根据注册信息通知这些注册者。
  • 一个事件可以有多个实现方响应。

6.8 Go语言类型内嵌和结构体内嵌

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

匿名字段本身可以是一个结构体类型,即结构体可以包含内嵌结构体。

Go语言中的继承是通过内嵌或组合来实现的,所以可以说,在Go语言中,相比较于继承,组合更受青睐。

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)
}

在一个结构体中对于每一种数据类型只能有一个匿名字段。

内嵌结构体

Go语言的结构体内嵌有如下特性:

  • 内嵌的结构体可以直接访问其成员变量
  • 编译器在发现可能的赋值歧义时会报错。

6.9 Go语言结构体内嵌模拟类的继承

Go语言的结构体内嵌特性就是一种组合特性,使用组合特性可以快速构建对象的不同特性。

package main
import "fmt"
// 可飞行的
type Flying struct{}
func (f *Flying) Fly() {
    fmt.Println("can fly")
}
// 可行走的
type Walkable struct{}
func (f *Walkable) Walk() {
    fmt.Println("can calk")
}
// 人类
type Human struct {
    Walkable // 人类能行走
}
// 鸟类
type Bird struct {
    Walkable // 鸟类能行走
    Flying   // 鸟类能飞行
}
func main() {
    // 实例化鸟类
    b := new(Bird)
    fmt.Println("Bird: ")
    b.Fly()
    b.Walk()
    // 实例化人类
    h := new(Human)
    fmt.Println("Human: ")
    h.Walk()
}

使用Go语言的内嵌结构体实现对象特性,可以自由地在对象中增、删、改各种特性。Go语言会在编译时检查能否使用这些特性。

6.10 Go语言初始化内嵌结构体

// 车轮
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)
}

6.11 Go语言垃圾回收和SetFinalizer

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

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

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

在Go语言中 SetFinalizer 函数是这样定义的:

func SetFinalizer(x, f interface{})

参数说明如下:

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

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

另外,x 的终止器会在 x 不能直接或间接访问后的任意时间被调用执行,不保证终止器会在程序退出前执行,因此一般终止器只用于在长期运行的程序中释放关联到某对象的非内存资源。

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

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

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

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

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()
    }
}

6.12 Go语言结构体部分相关应用示例*

JSON数据处理

链表操作

I/O对象操作

posted @ 2021-07-13 19:39  tlamm  阅读(209)  评论(0)    收藏  举报