zoukankan      html  css  js  c++  java
  • Go基础系列:struct和嵌套struct

    struct

    struct定义结构,结构由字段(field)组成,每个field都有所属数据类型,在一个struct中,每个字段名都必须唯一。

    说白了就是拿来存储数据的,只不过可自定义化的程度很高,用法很灵活,Go中不少功能依赖于结构,就这样一个角色。

    Go中不支持面向对象,面向对象中描述事物的类的重担由struct来挑。比如面向对象中的继承,可以使用组合(composite)来实现:struct中嵌套一个(或多个)类型。面向对象中父类与子类、类与对象的关系是is a的关系,例如Horse is a Animal,Go中的组合则是外部struct与内部struct的关系、struct实例与struct的关系,它们是has a的关系。Go中通过struct的composite,可以"模仿"很多面向对象中的行为,它们很"像"。

    定义struct

    定义struct的格式如下:

    type identifier struct {
        field1 type1
        field2 type2
        …
    }
    // 或者
    type T struct { a, b int }
    

    理论上,每个字段都是有具有唯一性的名字的,但如果确定某个字段不会被使用,可以将其名称定义为空标识符_来丢弃掉:

    type T struct {
        _ string
        a int
    }
    

    每个字段都有类型,可以是任意类型,包括内置简单数据类型、其它自定义的struct类型、当前struct类型本身、接口、函数、channel等等。

    如果某几个字段类型相同,可以缩写在同一行:

    type mytype struct {
        a,b int
        c string
    }
    

    构造struct实例

    定义了struct,就表示定义了一个数据结构,或者说数据类型,也或者说定义了一个类。总而言之,定义了struct,就具备了成员属性,就可以作为一个抽象的模板,可以根据这个抽象模板生成具体的实例,也就是所谓的"对象"。

    例如:

    type person struct{
        name string
        age int
    }
    
    // 初始化一个person实例
    var p person
    

    这里的p就是一个具体的person实例,它根据抽象的模板person构造而出,具有具体的属性name和age的值,虽然初始化时它的各个字段都是0值。换句话说,p是一个具体的人。

    struct初始化时,会做默认的赋0初始化,会给它的每个字段根据它们的数据类型赋予对应的0值。例如int类型是数值0,string类型是"",引用类型是nil等。

    因为p已经是初始化person之后的实例了,它已经具备了实实在在存在的属性(即字段),所以可以直接访问它的各个属性。这里通过访问属性的方式p.FIELD为各个字段进行赋值。

    // 为person实例的属性赋值,定义具体的person
    p.name = "longshuai"
    p.age = 23
    

    获取某个属性的值:

    fmt.Println(p.name) // 输出"longshuai"
    

    也可以直接赋值定义struct的属性来生成struct的实例,它会根据值推断出p的类型。

    var p = person{name:"longshuai",age:23}
    
    p := person{name:"longshuai",age:23}
    
    // 不给定名称赋值,必须按字段顺序
    p := person{"longshuai",23}
    
    p := person{age:23}
    p.name = "longshuai"
    

    如果struct的属性分行赋值,则必须不能省略每个字段后面的逗号",",否则就会报错。这为未来移除、添加属性都带来方便:

    p := person{
    	name:"longshuai",
    	age:23,     // 这个逗号不能省略
    }
    

    除此之外,还可以使用new()函数或&TYPE{}的方式来构造struct实例,它会为struct分配内存,为各个字段做好默认的赋0初始化。它们是等价的,都返回数据对象的指针给变量,实际上&TYPE{}的底层会调用new()。

    p := new(person)
    p := &person{}
    
    // 生成对象后,为属性赋值
    p.name = "longshuai"
    p.age = 23
    

    使用&TYPE{}的方式也可以初始化赋值,但new()不行:

    p := &person{
        name:"longshuai",
        age:23,
    }
    

    选择new()还是选择&TYPE{}的方式构造实例?完全随意,它们是等价的。但如果想要初始化时就赋值,可以考虑使用&TYPE{}的方式。

    struct的值和指针

    下面三种方式都可以构造person struct的实例p:

    p1 := person{}
    p2 := &person{}
    p3 := new(person)
    

    但p1和p2、p3是不一样的,输出一下就知道了:

    package main
    
    import (
    	"fmt"
    )
    
    type person struct {
    	name string
    	age  int
    }
    
    func main() {
    	p1 := person{}
    	p2 := &person{}
    	p3 := new(person)
    	fmt.Println(p1)
    	fmt.Println(p2)
    	fmt.Println(p3)
    }
    

    结果:

    { 0}
    &{ 0}
    &{ 0}
    

    p1、p2、p3都是person struct的实例,但p2和p3是完全等价的,它们都指向实例的指针,指针中保存的是实例的地址,所以指针再指向实例,p1则是直接指向实例。这三个变量与person struct实例的指向关系如下:

     变量名      指针     数据对象(实例)
    -------------------------------
    p1(addr) -------------> { 0}
    p2 -----> ptr(addr) --> { 0}
    p3 -----> ptr(addr) --> { 0}
    

    所以p1和ptr(addr)保存的都是数据对象的地址,p2和p3则保存ptr(addr)的地址。通常,将指向指针的变量(p1、p2)直接称为指针,将直接指向数据对象的变量(p1)称为对象本身,因为指向数据对象的内容就是数据对象的地址,其中ptr(addr)和p1保存的都是实例对象的地址。

    尽管一个是数据对象值,一个是指针,它们都是数据对象的实例。也就是说,p1.namep2.name都能访问对应实例的属性。

    var p4 *person呢,它是什么?该语句表示p4是一个指针,它的指向对象是person类型的,但因为它是一个指针,它将初始化为nil,即表示没有指向目标。但已经明确表示了,p4所指向的是一个保存数据对象地址的指针。也就是说,目前为止,p4的指向关系如下:

    p4 -> ptr(nil)
    

    既然p4是一个指针,那么可以将&person{}new(person)赋值给p4。

    var p4 *person
    p4 = &person{
        name:"longshuai",
        age:23,
    }
    fmt.Println(p4) 
    

    上面的代码将输出:

    &{longshuai 23}
    

    传值 or 传指针

    Go函数给参数传递值的时候是以复制的方式进行的。

    复制传值时,如果函数的参数是一个struct对象,将直接复制整个数据结构的副本传递给函数,这有两个问题:

    • 函数内部无法修改传递给函数的原始数据结构,它修改的只是原始数据结构拷贝后的副本
    • 如果传递的原始数据结构很大,完整地复制出一个副本开销并不小

    所以,如果条件允许,应当给需要struct实例作为参数的函数传struct的指针。例如:

    func add(p *person){...}
    

    既然要传指针,那struct的指针何来?自然是通过&符号来获取。分两种情况,创建成功和尚未创建的实例。

    对于已经创建成功的struct实例p,如果这个实例是一个值而非指针(即p->{person_fields}),那么可以&p来获取这个已存在的实例的指针,然后传递给函数,如add(&p)

    对于尚未创建的struct实例,可以使用&person{}或者new(person)的方式直接生成实例的指针p,虽然是指针,但Go能自动解析成实例对象。然后将这个指针p传递给函数即可。如:

    p1 := new(person)
    p2 := &person{}
    add(p1)
    add(p2)
    

    struct field的tag属性

    在struct中,field除了名称和数据类型,还可以有一个tag属性。tag属性用于"注释"各个字段,除了reflect包,正常的程序中都无法使用这个tag属性。

    type TagType struct { // tags
        field1 bool   "An important answer"
        field2 string "The name of the thing"
        field3 int    "How much there are"
    }
    

    匿名字段和struct嵌套

    struct中的字段可以不用给名称,这时称为匿名字段。匿名字段的名称强制和类型相同。例如:

    type animal struct {
        name string
        age int
    }
    type Horse struct{
        int
        animal
        sound string
    }
    

    上面的Horse中有两个匿名字段intanimal,它的名称和类型都是int和animal。等价于:

    type Horse struct{
        int int
        animal animal
        sound string
    }
    

    显然,上面Horse中嵌套了其它的struct(如animal)。其中animal称为内部struct,Horse称为外部struct。

    以下是一个嵌套struct的简单示例:

    package main
    
    import (
    	"fmt"
    )
    
    type inner struct {
    	in1 int
    	in2 int
    }
    
    type outer struct {
    	ou1 int
    	ou2 int
    	int
    	inner
    }
    
    func main() {
    	o := new(outer)
    	o.ou1 = 1
    	o.ou2 = 2
    	o.int = 3
    	o.in1 = 4
    	o.in2 = 5
    	fmt.Println(o.ou1)  // 1
    	fmt.Println(o.ou2)  // 2
    	fmt.Println(o.int)  // 3
    	fmt.Println(o.in1)  // 4
    	fmt.Println(o.in2)  // 5
    }
    

    上面的o是outer struct的实例,但o除了具有自己的显式字段ou1和ou2,还具备int字段和inner字段,它们都是嵌套字段。一被嵌套,内部struct的属性也将被外部struct获取,所以o.into.in1o.in2都属于o。也就是说,外部struct has a 内部struct,或者称为struct has a field

    输出以下外部struct的内容就很清晰了:

    fmt.Println(o)  // 结果:&{1 2 3 {4 5}}
    

    上面的outer实例,也可以直接赋值构建:

    o := outer{1,2,3,inner{4,5}}
    

    在赋值inner中的in1和in2时不能少了inner{},否则会认为in1、in2是直接属于outer,而非嵌套属于outer。

    显然,struct的嵌套类似于面向对象的继承。只不过继承的关系模式是"子类 is a 父类",例如"轿车是一种汽车",而嵌套struct的关系模式是外部struct has a 内部struct,正如上面示例中outer拥有inner。而且,从上面的示例中可以看出,Go是支持"多重继承"的。

    具名struct嵌套

    前面所说的是在struct中以匿名的方式嵌套另一个struct,但也可以将嵌套的struct带上名称。

    直接带名称嵌套struct时,不会再自动深入到嵌套struct中去查找属性和方法。想要访问内部struct属性时,必须带上该struct的名称。

    例如:

    type animal struct {
        name string
        age int
    }
    type Horse struct{
        a animal
        sound string
    }
    

    这时候,想要访问嵌套在Horse中animal的name属性,则只能通过h.a.name的方式(h为Horse的实例对象),且访问h.name时将直接报错,因为在Horse里找不到name属性。

    嵌套struct的名称冲突问题

    假如外部struct中的字段名和内部struct的字段名相同,会如何?

    有以下两个名称冲突的规则:

    1. 外部struct覆盖内部struct的同名字段、同名方法
    2. 同级别的struct出现同名字段、方法将报错

    第一个规则使得Go struct能够实现面向对象中的重写(override),而且可以重写字段、重写方法。

    第二个规则使得同名属性不会出现歧义。例如:

    type A struct {
        a int
        b int
    }
    
    type B struct {
        b float32
        c string
        d string
    }
    
    type C struct {
        A
        B
        a string
        c string
    }
    
    var c C
    

    按照规则(1),直属于C的a和c会分别覆盖A.a和B.c。可以直接使用c.a、c.c分别访问直属于C中的a、c字段,使用c.d或c.B.d都访问属于嵌套的B.d字段。如果想要访问内部struct中被覆盖的属性,可以c.A.a的方式访问。

    按照规则(2),A和B在C中是同级别的嵌套结构,所以A.b和B.b是冲突的,将会报错,因为当调用c.b的时候不知道调用的是c.A.b还是c.B.b。

    递归struct:嵌套自身

    如果struct中嵌套的struct类型是自己的指针类型,可以用来生成特殊的数据结构:链表或二叉树(双端链表)。

    例如,定义一个单链表数据结构,每个Node都指向下一个Node,最后一个Node指向空。

    type Node struct {
    	data string
    	ri   *Node
    }
    

    以下是链表结构示意图:

     ------|----         ------|----         ------|-----
    | data | ri |  -->  | data | ri |  -->  | data | nil |
     ------|----         ------|----         ------|----- 
    

    如果给嵌套两个自己的指针,每个结构都有一个左指针和一个右指针,分别指向它的左边节点和右边节点,就形成了二叉树或双端链表数据结构

    二叉树的左右节点可以留空,可随时向其中加入某一边加入新节点(像节点加入到树中)。添加节点时,节点与节点之间的关系是父子关系。添加完成后,节点与节点之间的关系是父子关系或兄弟关系。

    双端链表有所不同,添加新节点时必须让某节点的左节点和另一个节点的右节点关联。例如目前已有的链表节点A <-> C,现在要将B节点加入到A和C的中间,即A<->B<->C,那么A的右节点必须设置为B,B的左节点必须设置为A,B的右节点必须设置为C,C的左节点必须设置为B。也就是涉及了4次原子性操作,它们要么全设置成功,失败一个则链表被破坏。

    例如,定义一个二叉树:

    type Tree struct {
    	le   *Tree
    	data string
    	ri   *Tree
    }
    

    最初生成二叉树时,root节点没有任何指向。

    // root节点:初始左右两端为空
    root := new(Tree)
    root.data = "root node"
    

    随着节点增加,root节点开始指向其它左节点、右节点,这些节点还可以继续指向其它节点。向二叉树中添加节点的时候,只需将新生成的节点赋值给它前一个节点的le或ri字段即可。例如:

    // 生成两个新节点:初始为空
    newLeft := new(Tree)
    newLeft.data = "left node"
    newRight := &Tree{nil, "Right node", nil}
    
    // 添加到树中
    root.le = newLeft
    root.ri = newRight
    
    // 再添加一个新节点到newLeft节点的右节点
    anotherNode := &Tree{nil, "another Node", nil}
    newLeft.ri = anotherNode
    

    简单输出这个树中的节点:

    fmt.Println(root)
    fmt.Println(newLeft)
    fmt.Println(newRight)
    

    输出结果:

    &{0xc042062400 root node 0xc042062420}
    &{<nil> left node 0xc042062440}
    &{<nil> Right node <nil>}
    

    当然,使用二叉树的时候,必须为二叉树结构设置相关的方法,例如添加节点、设置数据、删除节点等等。

    另外需要注意的是,一定不要将某个新节点的左、右同时设置为树中已存在的节点,因为这样会让树结构封闭起来,这会破坏了二叉树的结构。

  • 相关阅读:
    83. Remove Duplicates from Sorted List
    35. Search Insert Position
    96. Unique Binary Search Trees
    94. Binary Tree Inorder Traversal
    117. Populating Next Right Pointers in Each Node II
    116. Populating Next Right Pointers in Each Node
    111. Minimum Depth of Binary Tree
    169. Majority Element
    171. Excel Sheet Column Number
    190. Reverse Bits
  • 原文地址:https://www.cnblogs.com/f-ck-need-u/p/9882315.html
Copyright © 2011-2022 走看看