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

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

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

12. 深浅拷贝

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

  • 15
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值