方法

寒江蓑笠翁大约 10 分钟

方法

方法与函数的区别在于,方法拥有接收者,而函数没有,且只有自定义类型能够拥有方法。先来看一个例子。

type IntSlice []int

func (i IntSlice) Get(index int) int {
	return i[index]
}
func (i IntSlice) Set(index, val int) {
	i[index] = val
}

func (i IntSlice) Len() int {
	return len(i)
}

先声明了一个类型IntSlice,其底层类型为[]int,再声明了三个方法GetSetLen,方法的长相与函数并无太大的区别,只是多了一小段(i IntSlice)i就是接收者,IntSlice就是接收者的类型,接收者就类似于其他语言中的thisself,只不过在Go中需要显示的指明。

func main() {
   var intSlice IntSlice
   intSlice = []int{1, 2, 3, 4, 5}
   fmt.Println(intSlice.Get(0))
   intSlice.Set(0, 2)
   fmt.Println(intSlice)
   fmt.Println(intSlice.Len())
}

方法的使用就类似于调用一个类的成员方法,先声明,再初始化,再调用。


值接收者

接收者也分两种类型,值接收者和指针接收者,先看一个例子

type MyInt int

func (i MyInt) Set(val int) {
   i = MyInt(val) // 修改了,但是不会造成任何影响
}

func main() {
   myInt := MyInt(1)
   myInt.Set(2)
   fmt.Println(myInt)
}

上述代码运行过后,会发现myInt的值依旧是1,并没有被修改成2。方法在被调用时,会将接收者的值传入方法中,上例的接收者就是一个值接收者,可以简单的看成一个形参,而修改一个形参的值,并不会对方法外的值造成任何影响,那么如果通过指针调用会如何呢?

func main() {
	myInt := MyInt(1)
	(&myInt).Set(2)
	fmt.Println(myInt)
}

遗憾的是,这样的代码依旧不能修改内部的值,为了能够匹配上接收者的类型,Go会将其解引用,解释为(*(&myInt)).Set(2)


指针接收者

稍微修改了一下,就能正常修改myInt的值。

type MyInt int

func (i *MyInt) Set(val int) {
   *i = MyInt(val)
}

func main() {
   myInt := MyInt(1)
   myInt.Set(2)
   fmt.Println(myInt)
}

现在的接收者就是一个指针接收者,虽然myInt是一个值类型,在通过值类型调用指针接收者的方法时,Go会将其解释为(&myint).Set(2)。所以方法的接收者为指针时,不管调用者是不是指针,都可以修改内部的值。


函数的参数传递过程中,是值拷贝的,如果传递的是一个整型,那就拷贝这个整型,如果是一个切片,那就拷贝这个切片,但如果是一个指针,就只需要拷贝这个指针,显然传递一个指针比起传递一个切片所消耗的资源更小,接收者也不例外,值接收者和指针接收者也是同样的道理。在大多数情况下,都推荐使用指针接收者,不过两者并不应该混合使用,要么都用,要么就都不用,看下面一个例子。

提示

需要先了解接口

type Animal interface {
   Run()
}

type Dog struct {
}

func (d *Dog) Run() {
   fmt.Println("Run")
}

func main() {
   var an Animal
   an = Dog{}
   // an = &Dog{} 正确方式
   an.Run()
}

这一段代码将会无法通过编译,编译器将会输出如下错误

cannot use Dog{} (value of type Dog) as type Animal in assignment:
	Dog does not implement Animal (Run method has pointer receiver)

翻译过来就是,无法使用Dog{}初始化Animal类型的变量,因为Dog没有实现Animal ,解决办法有两种,一是将指针接收者改为值接收者,二是将Dog{}改为&Dog{},接下来逐个讲解。

type Dog struct {
}

func (d Dog) Run() { // 改为了值接收者
   fmt.Println("Run")
}

func main() { // 可以正常运行
   var an Animal
   an = Dog{}
   // an = &Dog{} 同样可以
   an.Run()
}

在原来的代码中,Run 方法的接收者是*Dog ,自然而然实现Animal接口的就是Dog指针,而不是Dog结构体,这是两个不同的类型,所以编译器就会认为Dog{}并不是Animal的实现,因此无法赋值给变量an,所以第二种解决办法就是赋值Dog指针给变量an。不过在使用值接收者时,Dog指针依然可以正常赋值给animal,这是因为Go会在适当情况下对指针进行解引用,因为通过指针可以找到Dog结构体,但是反过来的话,无法通过Dog结构体找到Dog指针。如果单纯的在结构体中混用值接收者和指针接收者的话无伤大雅,但是和接口一起使用后,就会出现错误,倒不如无论何时要么都用值接收者,要么就都用指针接收者,形成一个良好的规范,也可以减少后续维护的负担。


还有一种情况,就是当值接收者是可寻址的时候,Go会自动的插入指针运算符来进行调用,例如切片是可寻址,依旧可以通过值接收者来修改其内部值。比如下面这个代码

type Slice []int

func (s Slice) Set(i int, v int) {
	s[i] = v
}

func main() {
	s := make(Slice, 1)
	s.Set(0, 1)
	fmt.Println(s)
}

输出

[1]

但这样会引发另一个问题,如果对其添加元素的话,情况就不同了。看下面的例子

type Slice []int

func (s Slice) Set(i int, v int) {
	s[i] = v
}

func (s Slice) Append(a int) {
	s = append(s, a)
}

func main() {
	s := make(Slice, 1, 2)
	s.Set(0, 1)
	s.Append(2)
	fmt.Println(s)
}
[1]

它的输出还是和之前一样,append函数是有返回值的,向切片添加完元素后必须覆盖原切片,尤其是在扩容后,在方法中对值接收者修改并不会产生任何影响,这也就导致了例子中的结果,改成指针接收者就正常了。

type Slice []int

func (s *Slice) Set(i int, v int) {
	(*s)[i] = v
}

func (s *Slice) Append(a int) {
	*s = append(*s, a)
}

func main() {
	s := make(Slice, 1, 2)
	s.Set(0, 1)
	s.Append(2)
	fmt.Println(s)
}

输出

[1 2]