方法
方法
方法与函数的区别在于,方法拥有接收者,而函数没有,且只有自定义类型能够拥有方法。先来看一个例子。
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
,再声明了三个方法Get
,Set
和Len
,方法的长相与函数并无太大的区别,只是多了一小段(i IntSlice)
。i
就是接收者,IntSlice
就是接收者的类型,接收者就类似于其他语言中的this
或self
,只不过在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]