结构体
声明结构体
基本语法
1
2
3
4
| type 结构体名称 struct{
field1 type
field2 type
}
|
字段/属性
基本介绍
- 结构体字段 = 属性 = field
- 字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型
注意事项
- 字段声明语法同变量,示例:
Name string
- 字段的类型可以为:基本类型、数组或引用类型
- 在创建一个结构体变量后,如果没有给字符赋值,都对应一个零值(默认值)
- 布尔类型是
false
,数值是0
,字符串是""
- 数组类型的默认值和它的元素类型相关,比如:
score[3] int
默认值:[0,0,0]
- 指针、slice和map的零值都是
nil
,即还没有分配空间,需要make
后使用
- 不同结构体变量的字段是独立的,互不影响,一个结构体变量字段的更改,不影响另外一个
创建结构体变量
方式一:直接声明
方式二:{}
1
| var person Person = Person{}
|
方式三:new
1
| var person *Person = new(Person)
|
方式四:&
1
| var person *Person = &Person{}
|
说明
- 第3、4种方式返回的是结构体指针。
- 结构体指针访问字段的标准方式:
(*结构体指针).字段名
,比如:(*person).Name = "tom"
- 对于结构体指针访问字段,golang做了简化操作,也支持
结构体指针.字段名
,比如:person.Name = "tom"
。这样更加符合程序员使用习惯,golang底层进行了一步转化操作。
结构体内存分配机制
结构体变量在内存中存在示例图
变量总是存在内存中的,那么结构体变量也是一样的
1
2
3
4
5
6
7
8
9
10
| var p1 Person
p1.Name = "Joker"
p1.Age = 18
var p2 Person = p1
fmt.Println(p2.Age) //18
p2.Name = "Tommy"
fmt.Printf("p2.Name=%v, p1.Name=%v\n", p2.Name, p1.Name) //p2.Name=Tommy, p1.Name=Joker
|
示例图:
结构体指针变量在内存中存在示例图
1
2
3
4
5
6
7
8
9
10
11
12
| var p1 Person
p1.Name = "Joker"
p1.Age = 18
var p2 *Person = &p1
fmt.Println((*p2).Age) //18
fmt.Println(p2.Age) //18
p2.Name = "Tommy"
fmt.Printf("p2.Name=%v, p1.Name=%v\n", p2.Name, p1.Name) //p2.Name=Tommy, p1.Name=Tommy
fmt.Printf("p2.Name=%v, p1.Name=%v\n", (*p2).Name, p1.Name) //p2.Name=Tommy, p1.Name=Tommy
|
示例图:
结构体使用注意事项和细节
- 结构体的所有字段在内存中是连续的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| package main
import "fmt"
type Point struct {
x int
y int
}
type Rect struct {
leftUp, rightDown Point
}
type Rect2 struct {
leftUp, rightDown *Point
}
func main() {
r1 := Rect{Point{1, 2}, Point{3, 4}}
// r1有4个int,在内存中是连续分布的
fmt.Printf("r1.leftUp.x的地址=%p r1.leftUp.y的地址=%p r1.rightDown.x的地址=%p r1.rightDown.y的地址=%p\n",
&r1.leftUp.x, &r1.leftUp.y, &r1.rightDown.x, &r1.rightDown.y)
// r1.leftUp.x的地址=0x1400012c000 r1.leftUp.y的地址=0x1400012c008 r1.rightDown.x的地址=0x1400012c010 r1.rightDown.y的地址=0x1400012c018
r2 := Rect2{&Point{10, 20}, &Point{30, 40}}
// r2有2个*Point,在内存中2个*Point本身地址也是连续分布的
fmt.Printf("r2.leftUp 本身地址=%p r2.rightDown 本身地址=%p\n", &r2.leftUp, &r2.rightDown) //r2.leftUp 本身地址=0x1400008e210 r2.rightDown 本身地址=0x1400008e218
// 但是它们指向的地址不一定是连续的
fmt.Printf("r2.leftUp 指向地址=%p r2.rightDown 指向地址=%p\n", r2.leftUp, r2.rightDown) //r2.leftUp 指向地址=0x140000a8010 r2.rightDown 指向地址=0x140000a8020
}
|
内存分布图:
- 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| package main
import "fmt"
type A struct {
Num int
}
type B struct {
Num int
}
func main() {
var a A
var b B
a = A(b) // 可以进行转换
fmt.Println(a, b)
}
|
- 结构体进行type重新定义(相当于取别名),Golang认为是新的数据类型,但是相互间可以强转
1
2
3
4
5
6
7
8
9
10
11
| type Student struct {
Name string
Age int
}
type Stu Student
var stu1 Student
var stu2 Stu
//stu2 = stu1 //cannot use stu1 (variable of type Student) as type Stu in assignment
stu2 = Stu(stu1)
fmt.Println(stu1, stu2)
|
- struct的每个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见的使用场景就是序列化和反序列化
方法
Golang中的方法是作用在指定的数据类型上(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct
声明方法
1
2
3
4
| func (recevier type) methodName (参数列表) (返回值列表){
// 方法体
return 返回值
}
|
recevier type
:表示这个方法和type这个类型进行绑定,或者说该方法作用于type类型recevier type
:type可以是结构体,也可以是其它的自定义类型recevier
:就是type类型的一个变量(实例),- 参数列表:方法输入
- 返回值列表:返回值,可以有多个
- 方法体:逻辑代码
return
:不是必须的
示例:
方法的调用和传参机制原理
说明:方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当做实参也传递给方法。
方法注意事项和细节
1、结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式。
2、如果希望在方法中修改结构体变量的值,可以通过结构体指针的方式进行处理。(这种方式用的比较多,因为指针传递效率高)
3、Golang中的方法作用在指定的数据类型上(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct。
4、方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其他包访问。
5、如果一个变量实现了String()
这个方法,那么fmt.Println
默认会调用这个变量的String()
进行输出。
方法 VS 函数
1、调用方式不一样
函数的调用方式:函数名(实参列表)
方法的调用方式:变量.方法名(实参列表)
2、普通函数,接收者为值类型时,不能将指针类型的数据直接传递
3、对于方法,接收者为值类型时,可以直接用指针类型的变量调用方法,反过来也可以。
注:不管调用形式如何,真正决定是值拷贝还是地址拷贝还是看方式是和哪种类型绑定
工厂模式
Golang的结构体没有构造函数,通常可以使用工厂模式来解决这个问题。
使用工厂模式解决结构体首字母小写在别的包调用问题: