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() 所以还是地址传递