// 普通函数
func Add(a, b int) int {
return a + b
}
type Point struct {
X, Y int
}
// Point 类型的方法
func (p Point) Distance() float64 {
return math.Sqrt(float64(p.X*p.X + p.Y*p.Y))
}
sum := Add(3, 5) // 函数调用
p := Point{X: 3, Y: 4}
dist := p.Distance() // 方法调用
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
),调用时 可以修改原数据,即使通过值调用。*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()
。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()
,仍然是值拷贝。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
)。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
}
方法可以返回接收者本身,支持链式调用:
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 编译器有个“逃逸分析”机制:它会决定变量应该分配在栈还是堆。
但是注意:内联会影响逃逸分析的判断结果!
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