方法
Methods | 方法
方法一般是面向对象编程
方法是由函数演变而来,只是将函数的第一个对象参数移动到了函数名前面了而已。因此我们依然可以按照原始的过程式思维来使用方法。将第一个函数参数移动到函数前面,从代码角度看虽然只是一个小的改动,但是从编程哲学角度来看,
// C 风格函数
// 文件对象
type File struct {
fd int
}
// 打开文件
func OpenFile(name string) (f *File, err error) {
// ...
}
// 关闭文件
func CloseFile(f *File) error {
// ...
}
// 读文件数据
func ReadFile(f *File, offset int64, data []byte) int {
// ...
}
// Go 语言中的做法是,将 CloseFile 和 ReadFile 函数的第一个参数移动到函数名的开头。
// 关闭文件
func (f *File) CloseFile() error {
// ...
}
// 读文件数据
func (f *File) ReadFile(offset int64, data []byte) int {
// ...
}
// 简化名称
// 关闭文件
func (f *File) Close() error {
// ...
}
// 读文件数据
func (f *File) Read(offset int64, data []byte) int {
// ...
}
通过叫方法表达式的特性可以将方法还原为普通类型的函数。
// 不依赖具体的文件对象
// func CloseFile(f *File) error
var CloseFile = (*File).Close
// 不依赖具体的文件对象
// func ReadFile(f *File, offset int64, data []byte) int
var ReadFile = (*File).Read
// 文件处理
f, _ := OpenFile("foo.dat")
ReadFile(f, 0, data)
CloseFile(f)
Pointer | 指针
指类型实例上的方法。我们可以为任何已命名的类型(除了指针或接口)定义方法;接收者可不必为结构体。同一种类型的方法的接收者必须使用相同的名字。通常这个名字为一到两个字母组成,通常是类型名的首字母,前缀等,例如类型名为 “Client”
// p 是 Vertex 类型
p := Vertex{1, 2}
// q 是指向 Vertex 的指针
q := &p
// r 同样是指向 Vertex 对象的指针
r := &Vertex{1, 2}
// 指向 Vertex 结构体对象的指针类型为 *Vertex
var s *Vertex = new(Vertex)
当我们在定义结构体时,可以使用指针或者值作为接受者来定义方法用指针作为接收者,那么变量(或者可以称作对象)本身是按引用传递的,在方法内可以修改对象的数据。使用值接收者,以为这是按值传递的,那么对象在方法内是处于只读状态的。并且指针类型时调用方法会复制
type VideoItem struct {
GroupId int64
ItemId int64
AggrType int32
}
func (item *VideoItem) TestPointer(GroupId int64) {
fmt.Printf("TestPointer %p %v\n", item, item)
item.GroupId = GroupId
}
func (item VideoItem) TestValue(GroupId int64) {
fmt.Printf("TestPointer %p %v\n", &item, &item)
item.GroupId = GroupId
}
func main() {
v := VideoItem{}
fmt.Printf("TestPointer %p %v\n", &v, &v)
// 值不变
v.TestValue(1)
// v 的 GroupId 被修改为 2
v.TestPointer(2)
// 值不变
(&v).TestValue(3)
// v 的 GroupId 被修改为 4
(&v).TestPointer(4)
fmt.Println(v)
}
/*
TestPointer 0xc420018300 &{0 0 0}
TestPointer 0xc420018360 &{0 0 0}
TestPointer 0xc420018300 &{0 0 0}
TestPointer 0xc4200183c0 &{0 0 0}
TestPointer 0xc420018300 &{0 0 0}
*/
-
传递普通变量传递值拷贝,不能修改原始值,如果是大对象则内存效率不高。
-
传递变量的指针,指针为固定大小,效率更高,可以就地修改对象的原始值。
-
在方法集的使用上,无论接收者是变量还是指针,都能直接正确调用,无需特殊处理,能正确调用所有绑定在该值或指针上的方法,
Go 会自动帮我们处理引用与解引用。
接收者类型
接收者类型可是指针也可以是值。通常当我们想修改接收者本身时,使用指针,其他情况使用值。指针接收者和值接收者还有一个区别:值方法可通过指针和值调用,而指针方法只能通过指针来调用。之所以会有这条规则是因为指针方法可以修改接收者;通过值调用它们会导致方法接收到该值的副本,因此任何修改都将被丢弃,因此该语言不允许这种错误。
- 如果接收者类型为
map 、func、chan 不要使用指向他们的指针。如果接收者是slice ,而方法不会做重新切片或重新分配,也不要使用指针。 - 如果方法需要修改接收者,那么必须使用指针。
- 如果接收者中包含
sync.Mutex 或者其他类似的域,那么必须使用指针,以防止意外的值拷贝。 - 如果接收者是大结构体或者数组,那么使用指针会更高效。
- 值方法在运行时会拷贝一份接收者,如果接收者需要观察到其他 协程对接收者的修改,就必须使用指针。
- 如果接收者为结构体、数组、切片并且其中至少有一个域(或元素)的类型为指针,那么优先使用指针。
- 如果接收者是小数组或结构体,或是天然是值类型(比如,类似
time.Time ) ,没有可修改的域并且没有指针域,或者是基础类型例如string 或int ,那么用值接收者比较合理。值接收者可以减少垃圾生成;如果一个值通过值传递,那么他将在栈上创建一个副本,而不是在堆上分配一份空间。( 编译器已经足够聪明到能够优化防止在堆上分配空间了,但是不能完全保证生效。) 不要仅仅因为这个原因而选择值接收者,一切以Profiling 为准。 - 最后,犹豫不决,用指针。