go语言中的方法
go支持OO语言的一些特性,方法就是其中之一。本文将介绍go语言中方法相关的概念。
方法声明
在go语言中,我们可以明确的给类型T和*T声明一个方法,前提是T满足下面4点要求:
- T必须是定义的类型
- T的定义必须跟方法定义的在同一个包中。
- T不能是指针类型
- T不能是interface类型。
T和T被称作为他们定义的相应方法的接收器(Receiver). 类型T被称为类型T和T声明的所有方法的接收器的基础类型。
注意, 我们也可以给T和T的别名类型声明方法。效果等同于给T和T声明方法。
如果为类型声明了一个方法,我们可以说该类型具有(或拥有)该方法。
从上面列出的条件,我们将得出我们永远不能给符合下面条件的类型声明方法:
- 内置基本类型,例如int, string,因为我们不能在标准库里声明方法;
- interface类型,但是一个interface可以拥有方法。
- 未定义类型,包括各种未命名的组合类型。如果一个未命名类型内嵌了一个其他的有方法的类型,编译器将明确的为这个匿名类型和其指针声明响应的方法。
方法声明类似于函数声明,但它有一个额外的参数声明部分。额外部分能包含并且仅能包含一个这个方法的接受者类型的参数。这个唯一的接受者参数被称为方法声明的接受者参数。接受者参数必须被()括住,并且声明在func和函数名称之间。
下面是一个方法声明的例子:
// Age and int are two distinct types. We can't declare
// methods for int and *int, but can for Age and *Age.
type Age int
func (age Age) LargerThan(a Age) bool {
return age > a
}
func (age *Age) Increase() {
*age++
}
// Receiver of custom defined function type.
type FilterFunc func(in int) bool
func (ff FilterFunc) Filte(in int) bool {
return ff(in)
}
// Receiver of custom defined map type.
type StringSet map[string]struct{}
func (ss StringSet) Has(key string) bool {
_, present := ss[key]
return present
}
func (ss StringSet) Add(key string) {
ss[key] = struct{}{}
}
func (ss StringSet) Remove(key string) {
delete(ss, key)
}
// Receiver of custom defined struct type.
type Book struct {
pages int
}
func (b Book) Pages() int {
return b.pages
}
func (b *Book) SetPages(pages int) {
b.pages = pages
}
从上面的例子可以看出,接收器的基础类型不仅可以是结构体类型,还可以是其他的类型,例如基本类型和容器类型等,只要他们能满足上面列出的4个条件。
在 其他语言中接收器一直被明确为this,在go语言中,我们不推荐这么使用。
一个类型*T的接收器被称为指针接收器,非指针接收器被称为值接收器。就我个人来说,我不推荐吧指针类型视为值类型的对立面。因为指针类型的值不过是一个特殊的值。但是,我不反对使用指针接收器和值接收器。
方法名可以是空标识符_. 一个类型可以有多个有名为空标识符的方法。但是这样的方法永远无法调用。因为只有导出的方法可以被其他包调用。
对每一个方法来说,编译器将为它声明一个相应的隐式函数。对于在上一节的最后一个示例中为类型Book和type * Book声明的最后两个方法,编译器隐式声明了以下两个函数:
func Book.Pages(b Book) int {
return b.pages // the body is the same as the Pages method
}
func (*Book).SetPages(b *Book, pages int) {
b.pages = pages // the body is the same as the SetPages method
}
指针接收器的隐式方法
对于声明给值接收器类型T的每一个方法,编译器会隐式的给T声明一个有相同名字的方法。上面例子中Pages方法是给Book类型声明的。所以编译器将隐式的声明一个有相同名字的Pages方法给Book。 这个同名方法值包含一行代码,这行代码就是掉毛隐式方法Book.Pages.
func (b *Book) Pages() int {
return Book.Pages(*b)
}
当我们给一个非指针类型声明一个方法时,事实上我们声明了两个方法,一个明确的给非指针类型,还有一个隐式的给相应的指针类型。
方法原型和方法集
方法原型可以视为一个没有func关键字的函数原型。每个方法声明都是由func关键词,接受者参数声明,方法原型和方法正文构成。
例如,Pages和SetPages的方法原型如下:
Pages() int
SetPages(pages int)
每个类型都有一个方法集。非interface类型的方法集室友所有的声明的方法的方法原型组成的。这些方法包含明确声明的和隐式声明的。
例如:上文中的Book的方法集是:
Pages() int
*Book的方法集是
Pages() int
SetPages(pages int)
方法集中方法原型的顺序并不重要。
对于一个方法集,如果其中的每个方法原型都在另外一个方法集中。我们成它是另外一个方法集的子集。如果两个方法集互为子集,则他们相等。
给定一个非指针类型,非interface类型的类型T,类型T的方法集必定是类型*T的方法集的子集。
需要注意的是,不同包中非导出的方法名(以小写字母开头)将会被视为两个不同的方法名称,即使他们在字面上完全一样。
下面这些类型的方法集总是空的。
- 内置基本类型
- 定义的指针类型
- 基础类型是interface或者指针类型的指针
- 未定义的数组,切片,函数和通道类型
方法值和方法调用
方法其实是一种特殊的函数,常被称为成员函数。当一个类型拥有一个方法,这个类型的每一个值都拥有一个不可改变的函数类型的成员。成员名称与方法名称相同,成员的类型与使用方法声明的形式声明但没有接收器部分的函数相同。
方法调用只是对这样的成员函数的调用。对于值v,其方法m可以用选择器形式v.m表示,该形式是函数值。
包含一些方法调用的示例:
package main
import "fmt"
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
fmt.Printf("%T \n", book.Pages) // func() int
fmt.Printf("%T \n", (&book).SetPages) // func(int)
// &book 有一个隐含类型。
fmt.Printf("%T \n", (&book).Pages) // func() int
// Call the three methods.
(&book).SetPages(123)
book.SetPages(123) // 为啥这个也行了,其实这是一种语法糖,编译器会自动将book转换成它的指针类型
fmt.Println(book.Pages()) // 123
fmt.Println((&book).Pages()) // 123
}
如上所述,当为类型声明方法时,该类型的每个值都将拥有一个成员函数。如果类型由nil表示,则零值不是例外,无论零值是否为零。
package main
type StringSet map[string]struct{}
func (ss StringSet) Has(key string) bool {
_, present := ss[key] // Never panic here,
// even if ss is nil.
return present
}
type Age int
func (age *Age) IsNil() bool {
return age == nil
}
func (age *Age) Increase() {
*age++ // If age is a nil pointer, then
// dereferencing it will panic.
}
func main() {
_ = (StringSet(nil)).Has // will not panic
_ = ((*Age)(nil)).IsNil // will not panic
_ = ((*Age)(nil)).Increase // will not panic
_ = (StringSet(nil)).Has("key") // will not panic
_ = ((*Age)(nil)).IsNil() // will not panic
// 下面这行将会恐慌,但是不是因为函数调用导致的恐慌。
// 原因在于方法内部调用了指针的实体
((*Age)(nil)).Increase()
}
接收器参数是通过拷贝传入的
与很多普通函数的参数类型,接收器参数也是通过拷贝传入的。因此,方法调用中对接收器参数的直接部分的修改将不会反映到方法的外部。
package main
import "fmt"
type Book struct {
pages int
}
func (b Book) SetPages(pages int) {
b.pages = pages
}
func main() {
var b Book
b.SetPages(123)
fmt.Println(b.pages) // 0
}
另外一个例子
package main
import "fmt"
type Book struct {
pages int
}
type Books []Book
func (books Books) Modify() {
// 对接收器底层部分的修改将体现在方法外部.
books[0].pages = 500
//对接收器直接部分的修改,将不会体现在方法外部
books = append(books, Book{789})
}
func main() {
var books = Books{{123}, {456}}
books.Modify()
fmt.Println(books) // [{500} {456}]
}
有些偏离主题,如果交换上述Modify方法的顺序中的两行,则两个修改都不会反映到方法体外部。
func (books Books) Modify() {
books = append(books, Book{789})
books[0].pages = 500
}
func main() {
var books = Books{{123}, {456}}
books.Modify()
fmt.Println(books) // [{123} {456}]
}
这里的原因是append调用将分配一个新的内存块来存储传递的slice receiver参数的副本的元素。分配不会反映传递的切片接收器参数本身。
为了使两个修改都反映到方法体外部,该方法的接收器必须是指针接收器。
func (books *Books) Modify() {
*books = append(*books, Book{789})
(*books)[0].pages = 500
}
func main() {
var books = Books{{123}, {456}}
books.Modify()
fmt.Println(books) // [{500} {456} {789}]
}
一个方法该声明应该声明为以指针作为接收器还是值接收器的理由
首先,从上一节开始,我们知道有时我们必须用指针接收器声明方法。
实际上,我们总是可以使用指针接收器声明方法而不会出现任何逻辑问题。程序性能问题有时候用值接收器声明方法会更好。
对于这些情况,值接收器和指针接收器都是可接受的,这里有一些因素需要考虑做出决定:
- 指针副本太多可能会导致垃圾收集器的工作量增加。
- 如果值接收器类型的大小很大,则接收器参数复制成本可能不可忽略。指针类型都是小型的。
- 如果在多个goroutine中同时调用声明的方法,则声明同一基类型的值接收器和指针接收器的方法更有可能导致数据争用。
- 不应复制同步标准包中类型的值,因此使用将类型嵌入同步标准包中的结构类型的值接收器定义方法是有问题的。
如果很难确定方法是否应该使用指针接收器或值接收器,那么只需选择指针接收器方式。