Go的对象和方法(struct)

本文深入探讨Go语言中的struct,包括内存布局、默认值、值拷贝与指针操作。详细解释了struct的方法定义,强调了传值与传地址调用方法的差异,并通过实例解析了方法内部的内存变化。此外,还提到了tag标签在序列化与反序列化中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

struct 内存布局


struct 是值类型,直接指向 struct 的内存空间

字段=属性=field
没赋值的字段初始化为它们相应的 0 值
map、slice赋值前要 make

struct 默认是值拷贝

在这里插入图片描述
改变一个实例的字段不会影响另一个。

monster2 := monster1

这句代码内存变化如下:直接拷贝牛魔王,再在上面修改
在这里插入图片描述

结构体指针

在这里插入图片描述

	var person *Person = &Person{}

	//因为person 是一个指针,因此标准的访问字段的方法
	// (*person).Name = "scott"
	// go的设计者为了程序员使用方便,也可以 person.Name = "scott"
	// 原因和上面一样,底层会对 person.Name = "scott" 进行处理, 会加上 (*person)
	(*person).Name = "scott"
	person.Name = "scott~~"

	(*person).Age = 88
	person.Age = 10
	fmt.Println(*person)

(*person).Name = “scott” -----底层实现仍然是它
person.Name = “scott~~”-----方便程序员的简便写法

在这里插入图片描述

细节

(1)所有字段在内存中是连续分配的

package main 
import "fmt"

//结构体
type Point struct {
	x int
	y int
}

//结构体
type Rect struct {
	leftUp, rightDown Point
}

//结构体
type Rect2 struct {
	leftUp, rightDown *Point
}

func main() {

	r1 := Rect{Point{1,2}, Point{3,4}} 

	//r1有四个int, 在内存中是连续分布
	//打印地址
	fmt.Printf("r1.leftUp.x 地址=%p r1.leftUp.y 地址=%p r1.rightDown.x 地址=%p r1.rightDown.y 地址=%p \n", 
	&r1.leftUp.x, &r1.leftUp.y, &r1.rightDown.x, &r1.rightDown.y)

	//r2有两个 *Point类型,这个两个*Point类型的本身地址也是连续的,
	//但是他们指向的地址不一定是连续

	r2 := Rect2{&Point{10,20}, &Point{30,40}} 

	//打印指针本身的地址
	fmt.Printf("r2.leftUp 本身地址=%p r2.rightDown 本身地址=%p \n", 
		&r2.leftUp, &r2.rightDown)

	//他们指向的地址不一定是连续..., 这个要看系统在运行时是如何分配
	fmt.Printf("r2.leftUp 指向地址=%p r2.rightDown 指向地址=%p \n", 
		r2.leftUp, r2.rightDown)

}

指针本身地址连续,指向的地址不一定连续

(2)类型转换

(3)
在这里插入图片描述
(4)序列化与反序列化在这里插入图片描述
服务器和客户端使用的字段名,大小写不一样,需要转换。使用 tag 标签,通过反射可以得到 tag 标签。
在这里插入图片描述

struct 的方法

方法和指定的数据类型相绑定,
自定义类型也可以有方法。

细节
(1)调用方法,可以传值(拷贝),可以传地址

传值(拷贝):地址是不一样的,也就是说它们在内存里的两个独立位置。修改方法里的字段,不会影响主函数声明的字段,也就是说,这两者毫无关系

传地址:不必多说,地址是一样的。效率更高,因为值拷贝在方法的栈里要占用空间,而传地址不用再开辟空间,修改都在原数据上修改

package main

import (
	"fmt"	
)

type Circle struct {
	radius float64
}

//2)声明一个方法area和Circle绑定,可以返回面积。

func (c Circle) area() float64 {
	fmt.Printf("func c address=%p\n", &c)
	return 3.14 * c.radius * c.radius
}

//为了提高效率,通常我们方法和结构体的指针类型绑定
func (c *Circle) area2() float64 {
	//因为 c是指针,因此我们标准的访问其字段的方式是 (*c).radius
	//return 3.14 * (*c).radius * (*c).radius
	// (*c).radius 等价  c.radius 
	fmt.Printf("c 是  *Circle 指向的地址=%p", c)
	c.radius = 10
	return 3.14 * c.radius * c.radius
}
 
func main() {
// 1)声明一个结构体Circle, 字段为 radius
// 2)声明一个方法area和Circle绑定,可以返回面积。
// 3)提示:画出area执行过程+说明

	//创建一个Circle 变量
	// var c Circle 
	// c.radius = 4.0
	// res := c.area()
	// fmt.Println("面积是=", res)

	//创建一个Circle 变量
	var c Circle 
	fmt.Printf("main c 结构体变量地址 =%p\n", &c)
	c.radius = 7.0
	//res2 := (&c).area2()
	//编译器底层做了优化  (&c).area2() 等价 c.area()
	//因为编译器会自动的给加上 &c
	res2 := c.area()
	
	res2 = c.area2()
	fmt.Println("面积=", res2)
	fmt.Println("c.radius = ", c.radius) //10


}



(2)自定义类型也可以有方法。

package main

import (
	"fmt"
)

/*
Golang中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,
都可以有方法,而不仅仅是struct, 比如int , float32等都可以有方法
*/

type integer int

func (i integer) print() {
	fmt.Println("i=", i)
}

//编写一个方法,可以改变i的值
func (i *integer) change() {
	*i = *i + 1
}

type Student struct {
	Name string
	Age  int
}

//给*Student实现方法String()
func (stu *Student) String() string {
	str := fmt.Sprintf("Name=[%v] Age=[%v]", stu.Name, stu.Age)
	return str
}

func main() {
	var i integer = 10
	i.print()
	i.change()
	fmt.Println("i=", i)

	//定义一个Student变量
	stu := Student{
		Name: "tom",
		Age:  20,
	}
	//如果你实现了 *Student 类型的 String方法,就会自动调用
	fmt.Println(&stu)
}

给 student 定义了一个 String( )方法,fmt 函数会对实现了 String( ) 接口,直接调用

方法和函数的区别:
在这里插入图片描述
重点细节

package main

import (
	"fmt"	
)

type Person struct {
	Name string
} 

//函数
//对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然

func test01(p Person) {
	fmt.Println(p.Name)
}

func test02(p *Person) {
	fmt.Println(p.Name)
}

//对于方法(如struct的方法),
//接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以

func (p Person) test03() {
	p.Name = "jack"
	fmt.Printf("test03 p_address=%p\n", &p)
	fmt.Println("test03() name=", p.Name) // jack
	fmt.Println()
}

func (p *Person) test04() {
	p.Name = "mary"
	fmt.Println("test04() =", p.Name) // mary
}

func main() {

	p := Person{"tom"}
	test01(p)
	test02(&p)

	p.test03()
	fmt.Printf("main0 测试地址 p_address=%p\n", &p)
	fmt.Println("main1 测试值 p.name=", p.Name) // tom
	(&p).test03() // 从形式上是传入地址,但是本质仍然是值拷贝


	(&p).test04()
	fmt.Println("main2 测试值 p.name=", p.Name) // mary
	p.test04() // 等价 (&p).test04 , 从形式上是传入值类型,但是本质仍然是地址拷贝

}
tom
tom
test03 p_address=0xc000116080
test03() name= jack

main0 测试地址 p_address=0xc000116050
main1 测试值 p.name= tom
test03 p_address=0xc0001160b0
test03() name= jack

test04() = mary
main2 测试值 p.name= mary
test04() = mary

(&p).test03() :
仍然是传值,在主函数go底层将 (&p).test03() 转换成 p.test03() ------猜的,所以还是值传递,会开辟内存空间存储新的 p 。
可以看到:调用两次 test03() ,地址两次增加了 0x30,每调一次,在 test03 栈空间就会开辟一个 p main 中的 p 和 test03中的 p 地址不一样,证明确实是值传递

p.test04() :
因为方法声明的是 func (p *Person) test04() 所以还是地址传递

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值