1. 结构体介绍
1.1 什么是结构体
结构体(struct)是一种用户定义的类型,它由一系列的字段组成,每个字段都有自己的名称和类型。
结构体也是值类型的,就算加了指针也是,只不过是复制的内存地址。
1.2 为什么要用结构体
结构体是 Go 语言中一种非常重要的数据类型,它允许你将多个不同类型的数据组合成一个单一的数据结构。这对于创建复杂的数据模型和对象非常有用。
2. 定义结构体
使用type关键字定义结构体,可以把结构体看做类型来使用。
必须指定结构体的字段(属性)名称和类型。
2.1 type关键字
在go中,type关键字主要用于定义新的类型或者为现有类型定义别名。
2.1.1 定义类型
// 快捷方式:NewType.struct
type NewType struct {Field1 Type1Field2 Type2// ...
}
2.1.2 定义别名
type Alias = int
2.2 如何理解类型
类型表示一类具有相同特征的事务,比如用户a和b,都有id、name、adder等属性,那么它们俩就属于具有相同特征的事务。
2.3 定义结构体
注意下面定义了一个全局结构体和局部结构体。
package main// type:自定义类型的关键字
// User:类型名称
// struct: 具体的数据类型
type User struct { // 自定义名为User的结构体类型(全局结构体)// 结构体内部定义属性、字段id intname, addr stringscore float32 // 这玩意儿可以叫属性、字段、成员、变量等,叫法很多}func main() {type User struct { // 局部结构体id intname, addr stringscore float32}
}
2.4 定义结构体实例(初始化)
上面type User struct只是创建了一个新的结构体,是一类抽象事务的集合。
要想在代码中使用,还需要通过长短格式声明,使得结构体具体化。
其实还有另一种初始化方式,那就是构造函数,后续会演示。
怎么理解这个抽象?就好比int类型,它也是一类相同特征事务的抽象,你没有办法直接操作int,只能通过定义int实例来操作。
2.4.1 var声明(常用)
结构体中也是零值可用的。
package mainimport "fmt"type User struct {id int // 默认值为0name, addr string // 默认值为空字符串""(实际打印出来啥也没有)score float32 // 默认值为0
}func main() {var u1 User // u1就是结构体实例 零值可用fmt.Println(u1)fmt.Printf("%v\n", u1)fmt.Printf("%+v\n", u1) // 打印结构体实例内容较为详细fmt.Printf("%#v\n", u1) // 打印结构体实例内容非常详细
}
========调试结果========
{0 0}
{0 0}
{id:0 name: addr: score:0}
main.User{id:0, name:"", addr:"", score:0}
2.4.2 字面量定义(常用)
2.4.2.1 零值
package mainimport "fmt"type User struct {id intname, addr stringscore float32
}func main() {// var u1 User = User{} // 这样的话 就是明确数据类型// u2 := User{} // 这样也可以var u1 = User{} // 该方式也是相当于定义了一个0值结构体实例。数据类型由u1自动推断。fmt.Println(u1)fmt.Printf("%v\n", u1)fmt.Printf("%+v\n", u1)fmt.Printf("%#v\n", u1)
}
========调试结果========
{0 0}
{0 0}
{id:0 name: addr: score:0}
main.User{id:0, name:"", addr:"", score:0}
2.4.2.2 指定值
package mainimport "fmt"type User struct {id intname, addr stringscore float32
}func main() {u1 := User{id: 123} // 没有指定值的字段,依然零值。u2 := User{name: "tom", score: 98.5, id: 8} // 也可以这样,部分字段指定值,部分字段不给// 还可以这样,全部指定值,并且字段名称只要正确就行,对配置先后顺序无要求。u3 := User{score: 98.5,name: "tom",id: 8,addr: "四川", // 注意:最后一个字段后面一定要跟一个逗号,不然报错}}
2.4.2.3 不指定字段
package mainimport "fmt"type User struct {id intname, addr stringscore float32
}func main() {// 不指定字段赋值,必须按照结构体内字段顺序,且全部都定义好对应的值u1 := User{1, "张三", "春熙路", 98.5}// 这里替换了id和name的顺序,就直接报错了// u2 := User{"张三", 1, "春熙路", 98.5}fmt.Println(u1)fmt.Printf("%v\n", u1)fmt.Printf("%+v\n", u1)fmt.Printf("%#v\n", u1)
}
========调试结果========
{1 张三 春熙路 98.5}
{1 张三 春熙路 98.5}
{id:1 name:张三 addr:春熙路 score:98.5}
main.User{id:1, name:"张三", addr:"春熙路", score:98.5}
3. 结构体可见性
全局结构体:
- 全局结构体:结构体名称首字母大写,包外可见。
· 全局结构体内部属性名称首字母小写,属性包外不可见。
· 全局结构体内部属性名称首字母大写,属性包外可见。- 全局结构体:结构体名称首字母小写,包外不可见。
. 属性首字母不管大小写,包内都可见。
局部结构体:函数体内部可见。
package main// 全局结构体:结构体名称首字母大写,包外可见
type User struct {// 属性首字母小写,包外不可见id intname, addr string// 属性首字母大写,包外可见Score float32
}// 全局结构体:结构体名称首字母小写,包内可见
type user struct { id intname, addr stringscore float32
}func main() {// 局部结构体,main函数内可见type User struct {id intname, addr stringscore float32}
}
4. 结构体的查询与修改
4.1 查询结构体字段
package mainimport "fmt"type User struct {id intname, addr stringscore float32
}func main() {u1 := User{score: 98.5,name: "tom",id: 8,addr: "四川",}// 访问部分属性fmt.Println(u1.name, u1.addr)// 访问全部属性fmt.Println(u1.name, u1.addr, u1.id, u1.score)
}
========调试结果========
tom 四川
tom 四川 8 98.5
4.2 修改结构体字段
指定修改的字段即可。
package mainimport ("fmt"
)type User struct {id intname, addr stringscore float32
}func main() {u1 := User{score: 98.5,name: "tom",id: 8,addr: "四川",}u1.score += 1.5fmt.Println(u1.score)
}
========调试结果========
100
5. 成员方法(字段方法)
只要是通过type自定义的类型,都可以有方法。
5.1 什么是成员方法
在Go语言中,结构体(struct)是一种聚合的数据类型,它允许你将多个不同类型的数据项组合成一个单一的实体。结构体可以拥有自己的方法,这些方法称为结构体的成员方法。
还有一点,这个成员指的就是结构体里面的字段。
5.2 定义成员方法
要为结构体定义成员方法,需要使用特殊的方法接收者语法。方法接收者看起来像一个参数列表,但它位于方法名之前,并且它指定了方法绑定到的类型。
5.2.1 普通函数
这里先用普通函数来演示,如何查询结构体实例中的某些字段。
package mainimport ("fmt"
)type User struct {id intname, addr stringscore float32
}// 定义一个函数,接收外部传参,并返回对应结构体字段值
func getName(u User) string {return u.name
}func main() {u1 := User{score: 98.5,name: "tom",id: 8,addr: "四川",}// 调用函数fmt.Println(getName(u1))
}
========调试结果========
tom
5.2.2 使用方法
这里给函数多加一个receiver,使其变成特殊函数。
其实无论是普通函数方式还是定义成员方法方式,其实本质上都是一样的。
package mainimport "fmt"type User struct {id intname, addr stringscore float32
}// 普通函数
func getName(u User) string {return u.name
}// 这里的u,在go中被称为receiver(接收者),u:接收者变量名称。User:接收者类型。
// 该方式等价于上面的普通函数,但由于定义了一个receiver(u User),所以变成了User结构体的方法,GetName也变成了User类型的专属方法,并且还是一个值类型的接收者(副本),只要有User结构体实例调用GetName()方法,u就会成为调用者的副本。
// 定义一个成员方法,接收者(receiver)是User类型
func (u User) GetName() string { // 这样定义的GetName就属于是User类型的方法了(GetName属于User类型)return u.name
}func main() {u1 := User{score: 98.5,name: "tom",id: 8,addr: "四川",}// 调用成员方法(其实也是个函数)fmt.Println(u1.GetName())
}
========调试结果========
tom
5.3 成员方法总结
值类型接收者,接收的是结构体的副本,当操作方法内部字段时,不会影响原始结构体实例。
上面的成员方法示例,(u User)这里相当于是多了一个副本,同样的数据,有两份,如果数据量大,对系统的资源损耗也会变大。
解决办法:添加指针,具体的下面会介绍。
6. 结构体指针
结构体结合指针,可以减少完全复制对系统资源的消耗。
6.1 结构体指针的使用
6.1.1 普通方式
用&取结构体内存地址时,返回的是对应结构体类型的指针
package mainimport "fmt"type Point struct {x, y int
}func main() {p1 := Point{10, 30}fmt.Printf("%T %+v\n", p1, p1)// 用&取结构体内存地址时,返回的是对应类型的指针,如下就返回了Point类型的指针// 或者说用&取内存地址后,p2就变成了指针类型了,因为P2有个指针指向了Point的内存地址p2 := &Point{4, 5}fmt.Printf("%T\n%+[1]v\n", p2)// 直接读取内容可以使用指针fmt.Printf("%v", *p2)
}
========调试结果========
main.Point {x:10 y:30} // main.Point为结构体类型,main为包名,Point为结构体名称
*main.Point // main包中Point类型的指针(Point 是在 main 包中定义的结构体类型)
&{x:4 y:5}
{4 5}
6.1.2 内建函数new
new函数的作用是创建一个新的指针实例,并返回对应实例的指针
package mainimport "fmt"type Point struct {x, y int
}func main() {// 基于Point模版创建一个新的Point实例,并返回该实例的指针类型的地址// new(这里只需要写类型就行,不用写其他的)p3 := new(Point) // new只会返回指针fmt.Printf("%T %[1]v", p3)
}
========调试结果========
*main.Point &{0 0}
6.2 通过结构体指针修改值
package mainimport "fmt"type Point struct {x, y int
}func main() {p2 := &Point{}p3 := new(Point)fmt.Printf("%T %[1]v\n", p3)p2.x += 100p3.y += 100fmt.Println(p2, p3)fmt.Println(*p2, *p3, p3.x, (*p3).x)
}
========调试结果========
*main.Point &{0 0}
&{100 0} &{0 100}
{100 0} {0 100} 0 0
6.3 小练习
6.3.1 示例一
package mainimport "fmt"type Point struct {x, y int
}func main() {p1 := Point{10, 20}fmt.Printf("p1的类型:%T|p1的值: %+[1]v|p1的内存地址:%[2]p\n", p1, &p1)p2 := p1 // 没有&的都是值拷贝fmt.Printf("p2的类型:%T|p2的值: %+[1]v|p2的内存地址:%[2]p\n", p2, &p2)
}
请问p1和p2有什么关系?
没关系,p1是p1,p2是p2。或者说p2是p1的副本,是两个独立的结构体类型,内存地址是不一样的。
6.3.2 示例二
package mainimport "fmt"type Point struct {x, y int
}func main() {p1 := Point{10, 20}fmt.Printf("p1的类型:%T|p1的值: %+[1]v|p1的内存地址:%[2]p\n", p1, &p1)p3 := &p1fmt.Printf("p3的类型:%T|p3的值: %+[1]v|p3的值:%[2]p\n", p3, &p3)
}
请问p3和p1有什么关系?
p3就相当于是p1,虽然变量内存地址不同的,可p3的指针,指向了p1的内存地址,所以值的来源和p1一样。但是这里&p3使用是有问题的,首先p3 := &p1,就相当于是把p1的内存地址赋值给了p3,这没问题,但是&p3只能看到p3本身为了存储p1内存地址而开辟的内存地址,这里有点容易误导人。
其实把fmt中&p3改成p3,就能看到p3=p1。但注意他俩类型不同,p1是结构体类型,p3是结构体指针类型。
6.3.3 示例三
package mainimport "fmt"type Point struct {x, y int
}func foo(p Point) Point {fmt.Printf("4 p的类型:%T|p的值: %+[1]v|p的内存地址:%[2]p\n", p, &p)return p
}func bar(p *Point) *Point {p.x++fmt.Printf("6 p的类型:%T|p的值: %+[1]v|p的内存地址:%[2]p\n", p, &p)return p
}func main() {p1 := Point{10, 20}fmt.Printf("1 p1的类型:%T|p1的值: %+[1]v|p1的内存地址:%[2]p\n", p1, &p1)p2 := p1fmt.Printf("2 p2的类型:%T|p2的值: %+[1]v|p2的内存地址:%[2]p\n", p2, &p2)p3 := &p1fmt.Printf("3 p3的类型:%T|p3的值: %+[1]v|p3的值:%[2]p\n", p3, p3)p4 := foo(p1)fmt.Printf("5 p4的类型:%T|p4的值: %+[1]v|p4的内存地址:%[2]p\n", p4, &p4)p5 := bar(p3) // 或者传&p1也行fmt.Printf("7 p5的类型:%T|p5的值: %+[1]v|p5的内存地址:%[1]p\n", p5)
}
问题一:第4处和第1处,是同一块内存吗?
不是,只要没有&的,都是完全值拷贝。有&的,也是值拷贝,但拷贝的是内存地址。
问题二:p4和p1有啥关系?
没啥关系,都是独立的结构体实例,不同的内存地址。
还是那句话,只要没有&的,都是完全值拷贝。有&的,也是值拷贝,但拷贝的是内存地址。
问题三:第5处打印出来的地址,和第4处打印出来的地址,有什么关系?
没啥关系,地址都不一样。结论同上。
问题四:第1处和第6处、第7处的内存地址,有啥区别?
它们3都一样,p5和bar函数中的p,内存地址中存储的都是p1的内存地址。尽管外表看来内存地址不一样,但实际指向的内存地址相同。
这种也算值拷贝,虽然存储的是内存地址。
7. 匿名结构体
7.1 介绍
匿名结构体,只是为了快速方便地得到一个结构体实例,而不是使用结构体创建N个实例。
标识符直接使用struct部分结构体本身来作为类型,而不是使用type定义的有名字的结构体的标识符。如下图:
下图定义一个Point变量:
7.2 定义匿名结构体
匿名结构体都是一次性的,用一次后就不能再用了。
且匿名结构体也可以定义为全局或局部。
7.2.1 基本定义
package mainimport "fmt"func main() {// 定义匿名结构体,默认零值可用// 该方式相当于 var Point intvar Point struct {x, y int}// 错误的定义方式// var t1 = struct {} // 这种就相当于 var t1 = int,是不可以的// 可以换成这样就可以var t1 = struct {t string}{}// 短格式定义,struct{ s int }就是数据类型,后面的{}就相当于实例化,里面可以写具体的值,不写就零值t2 := struct{ s int }{1000}fmt.Printf("Point的类型:%T\nPoint的值:%[1]v\n", Point)fmt.Printf("t1的类型:%T\nt1的值:%[1]v\n", t1)fmt.Printf("t2的类型:%T\nt2的值:%[1]v\n", t2)
}
========调试结果========
Point的类型:struct { x int; y int }
Point的值:{100 0}
t1的类型:struct { s float64 }
t1的值:{0}
t2的类型:struct { s int }
t2的值:{1000}
7.2.2 修改值
package mainimport "fmt"func main() {var Point struct {x, y int}// 修改值Point.x = 100fmt.Printf("Point的类型:%T\nPoint的值:%[1]v", Point)
}
========调试结果========
Point的类型:struct { x int; y int }
Point的值:{100 0}
8. 匿名成员(匿名字段)
一般情况下,字段名还是应该见名知义,匿名不便于阅读。
package mainimport "fmt"type Point struct {// 正常的属性定义x, y int// 定义匿名成员,没有名称。但是类型名就是属性名// 但是注意,匿名属性是不能重复出现的intfloat32bool
}func main() {// 初始化结构体实例var p1 = Point{}fmt.Printf("p1 %+v\n", p1)var p2 Pointfmt.Printf("p2 %+v\n", p2)// 手动指定结构体内的值(一定要按顺序对应)p3 := Point{1,2,3,1.1,true,}fmt.Printf("p3 %+v\n", p3)// 按照名称给定值(不用按照顺序也行)p4 := Point{int: 100, bool: false}fmt.Printf("p4 %+v\n", p4) // 打印全部fmt.Println(p4.bool, p4.float32, p4.int) // 打印部分
}
========调试结果========
p1 {x:0 y:0 int:0 float32:0 bool:false}
p2 {x:0 y:0 int:0 float32:0 bool:false}
p3 {x:1 y:2 int:3 float32:1.1 bool:true}
p4 {x:0 y:0 int:100 float32:0 bool:false}
false 0 100
9. 构造函数
9.1 什么是构造函数
Go语言本身没有构造函数,但是我们可以使用结构体初始化的过程来模拟实现构造函数,说简单点,这就是定义结构体实例化的另一种方式。
一般都是定义一个函数,然后该函数返回结构体实例,这就称为该结构体的构造函数(这是一个借鉴的概念)。
习惯上,函数命名为 NewXxx 的形式。
9.2 定义方式
package mainimport "fmt"// 定义结构体
type Animal struct {name stringage int
}// 还可以通过普通函数来构造实例(构造函数,没有实例,就构造一个实例)
func NewAnimal(name string, age int) Animal {a := Animal{name, age}fmt.Printf("%+v %p\n", a, &a)// 返回Animal{}实例return a
}func main() {a := NewAnimal("Tom", 20)fmt.Printf("%+v %p\n", a, &a)
}
========调试结果========
{name:Tom age:20} 0xc000008090
{name:Tom age:20} 0xc000008078
上述构造函数需要注意一个值拷贝的问题,可以使用指针来避免值拷贝。
10. 父子关系构造
动物类包括猫类,猫属于猫类,猫也属于动物类,某动物一定是动物类,但不能说某动物一定是猫类。
将上例中的Animal结构体,使用匿名成员的方式,嵌入到Cat结构体中,看看效果。
10.1 定义方式
package mainimport "fmt"type Animal struct {name stringage int
}type Cat struct {// name string// age int// name和age都在Animal结构体中定义好了,所以可以直接引用Animal // 把匿名结构体Animal,通过匿名成员方式(结构体嵌套),引用进来color string
}func main() {// 为了方便学习,此处就不使用构造函数来演示了// 定义结构体实例c1 := Cat{} // Cat实例化,Animal同时被实例化fmt.Printf("%#v", c1)
}
========调试结果========
main.Cat{Animal:main.Animal{name:"", age:0}, color:""}
这里解释一下结果含义:
(1)main.Cat:表示c1是 main包中的Cat类型(就是结构体)
(2)Animal:字段名称,表示Cat类中嵌入的匿名成员Animal。
(3)main.Animal{name:“”, age:0}, color:“”}:表示字段Animal的值。
- main.Animal:表示值的类型为main包中的Animal类型。
- name和age:表示Animal值中具体的字段。
- color:表示Animal值中具体的字段。
10.2 修改字段属性
package mainimport "fmt"type Animal struct {name stringage int
}type Cat struct {// name string// age int// name和age都在Animal结构体中定义好了,所以可以直接引用Animal // 把匿名结构体Animal,通过匿名成员方式(结构体嵌套),引用进来color string
}func main() {// 为了方便学习,此处就不使用构造函数来演示了// 定义结构体实例c1 := Cat{}fmt.Printf("%#v\n", c1)c1.color = "black"fmt.Printf("%#v\n", c1)c1.Animal.name = "Tom"fmt.Printf("%#v\n", c1)// 和下面比较,其实Animal是可以省略的,属于一种简略写法,必须是匿名成员才可以c1.age++fmt.Printf("%#v\n", c1)// 但是这种写法更加清晰c1.Animal.age++fmt.Printf("%#v\n", c1)
}
========调试结果========
main.Cat{Animal:main.Animal{name:"", age:0}, color:""}
main.Cat{Animal:main.Animal{name:"", age:0}, color:"black"}
main.Cat{Animal:main.Animal{name:"Tom", age:0}, color:"black"}
main.Cat{Animal:main.Animal{name:"Tom", age:1}, color:"black"}
main.Cat{Animal:main.Animal{name:"Tom", age:2}, color:"black"}
c1.age++和c1.Animal.age++,证明了父子关系,子类可以继承父类的属性(不用写父类名称,就可以直接调用父类中的方法)。
在上述代码中,Animal是父类(基类),Cat是子类(派生类)。为什么Animal是父类,因为Animal以匿名成员的方式嵌套在了Cat中。
11. 指针类型receiver
Go语言中,可以为任意类型包括结构体增加方法,形式是 func Receiver 方法名 签名 {函数体} ,这个receiver类似其他语言中的this或self。
receiver必须是一个类型T实例或者类型T的指针,T不能是指针或接口。
this或self如何理解呢?在其他语言中,多数情况下this或self通常指向当前实例本身。
但是注意:代码中的p和p1或p1,都是不一样的,都有自己的内存地址。
11.1 为什么要用指针类型receiver
上面的5. 成员方法,讲的其实就是值类型的receiver。
当方法的receiver是值类型时,如:func (u User) GetName() string,这里的u User就是值类型,这个时候调用GetName() 方法,它使用的其实是User的一个副本,如果数据量很大,那么这个复制过程会占用系统大量的cpu和内存。
使用指针类型的receiver,可以解决这个问题。
11.2 示例
11.2.1 示例一:不使用指针的receiver
11.2.1.1 查询
package mainimport "fmt"type Point struct {x, y int
}func (p Point) getx() int {fmt.Printf("1.1 %+v %p\n", p, &p)p.y = 100fmt.Printf("1.2 %+v %p\n", p, &p)return p.x
}func main() {var p1 = Point{1, 2}fmt.Printf("1 %+v %p\n", p1, &p1) // 1 {x:1 y:2} 0xc0000180a0var p2 = Point{3, 4}fmt.Println(p1.x, p2.x) // 1 3p1.getx() // 1.1 {x:1 y:2} 0xc0000180f0 // 1.2 {x:1 y:100} 0xc0000180f0fmt.Printf("%+v\n", p1) // {x:1 y:2} // 为什么p1的y不是100,因为p1和p拥有不同的内存地址。
}
========调试结果========
1 {x:1 y:2} 0xc0000180a0
1 3
1.1 {x:1 y:2} 0xc0000180f0
1.2 {x:1 y:100} 0xc0000180f0
{x:1 y:2}
为什么上面的代码中,最终p1的y不是200?
因为p1和p都是分别独立的实例,都有自己的内存地址。
所以说,如果只是简单的查询,使用无指针的receiver,没问题,但是如果涉及到修改值的操作,就需要注意副本问题。
再看一个例子:
package mainimport "fmt"type Point struct {x, y int
}func (p Point) getx() int { // getx(p1) int {}fmt.Printf("%T %+[1]v %p\n", p, &p)return p.x
}func main() {var p1 = Point{11, 21}fmt.Println(p1.getx()) // 传递 p1 的一个副本给 getx 方法的接收者// 这样是所以能成功,是go的语法糖,实际传递的还是一个结构体实例fmt.Println((&p1).getx()) // 传递 p1 的地址的副本给 getx 方法,p会去这个地址中复制一份数据,其实和p1.getx()是一样的
}
========调试结果========
main.Point {x:11 y:21} 0xc0000180a0
11
main.Point {x:11 y:21} 0xc0000180f0
11
为什么p1.getx()和(&p1).getx()的内存地址不同?
其实很好分辨,直接看func (p Point) getx() int,这里的p Point是一个值类型的接收者,不管是p1.getx()还是(&p1).getx(),getx方法都会复制一份p1的值给到p。
这个(&p1).getx()只是为了演示值类型接收者会产生副本的问题,实际没有特殊含义。
11.2.1.2 修改
package mainimport "fmt"type Point struct {x, y int
}func (p Point) setX(v int) { // 相当于 setX(p Point, v int)fmt.Printf("%T %+[1]v %p\n", p, &p)p.x = vfmt.Printf("%T %+[1]v %p\n", p, &p)
}func main() {var p1 = Point{11, 21}fmt.Printf("原始结构体实例p1 %+v %p\n", p1, &p1)fmt.Println("------------------------------")p1.setX(600)fmt.Printf("600 %+v %p\n", p1, &p1)fmt.Println("------------------------------")(&p1).setX(700)fmt.Printf("700 %+v %p\n", p1, &p1)
}
========调试结果========
原始结构体实例p1 {x:11 y:21} 0xc000110050
------------------------------
main.Point {x:11 y:21} 0xc000110090
main.Point {x:600 y:21} 0xc000110090
600 {x:11 y:21} 0xc000110050
------------------------------
main.Point {x:11 y:21} 0xc000110100
main.Point {x:700 y:21} 0xc000110100
700 {x:11 y:21} 0xc000110050
11.2.2 示例二:使用指针的receiver
11.2.2.1 查询
package mainimport "fmt"type Point struct {x, y int
}func (p Point) getX() int {fmt.Printf("%T %+[1]v %p\n", p, &p)return p.x
}// 实际工作中,还是使用指针receiver更加节省内存与cpu,因为不会产生副本
// 或者说要修改原始结构体实例中的值时,就必须使用这种方式了。
func (p *Point) getY() int {fmt.Printf("%T %+[1]v %[1]p", p)return p.y
}func main() {var p1 = Point{11, 21}fmt.Printf("1 %+v %p\n", p1, &p1)fmt.Println((&p1).getY())fmt.Println(p1.getY()) // 为啥非指针类型也能调用?也是go的语法糖实现的
}
========调试结果========
1 {x:11 y:21} 0xc0000180a0
*main.Point &{x:11 y:21} 0xc0000180a021
*main.Point &{x:11 y:21} 0xc0000180a021
11.2.2.2 修改
值类型结构体修改
package mainimport "fmt"type Point struct {x, y int
}func (p Point) getX() int {fmt.Printf("%T %+[1]v %p\n", p, &p)return p.x
}func (p *Point) getY() int {fmt.Printf("%T %+[1]v %[1]p", p)return p.y
}func (p Point) setX(v int) { // 相当于 setX(p Point, v int)fmt.Printf("%T %+[1]v %p\n", p, &p)p.x = vfmt.Printf("%T %+[1]v %p\n", p, &p)
}func main() {var p1 = Point{11, 21}fmt.Printf("1 %+v %p\n", p1, &p1)fmt.Println("------------------------------")p1.setX(400) // 通过结构体实例调用方法修改值,会产生副本fmt.Printf("1 %+v %p\n", p1, &p1) // 通过输出可以看到,p1.setX(400)修改的只是副本p的值,原始p1结构体实例本身值无变化。
}
========调试结果========
1 {x:11 y:21} 0xc0000a6070
------------------------------
main.Point {x:11 y:21} 0xc0000a60c0
main.Point {x:400 y:21} 0xc0000a60c0
1 {x:11 y:21} 0xc0000a6070
指针类型修改
package mainimport "fmt"type Point struct {x, y int
}func (p Point) setX(v int) { // 相当于 setX(p Point, v int)fmt.Printf("%T %+[1]v %p\n", p, &p)p.x = vfmt.Printf("%T %+[1]v %p\n", p, &p)
}func (p *Point) setY(v int) {fmt.Printf("setY修改前 %T %+[1]v %p\n", p, p) // 为什么不是&p,因为p是个指针类型的变量,存储的p1的内存地址p.y = vfmt.Printf("setY修改后 %T %+[1]v %p\n", p, p)
}func main() {var p1 = Point{11, 21}fmt.Printf("原始结构体实例p1 %+v %p\n", p1, &p1)fmt.Println("------------------------------")p1.setY(600) // 语法糖实现内存地址传递。fmt.Printf("600 %+v %p\n", p1, &p1)fmt.Println("------------------------------")(&p1).setY(700)fmt.Printf("700 %+v %p\n", p1, &p1)
}
========调试结果========
原始结构体实例p1 {x:11 y:21} 0xc0000b6070
------------------------------
setY修改前 *main.Point &{x:11 y:21} 0xc0000b6070
setY修改后 *main.Point &{x:11 y:600} 0xc0000b6070
600 {x:11 y:600} 0xc0000b6070
------------------------------
setY修改前 *main.Point &{x:11 y:600} 0xc0000b6070
setY修改后 *main.Point &{x:11 y:700} 0xc0000b6070
700 {x:11 y:700} 0xc0000b6070
通过上述结果可以看到到指针类型接收者,修改都是修改的原始结构体数据,不会发生值拷贝。
11.3 receiver使用总结
- 非指针类型receiver
查询:传递结构体实例或结构体实例指针都可以。
修改:传递结构体实例或结构体实例指针都可以,但是,会产生原始结构体实例的副本,有值拷贝过程,且无法修改到原始结构体,只能修改副本结构体实例。- 指针类型receiver
查询:传递结构体实例或结构体实例指针都可以。
修改:传递结构体实例或结构体实例指针都可以,不会有值拷贝过程,修改的是原始结构体实例本身。
仅仅查询的话,返回的数据量不大,使不使用指针接收者都行。
但修改的话,一定要先搞清楚实际需求,再来判断是否需要使用指针。
12. 深浅拷贝
1. 浅拷贝(Shallow Copy)
影子拷贝,也叫浅拷贝。遇到引用类型时,仅仅复制一个引用而已。
或者这样理解:创建一个新的变量,但这个新变量和原始变量指向相同的底层数据。这意味着对新变量的修改也会影响到原始变量,因为它们实际上是同一个数据。
2. 深拷贝(Deep Copy)
创建一个新的变量,并且复制原始变量的所有数据到这个新变量。这样,新变量和原始变量是完全独立的,修改新变量不会影响原始变量。
值类型的数据默认是深拷贝。