结构体(struct)
Go 的设计目标是取代 C/C++,所以 Go 里面的 struct 和 C 的类似,与 int/float 一样属于值类型,值类型的特点是内存紧凑,大小固定,对 GC 与内存访问来说都比较友好。
Go 语言中没有类的概念,因此在 Go 中结构体有着更为重要的地位。
结构体也是值类型,因此可以通过 new 函数来创建。
定义
结构体是由一系列称为字段(fields)的命名元素组成,每个元素都有一个名称和一个类型。
字段名称可以
显式指定(IdentifierList)隐式指定(EmbeddedField)
没有显式字段名称的字段称为 匿名(内嵌)字段。
在结构体中,非空字段名称必须是唯一的。
结构体定义的一般方式如下:
type identifier struct {field1 type1field2 type2...}// 或者type T struct { a, b int }
理论上,每个字段都是有具有唯一性的名字的,但如果确定某个字段不会被使用,可以将其名称定义为空标识符_来丢弃掉:
type T struct {_ stringa int}
具名结构体
type Point struct { X, Y int }type Rect1 struct { Min, Max Point }type Rect2 struct { Min, Max *Point }
定义了3个个具名结构体分别是 Point Rect1 Rect2 ,内存分布如下图

struct 内存分布示意图
可以看到, Point Rect1 Rect2 , 在内存中都是连续的。
匿名结构体
person:= struct { // 匿名结构name stringage int}{name:"匿名", age:1}f.Println("person:", person)
空结构体
可以使用 unsafe.Sizeof 计算出一个数据类型实例需要占用的字节数。
空结构体占用空间么
package mainimport ("fmt""unsafe")func main() {fmt.Println(unsafe.Sizeof(struct{}{}))}// 运行上面的例子将会输出:$ go run main.go0
也就是说,空结构体 struct{} 实例不占据任何的内存空间。
struct{}表示一个空的结构体,注意,直接定义一个空的结构体并没有意义。
但在某些方面有很多实用的功能,具体可以参考:Go 语言高性能编程 - 使用空结构体节省内存
初始化
内存分配
使用 new 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:
type S struct {a int;b float64}new(S)
new(S) 为 S 类型的变量分配内存,并初始化(a = 0,b = 0.0),返回包含该位置地址的类型 * S的值。
我们一般的惯用方法是:
t := new(T) // 变量 t 是一个指向 T的指针,此时结构体字段的值是它们所属类型的零值。或var t T // 给 t 分配内存,并零值化内存,但是这个时候 t 是类型 T。
两种方式中,t 通常被称做类型 T 的一个实例(instance)或对象(object)。
结构体普通变量初始化
这个使用方式并没有为字段赋初始值,因此所有字段都会被自动赋予自已类型的零值
比如name的值为空字符串 "",age 的值为 0。
type Member struct {id intname stringemail stringgender intage int}// 直接定义变量var m1 Member // 所有字段均为字段类型的默认值var m2 = Member{1, "小明", "xiaoming@163.com", 1, 18} // 简短变量声明方式:m2 := Member{1,"小明","xiaoming@163.com",1,18}var m3 = Member{id:2, "name":"小红"} // 简短变量声明方式:m3 := Member{id:2,"name":"小红"}
使用字面量创建变量,这种使用方式,可以在大括号中为结构体的成员赋初始值,有两种赋初始值的方式:
- 一种是按字段在结构体中的顺序赋值,上面代码中m2就是使用这种方式,这种方式要求所有的字段都必须赋值,因此如果字段太多,每个字段都要赋值,会很繁琐,
- 一种则使用字段名为指定字段赋值,如上面代码中变量m3的创建,使用这种方式,对于其他没有指定的字段,则使用该字段类型的零值作为初始化值。
初始化一个结构体实例
type myStruct struct { i int }var v myStruct // v 是结构体类型变量var p *myStruct // p 是指向一个结构体类型变量的指针v.ip.i
字面量初始化
var st structTestst = structTest{10, 15.5, "皮肤较黑"}
new 初始化 (指针) &TYPE{}
使用new()函数或&TYPE{}的方式来构造struct实例,它会为struct分配内存,为各个字段做好默认的赋0初始化。
它们是等价的,都返回数据对象的指针给变量,实际上&TYPE{}的底层会调用new()。
type structTest struct {i1 intf1 float32str string}// new初始化时不能赋值st := new(structTest)st.i1 = 10st.f1 = 15.5st.str= "皮肤较黑"// 等价于// &TYPE{} 初始化时可以赋值st := &structTest{10,15.5,"皮肤较黑",}// 此时st的类型是 *structTestfmt.Printf("st type is %T\n", st) // st type is *main.structTest
混合字面量语法(composite literal syntax)&structTest{a, b, c} 是一种简写,底层仍然会调用 new (),这里值的顺序必须按照字段顺序来写。
在下面的例子中能看到可以通过在值的前面放上字段名来初始化字段的方式。
表达式 new(Type) 和 &Type{} 是等价的。
时间间隔(开始和结束时间以秒为单位)是使用结构体的一个典型例子:
type Interval struct {start intend int}
初始化方式:
intr := Interval{0, 3} (A)intr := Interval{end:5, start:1} (B)intr := Interval{end:5} (C)
- (A)中,值必须以字段在结构体定义时的顺序给出,& 不是必须的。
- (B)显示了另一种方式,字段名加一个冒号放在值的前面,这种情况下值的顺序不必一致,并且某些字段还可以被忽略掉,就像(C)中那样。
选择 new() 还是选择 &TYPE{} 的方式构造实例?完全随意,它们是等价的。
但如果想要初始化时就赋值,可以考虑使用 &TYPE{} 的方式。
struct的值和指针
type person struct{name stringage int}// 下面三种方式都可以构造person struct的实例p: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)称为对象本身,因为指向数据对象的内容就是数据对象的地址
尽管一个是数据对象值,一个是指针,它们都是数据对象的实例。
也就是说,p1.name和p2.name都能访问对应实例的属性。
那 var p4 *person 呢,它是什么?
该语句表示 p4是一个指针,它的指向对象是person类型的。
但因为它是一个指针,它将初始化为nil,即表示没有指向目标。
但已经明确表示了,p4所指向的是一个保存数据对象地址的指针。也就是说,目前为止,p4的指向关系如下:
p4 -> ptr(nil)
既然 p4是一个指针,那么可以将 &person{} 或 new(person)赋值给p4。
var p4 *personp4 = &person{name:"longshuai",age:23,}fmt.Println(p4) // &{longshuai 23}
访问字段
通过变量名,使用逗号.,可以访问结构体类型中的:
- 字段
- 为字段赋值
- 对字段进行取址(&)操作
普通变量
package mainimport "fmt"//定义一个结构体类型type Student struct {id intname stringsex byte //字符类型age intaddr string}func main() {//定义一个结构体普通变量var s Student//操作成员,需要使用点(.)运算符s.id = 1s.name = "mike"s.sex = 'm' //字符s.age = 18s.addr = "bj"fmt.Println("s = ", s) // s = {1 mike 109 18 bj}fmt.Println(s.age) // 18}
指针结构体
指针结构体,即一个指向结构体的指针,声明结构体变量时,在结构体类型前加*号,便声明一个指向结构体的指针,如:
注意,指针类型为引用类型,声明结构体指针时,如果未初始化,则初始值为nil,只有初始化后,才能访问字段或为字段赋值。
var p1 *Studentp1.name = "小明" // 错误用法,未初始化,p1为nilp1 = &Student{}p1.name = "小明" // 初始化后,结构体指针指向某个结构体地址,才能访问字段,为字段赋值。
package mainimport "fmt"//定义一个结构体类型type Student struct {id intname stringsex byte //字符类型age intaddr string}func main() {//1、指针有合法指向后,才操作成员//先定义一个普通结构体变量var s Student//在定义一个指针变量,保存s的地址var p1 *Studentp1 = &s//通过指针操作成员 p1.id 和(*p1).id完全等价,只能使用.运算符p1.id = 1(*p1).name = "mike"p1.sex = 'm'p1.age = 18p1.addr = "bj"fmt.Println("p1 = ", p1)//2、通过new申请一个结构体p2 := new(Student)p2.id = 1p2.name = "mike"p2.sex = 'm'p2.age = 18p2.addr = "bj"fmt.Println("p2 = ", p2)//顺序初始化,每个成员必须初始化, 别忘了&var p3 *Student = &Student{1, "mike", 'm', 18, "bj"}fmt.Println("p1 = ", p3) // p3 = &{1 mike 109 18 bj}//指定成员初始化,没有初始化的成员,自动赋值为0p4 := &Student{name: "mike", addr: "bj"}fmt.Printf("p2 type is %T\n", p4) // p4 type is *main.Studentfmt.Println("p2 = ", p4) // p4 = &{0 mike 0 0 bj}}
结构体参数
结构体与数组一样,都是值传递
- 比如当把数组或结构体作为实参传给函数的形参时,会复制一个副本,所以为了提高性能,一般不会把数组直接传递给函数,而是使用切片(引用类型)代替
- 结构体传给函数时,可以使用指针结构体传递(传引用)。使用频率非常高!!!
package mainimport "fmt"type View struct {Id intIp stringTitle string}// 引用传递func TestViewP(p *View) {p.Ip = "192.168.1.1"}// 值拷贝func TestView(p View) {p.Ip = "192.168.1.1"}func main() {// 结构体指针pView := &View{Id: 1001,Ip: "127.0.0.1",Title: "Hello",}fmt.Println(pView) // &{1001 127.0.0.1 Hello}TestViewP(pView) // 传的是地址 引用fmt.Println(pView) // &{1001 192.168.1.1 Hello}// 普通结构体myView := View{Id: 1001,Ip: "180.0.0.1",Title: "World",}fmt.Println(myView) // {1001 180.0.0.1 World}//值传递TestView(myView)fmt.Println(myView) // {1001 180.0.0.1 World}//引用传递TestViewP(&myView)fmt.Println(myView) // {1001 192.168.1.1 World}}// &{1001 127.0.0.1 Hello}// &{1001 192.168.1.1 Hello}// {1001 180.0.0.1 World}// {1001 180.0.0.1 World}// {1001 192.168.1.1 World}
字段标记
在定义结构体字段时,除字段名称和数据类型外,还可以使用反引号为结构体字段声明元信息,这种元信息称为Tag。它是一个附属于字段的字符串,可以是文档或其他的重要标记。
这些标记信息通过反射接口可见,并参与结构体的类型标识,但在其他情况下被忽略。
type Member struct {Id int `json:"id,-"`Name string `json:"name"`Email string `json:"email"`Gender int `json:"gender,"`Age int `json:"age"`}
Tag由反引号括起来的一系列用空格分隔的 key:"value" 键值对组成,如:
Id int `json:"id" gorm:"AUTO_INCREMENT"`
标签的内容不可以在一般的编程中使用,只有包 reflect 能获取它。
使用 reflect 包,它可以在运行时自省类型、属性和方法,比如:在一个变量上调用 reflect.TypeOf() 可以获取变量的正确类型,如果变量是一个结构体类型,就可以通过 Field 来索引结构体的字段,然后就可以使用 Tag 属性。
package mainimport ("fmt""reflect")type TagType struct { // tagsfield1 bool "An important answer"field2 string "The name of the thing"field3 int "How much there are"}func main() {tt := TagType{true, "Barak Obama", 1}for i := 0; i < 3; i++ {refTag(tt, i)}}func refTag(tt TagType, ix int) {ttType := reflect.TypeOf(tt)ixField := ttType.Field(ix)fmt.Printf("%v\n", ixField.Tag)}// An important answer// The name of the thing// How much there are
结构匿名组合
匿名字段
一般情况下,定义结构体的时候是字段名与其类型一一对应,
实际上Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。
当匿名字段也是一个结构体的时候,那么这个结构体所拥有的全部字段都被隐式地引入了当前定义的这个结构体。
package mainimport "fmt"type Person struct {name string //名字sex byte //性别age int //年龄}// 匿名字段 Persontype Student struct {Person // 只有类型,没有名字,匿名字段,继承了Person的成员id intaddr string}func main() {//顺序初始化var s1 Student = Student{Person{"mike", 'm', 18}, 1, "bj"}fmt.Println("s1 = ", s1) // s1 = {{mike 109 18} 1 bj}//自动推导类型s2 := Student{Person{"mike", 'm', 18}, 1, "bj"}//fmt.Println("s2 = ", s2)//%+v, 显示更详细fmt.Printf("s2 = %+v\n", s2) // s2 = {Person:{name:mike sex:109 age:18} id:1 addr:bj}//指定成员初始化,没有初始化的常用自动赋值为0s3 := Student{id: 1}fmt.Printf("s3 = %+v\n", s3) // s3 = {Person:{name: sex:0 age:0} id:1 addr:}s4 := Student{Person: Person{name: "mike"}, id: 1}fmt.Printf("s4 = %+v\n", s4) // s4 = {Person:{name:mike sex:0 age:0} id:1 addr:}//s5 := Student{"mike", 'm', 18, 1, "bj"} //err}
成员操作
package mainimport "fmt"type Person struct {name string //名字sex byte //性别, 字符类型age int //年龄}type Student struct {Person //只有类型,没有名字,匿名字段,继承了Person的成员id intaddr string}func main() {s1 := Student{Person{"mike", 'm', 18}, 1, "bj"}//给成员赋值s1.name = "yoyo" // //等价于 s1.Person.name = "mike"s1.sex = 'f's1.age = 22s1.id = 666s1.addr = "sz"fmt.Printf("s1 = %+v\n", s1) // s1 = {Person:{name:yoyo sex:102 age:22} id:666 addr:sz}s1.Person = Person{"go", 'm', 18}fmt.Printf("s1 = %+v\n", s1) // s1 = {Person:{name:go sex:109 age:18} id:666 addr:sz}fmt.Println(s1.name, s1.sex, s1.age, s1.id, s1.addr) // go 109 18 666 sz}
同名字段
就近原则
package mainimport "fmt"type Person struct {name string //名字sex byte //性别, 字符类型age int //年龄}type Student struct {Person //只有类型,没有名字,匿名字段,继承了Person的成员id intaddr stringname string //和Person同名了}func main() {//声明(定义一个变量)var s Student//默认规则(纠结原则),如果能在本作用域找到此成员,就操作此成员// 如果没有找到,找到继承的字段s.name = "mike" //操作的是Student的name,还是Person的name?, 结论为Student的s.sex = 'm's.age = 18s.addr = "bj"//显式调用s.Person.name = "yoyo" //Person的namefmt.Printf("s = %+v\n", s) // s = {Person:{name:yoyo sex:109 age:18} id:0 addr:bj name:mike}}
其它匿名字段
非结构体类型
所有的内置类型和自定义类型都是可以作为匿名字段的:
package mainimport "fmt"type mystr string //自定义类型,给一个类型改名type Person struct {name string //名字sex byte //性别, 字符类型age int //年龄}type Student struct {Person // 结构体匿名字段int // 基础类型的匿名字段mystr}func main() {s := Student{Person{"mike", 'm', 18}, 666, "hehehe"}fmt.Printf("s = %+v\n", s) // s = {Person:{name:mike sex:109 age:18} int:666 mystr:hehehe}s.Person = Person{"go", 'm', 22}fmt.Println(s.name, s.age, s.sex, s.int, s.mystr) // go 22 109 666 hehehefmt.Println(s.Person, s.int, s.mystr) // {go 109 22} 666 hehehe}
结构体指针类型
package mainimport "fmt"type Person struct {name string // 名字sex byte // 性别, 字符类型age int // 年龄}type Student struct {*Person // 匿名字段,结构体指针类型id intaddr string}func main() {// 初始化 指针类型取址s1 := Student{&Person{"mike", 'm', 18}, 666, "bj"}fmt.Println(s1.name, s1.sex, s1.age, s1.id, s1.addr) // mike 109 18 666 bj//先定义变量var s2 Students2.Person = new(Person) //分配空间s2.name = "yoyo"s2.sex = 'm's2.age = 18s2.id = 222s2.addr = "sz"fmt.Println(s2.name, s2.sex, s2.age, s2.id, s2.addr) // yoyo 109 18 222 sz}
参考
