[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)
}
```
- Go准备工作
- 依赖管理
- Go基础
- 1、变量和常量
- 2、基本数据类型
- 3、运算符
- 4、流程控制
- 5、数组
- 数组声明和初始化
- 遍历
- 数组是值类型
- 6、切片
- 定义
- slice其他内容
- 7、map
- 8、函数
- 函数基础
- 函数进阶
- 9、指针
- 10、结构体
- 类型别名和自定义类型
- 结构体
- 11、接口
- 12、反射
- 13、并发
- 14、网络编程
- 15、单元测试
- Go常用库/包
- Context
- time
- strings/strconv
- file
- http
- Go常用第三方包
- Go优化
- Go问题排查
- Go框架
- 基础知识点的思考
- 面试题
- 八股文
- 操作系统
- 整理一份资料
- interface
- array
- slice
- map
- MUTEX
- RWMUTEX
- Channel
- waitGroup
- context
- reflect
- gc
- GMP和CSP
- Select
- Docker
- 基本命令
- dockerfile
- docker-compose
- rpc和grpc
- consul和etcd
- ETCD
- consul
- gin
- 一些小点
- 树
- K8s
- ES
- pprof
- mycat
- nginx
- 整理后的面试题
- 基础
- Map
- Chan
- GC
- GMP
- 并发
- 内存
- 算法
- docker