『就要学习 Go 语言』系列--第 24 篇分享文章
之前的文章给大家总结过方法的一些基本用法,最近在学 Go 面向对象式编程,对方法又有一些新的认识,总结一下。
方法分为值方法和指针方法,这篇文章主要来讲讲这两者的区别。两者的定义:接收者类型为 T 的方法称为值方法;接收者类型为 *T 的方法称为指针方法
其中 T 必须满足如下条件:
- T 必须是自定义类型;
- T 的定义必须与方法的声明在同一个包内;
- T 不能是接口类型或者接口指针类型;
可以认为 T 是 *T 的基本类型。
方法的接收者是副本
之前也讲过这个,值方法的接收者是原类型值的副本,指针方法的接收者是原类型值的指针副本。在值方法内对副本修改不会影响到原值,注意有例外,除非这个类型是引用类型的别名类型,例如切片、字典。而在指针方法内,对指针副本指向的值做的修改一定会体现在原值上。
type Book struct {
pages int
}
type Books []Book
func (books Books)modify() {
// 原值已被修改
books[0].pages = 188
// 下面这行代码不会修改原值
books = append(books,Book{234})
}
func main() {
books := Books{
{123},
{456},
}
books.modify()
fmt.Println(books) // 输出:[{188} {456}]
}
复制代码
输出:
[{188} {456}]
复制代码
append() 调用不会影响到原值是因为该操作会重新申请一块内存存放接收者 books,但不会影响到原值。我们只将这两行代码换下顺序,其他代码不变:
func (books Books)modify() {
fmt.Println(books) // [{123} {456}]
books = append(books,Book{234})
fmt.Println(books) // [{123} {456} {234}]
books[0].pages = 188
fmt.Println(books) // [{188} {456} {234}]
}
func main() {
books := Books{
{123},
{456},
}
books.modify()
fmt.Println(books) // [{123} {456}]
}
复制代码
输出:
[{123} {456}]
[{123} {456} {234}]
[{188} {456} {234}]
[{123} {456}]
复制代码
从结果可以看出,上面代码段的第 5 行,这个赋值操作只影响新创建的切片,不会影响到原来的值。
如果想让方法内的修改体现在原值上,可以使用指针接收者。
func (books *Books) modify() {
*books = append(*books, Book{234})
(*books)[0].pages = 188
}
func main() {
books := Books{
{123},
{456},
}
books.modify()
fmt.Println(books) // [{188} {456} {234}]
}
复制代码
输出:
[{188} {456} {234}]
复制代码
每个方法都对应一个隐式函数
我们给结构体声明两个方法:
func (b Book) Pages() int {
return b.pages
}
func (b *Book) SetPages(pages int) {
b.pages = pages
}
复制代码
上面代码声明了两个方法,一个值方法,一个指针方法。
每个方法声明的时候,编译器会各自声明相对应的隐式函数。例如上面这两个方法对应的隐式函数是:
func Book.Pages(b Book) int {
return b.pages
}
func (*Book).SetPages(b *Book, pages int) {
b.pages = pages
}
复制代码
从代码可以看出,接收者被当做形参,函数体与方法体依然保持一致。看下函数名 Book.Pages 和 (*Book).SetPages,可以看作是 Type.MethodName 的结果,但函数不能包含特殊字符,所以这两个函数不能显式声明,但我们可以调用这两个函数。
type Book struct {
pages int
}
func (b Book) Pages() int {
return b.pages
}
func (b *Book) SetPages(pages int) {
b.pages = pages
}
func main() {
var book Book
(*Book).SetPages(&book, 188)
fmt.Println(Book.Pages(book)) // 188
}
复制代码
事实上,编译器不仅隐式声明了方法对应的函数,而且还重写了方法,让声明的方法去调用隐式声明的函数,就像下面这样:
func (b Book) Pages() int {
return Book.pages(b)
}
func (b *Book) SetPages(pages int) {
(*Book).SetPages(b, pages)
}
复制代码
方法集
理解方法集非常重要,来理一下,方法集是一组关联到自定义类型的值或指针的方法。一个自定义类型 T 的方法集合仅包括它的值方法,该类型的指针类型 *T 的方法集包括所有的值方法和指针方法。例如:
type Dog struct {
Name string
}
func (d Dog) getName() string {
return d.Name
}
func (d *Dog) SetName(name string) {
d.Name = name
}
复制代码
自定义类型 Dog,声明了值方法 getName() 和指针方法 SetName()。Dog 类型的方法集合只包括值方法,即 getName(),而 *Dog 类型的方法集合包含这两个方法。
严格意义上来说,基本类型 Dog 只能调用它的值方法,但实际写代码的时候,也可以通过 Dog 类型调用到指针方法,是因为编译器为自动为我们转译了。
func main() {
var dog Dog
dog.SetName("dog") // 通过 Dog 类型调用指针方法
//(&dog).SetName("dog")
fmt.Println(dog.Name)
}
复制代码
我们来看一个经典的例子,对比下:
// 定义接口 notifier
type notifier interface {
notify()
}
type user struct {
name string
email string
}
func (u *user)notify() {
fmt.Printf("Sending user email to %s<%s>\n",
u.name,
u.email)
}
// 接收一个 notifier 接口类型的参数,并发送通知
func sendNotification(n notifier) {
n.notify()
}
func main() {
u := user{"Seeklaod", "email@gmail.com"}
sendNotification(u)
}
复制代码
上面的代码定义了接口类型 notifier,只包含一个方法 notify(),实现了该方法的类型就认为实现了接口 notifier,*user 类型实现了该方法,即实现了接口。另外还定义了一个函数 sendNotification(),该函数接收一个接口类型的值。 编译运行下程序,发现报错:
cannot use u (type user) as type notifier in argument to sendNotification:
user does not implement notifier (notify method has pointer receiver)
复制代码
两个错误:
- 不能将 u ( type user) 作为参数传递给参数类型为 notifier 函数 sendNotification();
- user 类型没有实现接口 notifier;
其实主要就是因为第 2 个错误引起的,上面已经讲过是 *user 实现了接口 notifier。
使用 *user 类型实现接口时为什么 user 类型无法实现接口呢?这就需要了解上面说过的方法集,关于方法集,Go 语言规范是这样定义的:
Values | Method Receivers |
---|---|
T | (t T) |
*T | (t T) 或 (t *T) |
是这个意思,自定义类型 T 的方法集合仅包括它的值方法,而该类型的指针类型 *T 的方法集包括所有的值方法和指针方法,上面也讲过。对应到本例子,user 类型的方法集不包括方法 notify(),也就没有实现接口 notifier。
现在知道问题出在哪了,修改下程序,让程序跑起来:
func main() {
u := user{"Seeklaod", "email@gmail.com"}
sendNotification(&u) // 传入地址
}
复制代码
上面的代码编译通过。因为使用指针接收者实现的接口,只有 user 类型的指针可以传给 sendNotification 函数。
现在的问题是,为什么这种情况下,编译器没有为我们自动转译呢?事实上,编译器并不是总能自动获得一个值的地址,这就是其中一种。
因为不是总能获取一个值的地址,所以值的方法集只包括了使用值接收者实现的方法。
希望这篇文章能够帮助你,Good day!
参考资料:
1.教女朋友写方法
2.Methods in Go
3.Methods, Interfaces and Embedded Types in Go
(全文完)
原创文章,若需转载请注明出处!
欢迎扫码关注公众号「Golang来啦」或者移步 seekload.net ,查看更多精彩文章。
给你准备了学习 Go 语言相关书籍,公号后台回复【电子书】领取!
有疑问加站长微信联系(非本文作者)