方法与函数
区别
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
- 如果实现了接收者是指针类型的方法,会隐含地也实现了接收者是值类型的方法。
- 如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。
- 要注意的是:如果只实现了值类型接收者,可以调用指针类型接收者,但是效果和值接收者一样,不能修改自身;如果只实现指针类型接收者,那么可以调用值类型接收者,效果和指针类型一样,可以修改自身
规则总结
- 如果实现了
*T
(指针接收者)方法,T
(值类型)也可以调用它(Go 自动转换)。 - 如果实现了
T
(值接收者)方法,*T
(指针类型)也可以调用它(Go 自动转换)。 - 但要注意:
- 如果方法接收者是 值类型(
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()
中被使用,没逃出作用域,不需要放到堆上,可以直接在栈上分配。
总结一句话
内联影响逃逸分析,而逃逸分析决定变量是分配在堆上还是栈上。