【go】方法与函数区别,函数的内联与逃逸分析

#新星杯·14天创作挑战营·第10期#

方法与函数

区别

1.定义方式不同
  • 函数是包级别的代码,独立存在,不依赖别的类型,可以独立调用
// 普通函数
func Add(a, b int) int {
    return a + b
}
  • 方法是绑定到某个类型的函数,依赖某个类型,只能通过某个类型调用,定义时要指定接收者receiver,表明方法属于哪个类型
type Point struct {
    X, Y int
}

// Point 类型的方法
func (p Point) Distance() float64 {
    return math.Sqrt(float64(p.X*p.X + p.Y*p.Y))
}

2.调用方式不同
  • 函数调用时,直接调用函数名即可,不需要指定接收者
sum := Add(3, 5) // 函数调用
  • 方法调用时,需要指定接收者,即方法所属的对象,通过**变量.方法名()**调用
p := Point{X: 3, Y: 4}
dist := p.Distance() // 方法调用
3.接收者
  • 值接收者:方法操作的是接收者的副本
func (p Point) Move(dx, dy int) {
    p.X += dx // 修改的是副本,不影响原数据
    p.Y += dy
}

p := Point{X: 1, Y: 2}
p.Move(3, 4) // p.X 仍然是 1,p.Y 仍然是 2
  • 指针接收者:方法操作的是接收者的指针,可以修改原数据。
func (p *Point) Move(dx, dy int) {
    p.X += dx // 修改的是原数据
    p.Y += dy
}

p := &Point{X: 1, Y: 2}
p.Move(3, 4) // p.X 变为 4,p.Y 变为 6
  1. 如果实现了接收者是指针类型的方法,会隐含地也实现了接收者是值类型的方法。
  2. 如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。
  3. 要注意的是:如果只实现了值类型接收者,可以调用指针类型接收者,但是效果和值接收者一样,不能修改自身;如果只实现指针类型接收者,那么可以调用值类型接收者,效果和指针类型一样,可以修改自身

规则总结

  1. 如果实现了 *T(指针接收者)方法,T(值类型)也可以调用它(Go 自动转换)。
  2. 如果实现了 T(值接收者)方法,*T(指针类型)也可以调用它(Go 自动转换)。
  3. 但要注意:
    • 如果方法接收者是 值类型(T,调用时 始终是值拷贝,即使通过指针调用。
    • 如果方法接收者是 指针类型(*T,调用时 可以修改原数据,即使通过值调用。

代码示例

示例 1:指针接收者方法(*T

type Person struct {
    Name string
}

// 指针接收者方法(可以修改原数据)
func (p *Person) SetName(name string) {
    p.Name = name
}

func main() {
    // 情况 1:用指针调用(正常)
    p1 := &Person{Name: "Alice"}
    p1.SetName("Bob") // 可以修改
    fmt.Println(p1.Name) // 输出 "Bob"

    // 情况 2:用值调用(Go 自动转换,但仍然可以修改!)
    p2 := Person{Name: "Charlie"}
    p2.SetName("Dave") // Go 会自动转换成 (&p2).SetName("Dave")
    fmt.Println(p2.Name) // 输出 "Dave"
}

关键点:

  • 即使 p2 是值类型,p2.SetName() 仍然可以修改数据,因为 Go 自动转换成 (&p2).SetName()

示例 2:值接收者方法(T

type Person struct {
    Name string
}

// 值接收者方法(不能修改原数据)
func (p Person) SetName(name string) {
    p.Name = name
}

func main() {
    // 情况 1:用值调用(正常,但修改的是副本)
    p1 := Person{Name: "Alice"}
    p1.SetName("Bob") // 修改的是副本
    fmt.Println(p1.Name) // 仍然是 "Alice"

    // 情况 2:用指针调用(Go 自动转换,但仍然不能修改!)
    p2 := &Person{Name: "Charlie"}
    p2.SetName("Dave") // Go 自动转换成 (*p2).SetName("Dave")
    fmt.Println(p2.Name) // 仍然是 "Charlie"
}

关键点:

  • 即使 p2 是指针类型,p2.SetName() 仍然不能修改原数据,因为 Go 自动转换成 (*p2).SetName(),仍然是值拷贝。

示例 3:接口实现

type Namer interface {
    SetName(name string)
}

type Person struct {
    Name string
}

// 只实现指针接收者方法
func (p *Person) SetName(name string) {
    p.Name = name
}

func main() {
    // 情况 1:指针类型可以实现接口
    var p1 Namer = &Person{Name: "Alice"}
    p1.SetName("Bob") // 可以修改
    fmt.Println(p1.(*Person).Name) // 输出 "Bob"

    // 情况 2:值类型不能实现接口(编译错误)
    // var p2 Namer = Person{Name: "Charlie"} // 报错!
}

关键点:

  • 如果方法接收者是 *T,那么 只有 *T 能实现接口T 不能。
  • 如果方法接收者是 T,那么 T*T 都能实现接口

总结

方法接收者调用方式能否修改原数据接口实现
(p T)(值接收者)T.SetName()(*T).SetName()不能T*T 都能实现
(p *T)(指针接收者)(*T).SetName()T.SetName()只有 *T 能实现

最佳实践

  • 如果需要修改原数据,用指针接收者(*T)。
  • 如果只是计算或读取数据,用值接收者(T)。
  • 如果方法要用于接口,确保接收者类型一致(T*T)。

5. 方法的特殊用途
(1)实现接口(Interface)

Go 的 接口(Interface) 要求类型实现所有方法,而不是函数。

type Shape interface {
    Area() float64
}

type Circle struct { Radius float64 }

// Circle 实现 Shape 接口
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}
(2)链式调用(Fluent API)

方法可以返回接收者本身,支持链式调用:

type Person struct { Name string; Age int }

func (p *Person) SetName(name string) *Person {
    p.Name = name
    return p
}

func (p *Person) SetAge(age int) *Person {
    p.Age = age
    return p
}

// 链式调用
p := &Person{}
p.SetName("Alice").SetAge(25)

方法更符合面向对象思想,函数更符合过程式编程。 Go 语言两者都支持,根据场景选择即可。


内联

什么是“内联”?

内联(Inlining),是编译器做的一种优化技巧:

把函数调用“摊开”,直接把函数体的代码插入调用点,避免真正调用函数。

比如下面两个函数:

func add(a, b int) int {
    return a + b
}

func main() {
    x := add(1, 2)
}

如果 add 被内联了,等价于:

func main() {
    x := 1 + 2
}

省掉了 add() 的函数调用过程(包括栈帧创建/销毁等),提高效率。


内联和逃逸分析的关系

Go 编译器有个“逃逸分析”机制:它会决定变量应该分配在还是

  • 如果变量的作用域明确,只在当前函数中用完,那就放在栈上
  • 如果变量“逃出了函数”,比如被返回,或传给了其他 goroutine,就放在堆上

但是注意:内联会影响逃逸分析的判断结果


例子

type smallStruct struct {
   a, b int64
   c, d float64
}

func main() {
   smallAllocation()
}

//go:noinline
func smallAllocation() *smallStruct {
    return &smallStruct{}
}

加了 //go:noinline 后,Go 编译器不会把 smallAllocation() 函数体直接塞到 main() 中,它必须保留函数调用。

main() 调用这个函数后,返回的是一个 *smallStruct,而这个指针跨越了函数边界,逃逸分析会判断:

嗯,这个结构体逃出了 smallAllocation(),需要放到堆上。


如果你不加 //go:noinline

Go 编译器会尝试优化,把 smallAllocation 函数体内联到 main:

func main() {
    _ = &smallStruct{}
}

这时它会看到:&smallStruct{} 只在 main() 中被使用,没逃出作用域,不需要放到堆上,可以直接在栈上分配。


总结一句话

内联影响逃逸分析,而逃逸分析决定变量是分配在堆上还是栈上。


https://github.com/0voice

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值