多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
[TOC] # 结构体 ## 结构体的定义 使用`type`和`struct`关键字来定义结构体,具体代码格式如下: ~~~go type 类型名 struct { 字段名 字段类型 字段名 字段类型 … } ~~~ 其中: * 类型名:标识自定义结构体的名称,在同一个包内不能重复。 * 字段名:表示结构体字段名。结构体中的字段名必须唯一。 * 字段类型:表示结构体字段的具体类型。 ~~~go type person struct { name string age int8 } ~~~ 同样类型的字段也可以写在一行, ~~~go type person1 struct { name, province string age int8 } ~~~ ## 结构体实例化 只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。 ~~~go var 结构体实例 结构体类型 ~~~ ### 基本实例化 ~~~go type person struct { name string age int8 } func main() { var p1 person p1.name = "小叮当" p1.age = 18 fmt.Printf("p1=%v\n", p1) //p1={小叮当18} fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"小叮当", age:18} } ~~~ 通过`.`来访问结构体的字段(成员变量),例如`p1.name`和`p1.age`等。 ### 匿名结构体 在定义一些临时数据结构等场景下还可以使用匿名结构体。 ~~~ package main import ( "fmt" ) func main() { var user struct{Name string; Age int} user.Name = "小叮当" user.Age = 18 fmt.Printf("%#v\n", user) } ~~~ ### 创建指针类型结构体 可以通过使用`new`关键字对结构体进行实例化,得到的是结构体的地址 ~~~go var p2 = new(person) fmt.Printf("%T\n", p2) //*main.person fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"",age:0} ~~~ 需要注意的是在Go语言中支持对结构体指针直接使用`.`来访问结构体的成员。 ~~~go var p2 = new(person) p2.name = "小叮当" p2.age = 18 fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"小叮当", age:18} ~~~ ### 取结构体的地址实例化 使用`&`对结构体进行取地址操作相当于对该结构体类型进行了一次`new`实例化操作。 ~~~go p3 := &person{} fmt.Printf("%T\n", p3) //*main.person fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"",age:0} p3.name = "小叮当" p3.age = 18 fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"小叮当",age:18} ~~~ `p3.name = "小叮当"`其实在底层是`(*p3).name = "小叮当"`,这是Go语言帮我们实现的语法糖。 ## 结构体初始化 没有初始化的结构体,其成员变量都是对应其类型的零值。 ~~~go type person struct { name string age int8 } func main() { var p4 person fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"",age:0} } ~~~ ### 使用键值对初始化 使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。 ~~~go p5 := person{ name: "小叮当", age: 18, } fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"小叮当", age:18} ~~~ 也可以对结构体指针进行键值对初始化 ~~~go p6 := &person{ name: "小叮当", city: "北京", age: 18, } fmt.Printf("p6=%#v\n", p6) ~~~ 当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。 ~~~go p7 := &person{ city: "北京", } fmt.Printf("p7=%#v\n", p7) ~~~ ### 使用值的列表初始化 初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值: ~~~go p8 := &person{ "小叮当", 18, } fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"小叮当",age:18} ~~~ 使用这种格式初始化时,需要注意: 1. 必须初始化结构体的所有字段。 2. 初始值的填充顺序必须与字段在结构体中的声明顺序一致。 3. 该方式不能和键值初始化方式混用。 ## 结构体内存布局 结构体占用一块连续的内存。 ~~~go type test struct { a int8 b int8 c int8 d int8 } n := test{ 1, 2, 3, 4, } fmt.Printf("n.a %p\n", &n.a) fmt.Printf("n.b %p\n", &n.b) fmt.Printf("n.c %p\n", &n.c) fmt.Printf("n.d %p\n", &n.d) ~~~ 输出: ~~~bash n.a 0xc0000a0060 n.b 0xc0000a0061 n.c 0xc0000a0062 n.d 0xc0000a0063 ~~~ ### 空结构体 空结构体是不占用空间的。 ~~~go var v struct{} fmt.Println(unsafe.Sizeof(v)) // 0 ~~~ ### 面试题 ~~~ type student struct {     Name string     Age  int } func pase_student() map[string]*student { m := make(map[string]*student) stus := []student{         {Name: "zhou", Age: 24},         {Name: "li", Age: 23},         {Name: "wang", Age: 22},     } for _, stu := range stus {         m[stu.Name] = &stu } return m } func main() { students := pase_student() fork, v := range students {         fmt.Printf("key=%s,value=%v \n", k, v)     } } ~~~ output:&{wang 22} **解析:**因为 for 遍历时,变量 `stu` 指针不变,每次遍历仅进行 struct 值拷贝,故 `m[stu.Name]=&stu` 实际上一致指向同一个指针,最终该指针的值为遍历的最后一个 struct 的值拷贝。形同如下代码: ~~~ varstu student for _, stu := range stus {     m[stu.Name] = &stu } ~~~ 修正方案,取数组中原始值的指针: ~~~ for i, _ := range stus { stu:=stus[i]     m[stu.Name] = &stu } ~~~ ## 构造函数 Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个`person`的构造函数。 因为`struct`是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。 ~~~go func newPerson(name, city string, age int8) *person { return &person{ name: name, age: age, } } ~~~ 调用构造函数 ~~~go p9 := newPerson("小叮当", 18) fmt.Printf("%#v\n", p9) //&main.person{name:"小叮当", age:18} ~~~ ## 方法和接收者 `方法(Method)`是一种作用于特定类型变量的函数。这种特定类型变量叫做`接收者(Receiver)`。接收者的概念就类似于其他语言中的`this`或者`self`。 格式如下: ~~~go func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) { 函数体 } ~~~ 其中: * 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是`self`、`this`之类的命名。例如,`Person`类型的接收者变量应该命名为`p`,`Connector`类型的接收者变量应该命名为`c`等。 * 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。 * 方法名、参数列表、返回参数:具体格式与函数定义相同。 ~~~go //Person 结构体 type Person struct { name string age int8 } //NewPerson 构造函数 func NewPerson(name string, age int8) *Person { return &Person{ name: name, age: age, } } //Dream func (p Person) Dream() { fmt.Printf("%s的梦想是学好Go语言!\n", p.name) } func main() { p1 := NewPerson("小叮当", 18) p1.Dream() } ~~~ `方法`与`函数`的区别是,函数不属于任何类型,方法属于特定的类型。 ### 指针类型的接收者 指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的`this`或者`self` ~~~go // SetAge 设置p的年龄 // 使用指针接收者 func (p *Person) SetAge(newAge int8) { p.age = newAge } ~~~ 调用该方法: ~~~go func main() { p1 := NewPerson("小叮当", 18) fmt.Println(p1.age) // 18 p1.SetAge(99) fmt.Println(p1.age) // 99 } ~~~ ### 值类型的接收者 当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。 ~~~go // SetAge2 设置p的年龄 // 使用值接收者 func (p Person) SetAge2(newAge int8) { p.age = newAge } func main() { p1 := NewPerson("小叮当", 18) p1.Dream() fmt.Println(p1.age) // 18 p1.SetAge2(99) // (*p1).SetAge2(99) fmt.Println(p1.age) // 18 } ~~~ ### 什么时候应该使用指针类型接收者 1. 需要修改接收者中的值 2. 接收者是拷贝代价比较大的大对象 3. 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。 ## 任意类型添加方法 在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的`int`类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。 ~~~go //MyInt 将int定义为自定义MyInt类型 type MyInt int //SayHello 为MyInt添加一个SayHello的方法 func (m MyInt) SayHello() { fmt.Println("Hello, 我是一个int。") } func main() { var m1 MyInt m1.SayHello() //Hello, 我是一个int。 m1 = 100 fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt } ~~~ **注意事项:**非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。 ## 结构体的匿名字段 结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。 ~~~go //Person 结构体Person类型 type Person struct { string int } func main() { p1 := Person{ "小叮当", 18, } fmt.Printf("%#v\n", p1) //main.Person{string:"小叮当", int:18} fmt.Println(p1.string, p1.int) //北京 18 } ~~~ **注意:**这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。 ## 嵌套结构体 一个结构体中可以嵌套包含另一个结构体或结构体指针 ![](https://img.kancloud.cn/be/0a/be0a81706aaf5b7cfcb82809426be1cb_789x671.png) ### 嵌套匿名字段 ![](https://img.kancloud.cn/f8/07/f8075a0f5c708a33dc1a26ee33f90aa6_1421x742.png) 字段查找顺序:先在结构体中查找该字段,找不到再去嵌套的匿名字段中查找 ### 嵌套结构体的字段名冲突 嵌套结构体内部可能存在相同的字段名,在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。 ![](https://img.kancloud.cn/c4/ba/c4ba508662d50234ffe3b388addeb31e_1024x792.png) ## 结构体的“继承” ![](https://img.kancloud.cn/c4/ba/c4ba508662d50234ffe3b388addeb31e_1024x792.png) ## 结构体字段的可见性 结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。 ## 结构体与JSON序列化 ``` type School struct { SchName string     Class   []*Class } type Class struct { ClassNum int } func main() { school := &School{ SchName: "小叮当", Class: make([]*Class, 0, 2),     } for i := 1; i <= 2; i++ { c := &Class{             ClassNum: i, } school.Class = append(school.Class, c) } data, err := json.Marshal(school) if err != nil {     fmt.Println("json marshal failed") return  }  fmt.Printf("json:%s\n", data) //{"SchName":"小叮当","Class":[{"ClassNum":1},{"ClassNum":2}]} jstr := `{"SchName":"小叮当","Class":[{"ClassNum":1},{"ClassNum":2}]}` c := &School{} err = json.Unmarshal([]byte(jstr), c) if err != nil {      fmt.Println("json marshal failed") return } fmt.Printf("%#v", c) //&main.School{SchName:"小叮当", Class:[]*main.Class{(*main.Class)(0xc000014218), (*main.Class)(0xc000014240)}} } ``` ## 结构体标签(Tag) `Tag`是结构体的元信息,可以在运行的时候通过反射的机制读取出来。`Tag`在结构体字段的后方定义,由一对**反引号**包裹起来,具体的格式如下: ~~~ `key1:"value1" key2:"value2"` ~~~ 结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。 ![](https://img.kancloud.cn/6a/03/6a03c5f80f3c9c4955d3dbe89e47587f_566x745.png) **注意事项:**为结构体编写`Tag`时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。 ## 结构体和方法补充点 因为slice和map这两种数据类型都包含了指向底层数据的指针 ``` type Person struct { name string age int8 dreams []string } func (p *Person) SetDreams(dreams []string) { p.dreams = dreams } func main() { p1 := Person{name: "小叮当", age: 18} data := []string{"吃饭", "睡觉", "变魔术"} p1.SetDreams(data) // 下面的这个操作会改变 p1的dreams data[1] = "做梦" fmt.Println(p1.dreams) } //如果不改变,那就用copy去复制一份数据,保存在新的内存当中 func (p *Person) SetDreams(dreams []string) { p.dreams = make([]string, len(dreams)) copy(p.dreams, dreams) } ```