go结构体
# 面向对象
Go
语言仅支持封装,不支持继承和多态Go
语言没有class
,只有struct
# 结构体的定义
type point struct {i, j int}
示例:
package main
import "fmt"
type treeNode struct {
value int
left, right *treeNode // 指针
}
func main() {
// 建立了一个根节点
var root treeNode
root = treeNode{value: 3}
root.left = &treeNode{}
root.right = &treeNode{5, nil, nil}
root.right.left = new(treeNode) // new 返回的是一个地址
nodes := []treeNode{
{value: 3},
{},
{6, nil, &root},
}
fmt.Println(nodes)
}
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
Go
语言使用.
的方式进行调用变量或者调用函数或者调用指针。
如果想要使用类似类中的构造函数,我们需要自己定义工厂函数
func createNode(value int) *treeNode {
return &treeNode{value: value}
}
2
3
root.left.right = createNode(2)
注意:这里是返回的是函数体内部的局部变量的地址!
那么这个局部变量是分配在栈上还是在堆上呢?
- 不需要知道
- 根据编译器和运行环境来决定的
如果这里的返回值没有取地址且返回出去,编译器很可能认为这个变量不需要外面去访问,就在栈上分配;
当编译器看到你这个加了取地址返回给别人使用时,它就会去堆上去分配,然后
treeNode
会参与垃圾回收机制,外面拿着指针的人去做事,等到做完了,扔掉不用了,就会被回收掉。
在
Go
语言中可以这样使用,但是在C++
里就会发生错误!
通过上述的代码,我们构造了一个如下图的树结构:
我们对其进行遍历操作:
我们首先要为这个结构体定义方法,这个方法不是写在结构体里的,而是要写在结构体外部的。
func (node treeNode) print() {
fmt.Print(node.value)
}
2
3
它的func
有一个特点,有一个接收者
,表示这个print
方法是给这个node
来进行使用的。
root.print()
当我们调用了root.print()
就是代表root
是print
函数的接收者,这里为什么会放在前面呢,但其实放在函数括号内也可以,无非是调用的方式变了:
func print(node treeNode) {
fmt.Print(node.value)
}
2
3
调用方式就变为:
print(root)
其实和上面的没啥两样。
这两种方式都相当于参数传递,但是这两个是引用传参呢还是值传递呢?
Go
当然是值传递了
func (node treeNode) setValue(value int) {
node.value = value
}
2
3
然后我们来修改某一个节点的值
func main() {
// 建立了一个根节点
var root treeNode
root = treeNode{value: 3}
root.left = &treeNode{}
root.right = &treeNode{5, nil, nil}
root.right.left = new(treeNode)
root.left.right = createNode(2)
// 这里设置vlaue = 4
root.right.left.setValue(4)
// 打印其实,它没有变成4,还是0,所以Go是值传递
root.right.left.print()
fmt.Println()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
我们如何更改为可以修改它的值呢?
func (node *treeNode) setValue(value int) {
node.value = value
}
2
3
我们只要将参数传递变为指针类型。我们调用的时候,语法上会有一些人性化的事情,如果接收者变成了指针接收者,它实际调用的会把root.right.left
的地址传给它,再说了,这个root.right.left
其实也是指针类型;当我们调用root.print()
的时候,其实也会解析出当前的地址,来获取到对应的value
,拷贝一份值给它,进行打印。
我们再次进行调用上述代码,最终值变成了4
- 只有使用指针才可以改变结构内容
nil
指针也可以调用方法!
使用nil
调用方法
func (node *treeNode) setValue(value int) {
if node == nil {
fmt.Println("setting a value to nil node. Ignored.")
return
}
node.value = value
}
2
3
4
5
6
7
# 中序遍历
func (node *treeNode) traverse() {
if node == nil {
return
}
// 中序遍历 左中右
node.left.traverse()
node.print()
node.right.traverse()
}
2
3
4
5
6
7
8
9
0 2 3 4 5
结合上述的图来看,因为 4 是后面设置的值,所以此时的图应该是 5 左下角的应该是 4.
# 值接收者和指针接收者
- 要改变内容必须使用指针接收者
- 结构过大也考虑使用指针接收者
- 一致性:如有指针接收者,最好都是指针接收者(建议)
- 值接收者是
Go
语言特有的 - 值/指针接收者均可接收值/指针
# 结构体的内存布局
结构体占用一块连续的内存
package main
import "fmt"
type test struct {
a int8
b int8
c int8
d int8
}
func main() {
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)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
输出:
n.a 0x14000134004
n.b 0x14000134005
n.c 0x14000134006
n.d 0x14000134007
2
3
4
空结构体不占内存空间
var t = test
fmt.Println(unsafe.Sizeof(t)) // 0
2
使用空结构体来省内存空间的案例:得到去重后的名称
nameList := []string{"张三", "李四", "王五", "张三"}
var nameMap = make(map[string]struct{})
for _, name := range nameList {
nameMap[name] = struct{}{}
}
for key := range nameMap {
fmt.Println(key)
}
2
3
4
5
6
7
8
9
10
# 方法和接收者
Go 语言中的方法,是一种作用于特定类型的变量的函数。这种特定类型变量叫做
接收者(receiver)
。接收者的概念就类似于其他语言中的this
或者self
。
方法定义格式如下:
func (接收者变量 接收者类型) 方法名 (参数列表) (返回参数) {
函数体
}
2
3
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是
self
、this
之类的命名。例如:Person
类型的接收者变量应该命名为p
等。 - 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型
- 方法名、参数列表、返回参数:具体格式与函数定义相同
type Person struct {
name string
age int
}
func NewPerson(name string, age int) *Person {
return &Person{name: name, age: age}
}
func main() {
p := NewPerson("张三", 20)
p.dream("吃喝拉撒")
}
func (p Person) dream(d string) {
fmt.Printf("%s的梦想是%s\n", p.name, d)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 结构体与 JSON 序列化
JSON
是一种轻量级的数据交换格式。易与人阅读和编写。同时也利于机器解析和生成。JSON
键值对是用来保存JS
对象的一种方式,键值对的组合中的键名写在前面并用双引号""
包裹,使用冒号:
来分隔,然后紧接着值;多个键值之间使用,
分隔。
结构体转换为JSON
的包
json.Marshal
方法
package main
import (
"encoding/json"
"fmt"
)
// Student1 学生
type Student1 struct {
ID int
Gender string
Name string
}
//Class 班级
type Class struct {
Title string
Students []*Student1
}
func main() {
c := &Class{
Title: "101",
Students: make([]*Student1, 0, 200),
}
for i := 0; i < 10; i++ {
stu := &Student1{
Name: fmt.Sprintf("stu%02d", i),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
//JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n", data)
//JSON反序列化:JSON格式的字符串-->结构体
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
json:{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gende"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Nastu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}
&main.Class{Title:"101", Students:[]*main.Student1{(*main.Student1)(0x140001066c0), (*main.Student1)(0x140001066f0), (*main.Student1)(0x14000106720), (*main.Student1)(0x14000106750), (*main.Student1)(0x140001067b0), (*main.Student1)(0x140001067e0), (*main.Student1)(0x14000106810), (*main.Student1)(0x14000106840), (*main.Student1)(0x14000106870), (*main.Student1)(0x140001068a0)}}
2
3
注意:反序列化一定要传入一个指针类型
如果需要解析出来的键为别的名称,我们需要在结构体的字段后面加上
tag
,即结构体标签,就是告诉对应的包或函数这个字段的名字
//Class 班级
type Class struct {
Title string
Students []*Student1 `json:"student_list"`
}
2
3
4
5
当你用json
包里的函数访问到这个结构体的时候,就可以读取到这个json
里的双引号的值来代替返回的属性值。
如果是和java
的传xml
格式,就再加一个:xml:"student_list"
即可,标签统一写到`反引号里,多个tag之间用空格分开,每组是用冒号分割的键值对。
json:{"Title":"101","student_list":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Ger":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男"e":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}
&main.Class{Title:"101", Students:[]*main.Student1(nil)}
2
3
# 结构体方法补充
因为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) // 会影响到 p1.dreams
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
正确的做法是在方法中使用传入的slice
的拷贝进行结构体赋值
func (p *Person) SetDreams(dreams []string) {
tmp := make([]string, len(dreams))
copy(tmp, dreams)
p.dreams = tmp
}
2
3
4
5
同样的问题也存在于返回值 slice 和 map 的情况,在实际编码过程中一定要注意这个问题。