struct定义结构,结构由字段(field)组成,每一个field都有所属数据类型,在一个struct中,每一个字段名都必须惟一。node
说白了就是拿来存储数据的,只不过可自定义化的程度很高,用法很灵活,Go中很多功能依赖于结构,就这样一个角色。数据结构
Go中不支持面向对象,面向对象中描述事物的类的重担由struct来挑。好比面向对象中的继承,可使用组合(composite)来实现:struct中嵌套一个(或多个)类型。面向对象中父类与子类、类与对象的关系是is a
的关系,例如Horse is a Animal
,Go中的组合则是外部struct与内部struct的关系、struct实例与struct的关系,它们是has a
的关系。Go中经过struct的composite,能够"模仿"不少面向对象中的行为,它们很"像"。ide
定义struct的格式以下:函数
type identifier struct { field1 type1 field2 type2 … } // 或者 type T struct { a, b int }
理论上,每一个字段都是有具备惟一性的名字的,但若是肯定某个字段不会被使用,能够将其名称定义为空标识符_
来丢弃掉:指针
type T struct { _ string a int }
每一个字段都有类型,能够是任意类型,包括内置简单数据类型、其它自定义的struct类型、当前struct类型自己、接口、函数、channel等等。code
若是某几个字段类型相同,能够缩写在同一行:对象
type mytype struct { a,b int c string }
定义了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{}
的方式。
下面三种方式均可以构造person struct的实例p:
p1 := person{} p2 := &person{} p3 := new(person)
但p1和p二、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}
p一、p二、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)的地址。一般,将指向指针的变量(p一、p2)直接称为指针,将直接指向数据对象的变量(p1)称为对象自己,由于指向数据对象的内容就是数据对象的地址,其中ptr(addr)和p1保存的都是实例对象的地址。
但尽管一个是数据对象值,一个是指针,它们都是数据对象的实例。也就是说,p1.name
和p2.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}
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属性。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中的字段能够不用给名称,这时称为匿名字段。匿名字段的名称强制和类型相同。例如:
type animal struct { name string age int } type Horse struct{ int animal sound string }
上面的Horse中有两个匿名字段int
和animal
,它的名称和类型都是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.int
、o.in1
、o.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{}
,不然会认为in一、in2是直接属于outer,而非嵌套属于outer。
显然,struct的嵌套相似于面向对象的继承。只不过继承的关系模式是"子类 is a 父类",例如"轿车是一种汽车",而嵌套struct的关系模式是外部struct has a 内部struct
,正如上面示例中outer拥有inner
。并且,从上面的示例中能够看出,Go是支持"多重继承"的。
前面所说的是在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的字段名相同,会如何?
有如下两个名称冲突的规则:
第一个规则使得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类型是本身的指针类型,能够用来生成特殊的数据结构:链表或二叉树(双端链表)。
例如,定义一个单链表数据结构,每一个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>}
固然,使用二叉树的时候,必须为二叉树结构设置相关的方法,例如添加节点、设置数据、删除节点等等。
另外须要注意的是,必定不要将某个新节点的左、右同时设置为树中已存在的节点,由于这样会让树结构封闭起来,这会破坏了二叉树的结构。