Go 1.19.4 结构体-Day 09

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使用总结

  1. 非指针类型receiver
    查询:传递结构体实例或结构体实例指针都可以。
    修改:传递结构体实例或结构体实例指针都可以,但是,会产生原始结构体实例的副本,有值拷贝过程,且无法修改到原始结构体,只能修改副本结构体实例。
  2. 指针类型receiver
    查询:传递结构体实例或结构体实例指针都可以。
    修改:传递结构体实例或结构体实例指针都可以,不会有值拷贝过程,修改的是原始结构体实例本身。

仅仅查询的话,返回的数据量不大,使不使用指针接收者都行。
但修改的话,一定要先搞清楚实际需求,再来判断是否需要使用指针。

12. 深浅拷贝

1. 浅拷贝(Shallow Copy)
影子拷贝,也叫浅拷贝。遇到引用类型时,仅仅复制一个引用而已。
或者这样理解:创建一个新的变量,但这个新变量和原始变量指向相同的底层数据。这意味着对新变量的修改也会影响到原始变量,因为它们实际上是同一个数据。
2. 深拷贝(Deep Copy)
创建一个新的变量,并且复制原始变量的所有数据到这个新变量。这样,新变量和原始变量是完全独立的,修改新变量不会影响原始变量。
值类型的数据默认是深拷贝。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/388420.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024年的AI人工智能风口是Python?一篇文章告诉你为什么!

Python是一种面向对象的、解释型的、通用的、开源的脚本编程语言,它之所以非常流行,我认为主要有三点原因: 1.Python 简单易用,学习成本低,看起来非常干净; 2.Python 标准库和第三库众多,功能…

stack和list

前言 stack和list的使用就不讲了&#xff0c;讲一下模拟实现&#xff0c;然后讲一下deque&#xff0c;最后讲一下优先队列 1. stack的模拟实现 template<class T,class container>//这个container是vector&#xff0c;或者list或者deque&#xff08;后面会说&#xff0…

测试用例之等价类划分、边界值法

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 一、测试用例/案例 1、定义&#xff1a;是在测试执行之前&#xff0c;由测试人员编写的指导测试过程的重要文档&#xff0c;主要包括&#xff1a;用例编号、测试目…

Windows安装服务

&#xff08;1&#xff09;下载设置工具 https://nssm.cc/release/nssm-2.24.zip &#xff08;2&#xff09;根据自己系统选择工具32/64版本 &#xff08;3&#xff09;选择版本后进入文件夹&#xff0c;打开cmd命令窗口输入命令&#xff1a;nssm.exe install &#xff08;4&a…

阿里云实时计算Flink在多行业的应用和实践

摘要&#xff1a;本文整理自 Flink Forward Asia 2023 中闭门会的分享。主要分享实时计算在各行业的应用实践&#xff0c;对回归实时计算的重点场景进行介绍以及企业如何使用实时计算技术&#xff0c;并且提供一些在技术架构上的参考建议。内容分为以下四个部分&#xff1a; 业…

SQL Server 端口配置

目录 默认端口 更改端口 示例&#xff1a;更改 TCP 端口 示例&#xff1a;验证端口设置 远程连接测试 示例&#xff1a;使用 telnet 测试连接 配置防火墙 示例&#xff1a;Windows 防火墙设置 远程连接测试 示例&#xff1a;使用 telnet 测试连接 默认端口 TCP/IP: …

jmeter 重试机制

一、功能实现 我们在测试过程中&#xff0c;请求接口可能是因为请求超时&#xff0c;或者接口异常失败&#xff0c;导致整个测试链路验证失败&#xff0c;jmeter重试机制&#xff0c;这个时候就可以避免上述问题发生 二、配置 1、添加线程组 首先&#xff0c;确保你已经在测…

ctfshow~菜狗杯 你会异或吗

下载文件附件得到一张png图片&#xff0c;用010打开看一下 全是乱码&#xff0c;结合题目提示 你会异或吗 和 神秘数字:0x50 我们试一下图片十六进制值异或十六进制0x50 打开010然后工具–>十六进制运算–>二进制异或 输入0x50 得到一张新的图片 然后到微信里的图片文字提…

函数模板和类模板

前言&#xff1a;各位老铁好&#xff0c;今天来分享函数模板和类模板的知识&#xff0c;这个算是一个小知识&#xff0c;但这个小知识非常重要&#xff0c;相信学C的各位老铁一定听过STL这个名词&#xff0c;那么STL是什么呢&#xff1f;它与我们今天分享的这个函数模板和类模板…

《Milvus Cloud向量数据库指南》——图像数据:ResNet50与图像及视频搜索的深度解析

图像数据:ResNet50与图像及视频搜索的深度解析 在当今信息爆炸的时代,图像和视频作为最直观、最富表现力的媒体形式之一,其搜索与检索技术显得尤为重要。无论是科研探索、艺术创作还是日常娱乐,人们越来越依赖于高效的图像和视频搜索工具来快速定位所需内容。其中,ResNet…

高频JMeter软件测试面试题

近期&#xff0c;有很多粉丝在催更关于Jmeter的面试题&#xff0c;索性抽空整理了一波&#xff0c;以下是一些高频JMeter面试题&#xff0c;拿走不谢~ 一、JMeter的工作原理 JMeter就像一群将请求发送到目标服务器的用户一样&#xff0c;它收集来自目标服务器的响应以及其他统计…

光伏气象站:绿色能源时代的守护者

光伏气象站&#xff0c;顾名思义&#xff0c;是结合了光伏发电技术与气象监测功能的创新设备。 它不仅能够利用太阳能自发电&#xff0c;实现绿色能源自给自足&#xff0c;还能精准监测并记录温度、湿度、风速、风向等关键气象参数。这些数据对于评估光伏系统的发电效率、优化电…

Java后端初开-->架构师学习路线!无偿分享!让你少走弯路

由于平台篇幅原因&#xff0c;很多java面试资料内容展示不了&#xff0c;需要的java面试宝典的伙伴们转发文章关注后&#xff0c;扫描下方二维码免费获取:

WebSocket 协议与 HTTP 协议、定时轮询技术、长轮询技术

目录 1 为什么需要 WebSocket&#xff1f;2 WebSocket2.1 采用 TCP 全双工2.2 建立 WebSocket 连接2.3 WebSocket 帧 3 WebSocket 解决的问题3.1 HTTP 存在的问题3.2 Ajax 轮询存在的问题3.3 长轮询存在的问题3.4 WebSocket 的改进 参考资料&#xff1a; 为什么有 h…

【调试笔记-20240731-Linux-Wordpress 添加 wp-weixin 插件支持微信用户扫码注册登录】

调试笔记-系列文章目录 调试笔记-20240731-Linux-Wordpress 添加 wp-weixin 插件支持微信用户扫码注册登录 文章目录 调试笔记-系列文章目录调试笔记-20240731-Linux-Wordpress 添加 wp-weixin 插件支持微信用户扫码注册登录 前言一、调试环境操作系统&#xff1a;Windows 10 …

有趣的PHP小游戏——猜数字

猜数字 这个游戏会随机生成一个1到100之间的数字&#xff0c;然后你需要猜测这个数字是什么。每次你输入一个数字后&#xff0c;程序会告诉你这个数字是“高了”还是“低了”&#xff0c;直到你猜对为止&#xff01; 使用指南&#xff1a; 代码如下&#xff0c;保存到一个p…

排序算法:快速排序,golang实现

目录 前言 快速排序 代码示例 1. 算法包 2. 快速排序代码 3. 模拟程序 4. 运行程序 5. 从大到小排序 快速排序的思想 快速排序的实现逻辑 1. 选择基准值 (Pivot) 2. 分区操作 (Partition) 3. 递归排序 循环次数测试 假如 10 条数据进行排序 假如 20 条数据进行…

DC-7靶机通关

今天咱们来学习第七个靶机&#xff01;&#xff01;&#xff01; 1实验环境 攻击机&#xff1a;kali2023.2 靶机&#xff1a;DC-7 2.1主机发现 2.2端口扫描 依旧是开了两个端口&#xff0c;一个 22 一个 80 &#xff01;&#xff01;&#xff01; 3.1查看对方网页 在这里我…

2024年必备技能:小红书笔记评论自动采集,零基础也能学会的方法

摘要&#xff1a; 面对信息爆炸的2024年&#xff0c;小红书作为热门社交平台&#xff0c;其笔记评论成为市场洞察的金矿。本文将手把手教你&#xff0c;即便编程零基础&#xff0c;也能轻松学会利用Python自动化采集小红书笔记评论&#xff0c;解锁营销新策略&#xff0c;提升…

redis的集群(高可用)

redis集群的三种模式&#xff1a; 主从复制 奇数 三台 一主两从 哨兵模式 3 一主两从 cluster集群 六台 主从复制&#xff1a;和mysql的主从复制类似&#xff0c;主可以写&#xff0c;写入主的数据通过RDB方式把数据同步到从服务器&#xff0c;从不能更新到主&#xff0c;也…