1. 结构体介绍
1.1 什么是结构体
结构体(struct)是一种用户定义的类型,它由一系列的字段组成,每个字段都有自己的名称和类型。
结构体也是值类型的,就算加了指针也是,只不过是复制的内存地址。
1.2 为什么要用结构体
结构体是 Go 语言中一种非常重要的数据类型,它允许你将多个不同类型的数据组合成一个单一的数据结构。这对于创建复杂的数据模型和对象非常有用。
2. 定义结构体
使用type关键字定义结构体,可以把结构体看做类型来使用。
必须指定结构体的字段(属性)名称和类型。
2.1 type关键字
在go中,type关键字主要用于定义新的类型或者为现有类型定义别名。
2.1.1 定义类型
// 快捷方式:NewType.struct
type NewType struct {
Field1 Type1
Field2 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 int
name, addr string
score float32 // 这玩意儿可以叫属性、字段、成员、变量等,叫法很多
}
func main() {
type User struct { // 局部结构体
id int
name, addr string
score float32
}
}
2.4 定义结构体实例(初始化)
上面type User struct只是创建了一个新的结构体,是一类抽象事务的集合。
要想在代码中使用,还需要通过长短格式声明,使得结构体具体化。
其实还有另一种初始化方式,那就是构造函数,后续会演示。
怎么理解这个抽象?就好比int类型,它也是一类相同特征事务的抽象,你没有办法直接操作int,只能通过定义int实例来操作。
2.4.1 var声明(常用)
结构体中也是零值可用的。
package main
import "fmt"
type User struct {
id int // 默认值为0
name, 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 main
import "fmt"
type User struct {
id int
name, addr string
score 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 main
import "fmt"
type User struct {
id int
name, addr string
score 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 main
import "fmt"
type User struct {
id int
name, addr string
score 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 int
name, addr string
// 属性首字母大写,包外可见
Score float32
}
// 全局结构体:结构体名称首字母小写,包内可见
type user struct {
id int
name, addr string
score float32
}
func main() {
// 局部结构体,main函数内可见
type User struct {
id int
name, addr string
score float32
}
}
4. 结构体的查询与修改
4.1 查询结构体字段
package main
import "fmt"
type User struct {
id int
name, addr string
score 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 main
import (
"fmt"
)
type User struct {
id int
name, addr string
score float32
}
func main() {
u1 := User{
score: 98.5,
name: "tom",
id: 8,
addr: "四川",
}
u1.score += 1.5
fmt.Println(u1.score)
}
========调试结果========
100
5. 成员方法(字段方法)
只要是通过type自定义的类型,都可以有方法。
5.1 什么是成员方法
在Go语言中,结构体(struct)是一种聚合的数据类型,它允许你将多个不同类型的数据项组合成一个单一的实体。结构体可以拥有自己的方法,这些方法称为结构体的成员方法。
还有一点,这个成员指的就是结构体里面的字段。
5.2 定义成员方法
要为结构体定义成员方法,需要使用特殊的方法接收者语法。方法接收者看起来像一个参数列表,但它位于方法名之前,并且它指定了方法绑定到的类型。
5.2.1 普通函数
这里先用普通函数来演示,如何查询结构体实例中的某些字段。
package main
import (
"fmt"
)
type User struct {
id int
name, addr string
score 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 main
import "fmt"
type User struct {
id int
name, addr string
score 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 main
import "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 main
import "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 main
import "fmt"
type Point struct {
x, y int
}
func main() {
p2 := &Point{}
p3 := new(Point)
fmt.Printf("%T %[1]v\n", p3)
p2.x += 100
p3.y += 100
fmt.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 main
import "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 main
import "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 := &p1
fmt.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 main
import "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 := p1
fmt.Printf("2 p2的类型:%T|p2的值: %+[1]v|p2的内存地址:%[2]p\n", p2, &p2)
p3 := &p1
fmt.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 main
import "fmt"
func main() {
// 定义匿名结构体,默认零值可用
// 该方式相当于 var Point int
var 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 main
import "fmt"
func main() {
var Point struct {
x, y int
}
// 修改值
Point.x = 100
fmt.Printf("Point的类型:%T\nPoint的值:%[1]v", Point)
}
========调试结果========
Point的类型:struct { x int; y int }
Point的值:{100 0}
8. 匿名成员(匿名字段)
一般情况下,字段名还是应该见名知义,匿名不便于阅读。
package main
import "fmt"
type Point struct {
// 正常的属性定义
x, y int
// 定义匿名成员,没有名称。但是类型名就是属性名
// 但是注意,匿名属性是不能重复出现的
int
float32
bool
}
func main() {
// 初始化结构体实例
var p1 = Point{}
fmt.Printf("p1 %+v\n", p1)
var p2 Point
fmt.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 main
import "fmt"
// 定义结构体
type Animal struct {
name string
age 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 main
import "fmt"
type Animal struct {
name string
age 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 main
import "fmt"
type Animal struct {
name string
age 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 main
import "fmt"
type Point struct {
x, y int
}
func (p Point) getx() int {
fmt.Printf("1.1 %+v %p\n", p, &p)
p.y = 100
fmt.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} 0xc0000180a0
var p2 = Point{3, 4}
fmt.Println(p1.x, p2.x) // 1 3
p1.getx() // 1.1 {x:1 y:2} 0xc0000180f0 // 1.2 {x:1 y:100} 0xc0000180f0
fmt.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 main
import "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 main
import "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 = v
fmt.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 main
import "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 main
import "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 = v
fmt.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 main
import "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 = v
fmt.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 = v
fmt.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)
创建一个新的变量,并且复制原始变量的所有数据到这个新变量。这样,新变量和原始变量是完全独立的,修改新变量不会影响原始变量。
值类型的数据默认是深拷贝。