go语言--笔记--Go语言结构体的方法

张开发
2026/6/9 14:26:00 15 分钟阅读
go语言--笔记--Go语言结构体的方法
一、Go语言结构体的方法在Go语言中方法是绑定到特定类型包括结构体的函数。它赋予了类型“行为”是面向对象编程风格的体现但Go采用组合而非继承。本节将深入探讨结构体方法的所有方面包括语法、底层机制、方法集、接口实现、性能考量及常见陷阱。1. 方法的基本概念与定义方法声明类似于普通函数但在函数名之前增加了一个接收者参数type Person struct { Name string Age int } // 值接收者方法 func (p Person) Greet() string { return Hello, p.Name } // 指针接收者方法 func (p *Person) SetAge(age int) { p.Age age }接收者可以是任何自定义类型不仅仅是结构体例如type MyInt int func (m MyInt) Double() MyInt { return m * 2 }2 接收者的类型值接收者 vs 指针接收者值接收者方法内部操作的是接收者的副本。方法内对接收者字段的修改不会影响原始变量除非字段本身是指针或引用类型(这里可以补充个小知识就是方法或者函数甚至是for range其实他们的传的参数遍历的item都是浅拷贝注意这里说的浅拷贝其实就是复制了栈上的那个变量的空间过去从而形成的副本。对于值类型复制栈中的空间存储其实就是复制了这个变量形成一个新变量跟旧的毫无关系是一个副本。对于引用类型复制栈中的空间存储其实就是复制了具体指向的值的地址到一个新的变量里这个变量其实也是一个副本因为两者在栈上的空间是不一样的但是但是因为是引用类型里面存储的地址指向的值是同一个所以会发生相互影响的情况)。指针接收者方法内部操作的是接收者的地址可以直接修改原始变量。本质区别方法调用时接收者作为第一个参数传递。值接收者传递的是整个结构体的拷贝指针接收者传递的是指针8字节。因此指针接收者通常更高效尤其是当结构体较大时。选择准则经典原则如果需要修改接收者的状态必须用指针接收者。如果接收者包含sync.Mutex或其他不可复制的字段必须用指针接收者。如果接收者很大例如包含大型缓冲区用指针接收者避免复制开销。如果接收者是小的基本类型如int值接收者可能更简单。当不确定时优先使用指针接收者以保持一致性并避免意外修改问题。值接收者的完整调用链type Point struct { X, Y float64 } func (p Point) Move(dx, dy float64) Point { //这里的p是一个副本由于是值类型这里的p发生改变也不会影响main中的p return Point{p.X dx, p.Y dy} } func main() { p : Point{1, 2} newP : p.Move(3, 4) // 发生了什么 }指针接收者的底层实现:func (p *Point) Scale(factor float64) { //指针类型这里的p指向的数据空间和main中的p指向的数据空间一样修改会相互影响 p.X * factor p.Y * factor } func main() { p : Point{1, 2} p.Scale(2) // 自动取址 p }方法调用的自动化转换规则type Item struct { Value int } func (i Item) GetValue() int { return i.Value } func (i *Item) SetValue(v int) { i.Value v } func main() { // 规则1值变量可以调用指针方法自动取址 var i1 Item i1.SetValue(10) // 编译为 (i1).SetValue(10) // 规则2指针变量可以调用值方法自动解引用 i2 : Item{} i2.GetValue() // 编译为 (*i2).GetValue() // 规则3不可寻址的值不能调用指针方法 Item{}.SetValue(10) // 编译错误cannot call pointer method on Item literal // 规则4nil指针可以调用值方法但panic风险 var i3 *Item i3.GetValue() // 等价于 (*i3).GetValue()解引用nil会panic }不可寻址的表达式// 这些不能调用指针方法 Item{42}.SetValue(10) // 字面量 GetItem().SetValue(10) // 函数返回值临时值 map[key].SetValue(10) // map索引返回值 //slice[i]的本质是 item : slice[i],item是一个副本是一个临时值临时值不能取地址 slice[i].SetValue(10) // 切片索引如果slice是表达式 // 这些可以 item : Item{42} item.SetValue(10) // 变量可寻址 items : []Item{{42}} items[0].SetValue(10) // 切片元素可寻址 m : map[string]Item{a: {42}} m[a].SetValue(10) // 错误map值不可寻址 // 必须item : m[a]; item.SetValue(10); m[a] item3方法的本质3.1方法只是带接收者的函数Go的方法在底层并非面向对象意义上的绑定而是一种语法糖package main type Rectangle struct { Width, Height float64 } // 方法声明 func (r Rectangle) Area() float64 { return r.Width * r.Height } // 编译器实际上将其转换为 // func Rectangle.Area(r Rectangle) float643.2 方法值的底层表示r : Rectangle{3, 4} // 方法值将方法和接收者绑定 areaFunc : r.Area // 底层实现伪代码 type MethodValue struct { Fn func(Rectangle) float64 // 方法地址 Receiver Rectangle // 接收者副本值接收者 } // 调用 areaFunc() 实际上是 MethodValue.Fn(MethodValue.Receiver)内存验证package main import ( fmt reflect unsafe ) type Counter struct { count int } func (c Counter) Value() int { return c.count } func main() { c : Counter{42} // 获取方法值 methodValue : c.Value // 方法值实际上是闭包在go版本17以前闭包放在栈中占用16字节两个指针 // 在go版本17以后闭包放在堆中占用8字节一个指针只存储这个闭包的地址 fmt.Println(unsafe.Sizeof(methodValue)) // 查看方法值的内部表示 ptr : (*[2]unsafe.Pointer)(unsafe.Pointer(methodValue)) fmt.Printf(代码指针: %p\n, ptr[0]) // 指向 Value 函数 fmt.Printf(接收者指针: %p\n, ptr[1]) // 指向 c 的副本 }4方法集接口实现的底层机制4.1 方法集的定义与计算方法集计算规则接收者类型包含的方法T值所有值接收者方法*T指针所有值接收者方法 所有指针接收者方法type Printer interface { Print() Debug() } type Document struct { Content string } func (d Document) Print() { /* 值接收者 */ } func (d *Document) Debug() { /* 指针接收者 */ } // 方法集分析 // Document 的方法集{Print} 只有值接收者方法 // *Document 的方法集{Print, Debug} 值指针方法都包含为什么指针类型包含值方法// 底层实现Go自动生成包装方法 // 对于 func (d Document) Print()编译器为 *Document 生成 func (d *Document) Print() { // 自动生成 (*d).Print() // 解引用后调用值方法 }但是大家在这需要区分清楚就是方法集这个概念是落在接口实现这边的跟实际使用是有区别的。实际使用的时候不管你实例变量是引用类型还是值类型对于引用类型的方法和值类型的方法都是可以调用的因为前面说过有语法糖会自动寻址或者解引用。但是对于方法集来说是按照上面表格的规则来的。也可以看看下面的具体例子理解。type Printer interface { Print() } type Document struct{} // 值接收者 func (d Document) Print() {} // 指针接收者 // func (d *Document) Print() {} func main() { var d Document // 情况1值接收者实现 var p1 Printer d // ✅ 可以 var p2 Printer d // ✅ 也可以自动解引用 // 情况2如果 Print 是 *Document 的方法指针接收者 // var p3 Printer d // ❌ 错误Document 没有 Print 方法 //为什么这里的d不能像调用方法那样取地址然后调用呢 //因为接口的改动是不能影响原本的值的所以对于var p3 Printer d实际上是做了浅拷贝的具体如下 temp : d // 第一步复制 d 的值接口要独立拥有数据 p.data temp // 第二步存副本的地址所以这个时候就不是原本的结构体了 var p4 Printer d // ✅ 只有指针可以 }一句话总结Go 方法调用会自动转换值调指针方法自动取指针调值方法自动解*。但接口实现时只有指针接收者值类型不实现接口

更多文章