方法

Methods | 方法

方法一般是面向对象编程(OOP)的一个特性,在 C++语言中方法对应一个类对象的成员函数,是关联到具体对象上的虚表中的。但是 Go 语言的方法却是关联到类型的,这样可以在编译阶段完成方法的静态绑定。一个面向对象的程序会用方法来表达其属性对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情。面向对象编程(OOP)进入主流开发领域一般认为是从 C++开始的,C++就是在兼容 C 语言的基础之上支持了 class 等面向对象的特性。然后 Java 编程则号称是纯粹的面向对象语言,因为 Java 中函数是不能独立存在的,每个函数都必然是属于某个类的。

方法是由函数演变而来,只是将函数的第一个对象参数移动到了函数名前面了而已。因此我们依然可以按照原始的过程式思维来使用方法。将第一个函数参数移动到函数前面,从代码角度看虽然只是一个小的改动,但是从编程哲学角度来看,Go 语言已经是进入面向对象语言的行列了。我们可以给任何自定义类型添加一个或多个方法。每种类型对应的方法必须和类型的定义在同一个包中,因此是无法给 int 这类内置类型添加方法的(因为方法的定义和类型的定义不在一个包中)。对于给定的类型,每个方法的名字必须是唯一的,同时方法和函数一样也不支持重载。

// 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”, 接收者名可以为 “c” 或者 “cl”。不要使用一些通用的名字,比如 this,me,self。在 Go 中接收者对于方法来说,只是一个隐藏的参数。

// p 是 Vertex 类型
p := Vertex{1, 2}

// q 是指向 Vertex 的指针
q := &p

// r 同样是指向 Vertex 对象的指针
r := &Vertex{1, 2}

// 指向 Vertex 结构体对象的指针类型为 *Vertex
var s *Vertex = new(Vertex)

当我们在定义结构体时,可以使用指针或者值作为接受者来定义方法用指针作为接收者,那么变量(或者可以称作对象)本身是按引用传递的,在方法内可以修改对象的数据。使用值接收者,以为这是按值传递的,那么对象在方法内是处于只读状态的。并且指针类型时调用方法会复制 receiver, 每调用一次 TestValue,item 就会被复制一次.实际相当于 TestValue(v),TestPointer(&v)。

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 会自动帮我们处理引用与解引用。

接收者类型

接收者类型可是指针也可以是值。通常当我们想修改接收者本身时,使用指针,其他情况使用值。指针接收者和值接收者还有一个区别:值方法可通过指针和值调用,而指针方法只能通过指针来调用。之所以会有这条规则是因为指针方法可以修改接收者;通过值调用它们会导致方法接收到该值的副本,因此任何修改都将被丢弃,因此该语言不允许这种错误。

  1. 如果接收者类型为 map、func、chan 不要使用指向他们的指针。如果接收者是 slice,而方法不会做重新切片或重新分配,也不要使用指针。
  2. 如果方法需要修改接收者,那么必须使用指针。
  3. 如果接收者中包含 sync.Mutex 或者其他类似的域,那么必须使用指针,以防止意外的值拷贝。
  4. 如果接收者是大结构体或者数组,那么使用指针会更高效。
  5. 值方法在运行时会拷贝一份接收者,如果接收者需要观察到其他 协程对接收者的修改,就必须使用指针。
  6. 如果接收者为结构体、数组、切片并且其中至少有一个域(或元素)的类型为指针,那么优先使用指针。
  7. 如果接收者是小数组或结构体,或是天然是值类型(比如,类似 time.Time),没有可修改的域并且没有指针域,或者是基础类型例如 string 或 int,那么用值接收者比较合理。值接收者可以减少垃圾生成;如果一个值通过值传递,那么他将在栈上创建一个副本,而不是在堆上分配一份空间。(编译器已经足够聪明到能够优化防止在堆上分配空间了,但是不能完全保证生效。) 不要仅仅因为这个原因而选择值接收者,一切以 Profiling 为准。
  8. 最后,犹豫不决,用指针。
上一页