声明方法
声明方法的语法和声明普通函数非常类似,只是在函数名字前面加上一个参数,这个参数把这个方法绑定到它对应的类型上。
func (e Employee) ToString() (description string) {
return fmt.Sprintf("[%d, %s], from %s", e.ID, e.Name, e.Address)
}
注意这里的(e Employee)
,这里的Employee
说明了这个ToString()
方法属于Employee
类型的方法,而e
就类似其它语言中的this
或self
,只是golang没有使用这种特殊名称,而是常常用类型的第一个小写字母来表示。
在golang中,可以将一个方法绑定到任意的类型上(指针类型和接口类型除外),包括内置的基础类型,这也有些类似 C# 语言中的扩展方法。
继承
type Employee struct {
ID int
Name, Address string
}
func (e Employee) ToString() (description string) {
return fmt.Sprintf("[%d, %s], from %s", e.ID, e.Name, e.Address)
}
func (e Employee) SayHello() string {
return fmt.Sprintf("hello, I'm %s", e.Name)
}
type EmployeeManager struct {
Employee // 匿名成员
ManagerLevel int
}
func (e EmployeeManager) ToString() (description string) {
return fmt.Sprintf("%s , level %d", e.Employee.ToString(), e.ManagerLevel)
}
var manager = EmployeeManager{
Employee: Employee{
ID: 2,
Name: "fooManager",
Address: "beijing",
},
ManagerLevel: 4,
}
fmt.Println(manager.SayHello())
关于匿名成员,之前的文章已经讨论过了,是用来实现继承的有效方式,可阅读 golang中的struct。从上面的代码可以看出来,匿名成员的方法也是可以直接使用的。在运行时,SayHello方法接收到的实参是 manager.Employee
,而不是manager
。
而这里面的两个类型都包含了ToString
方法,但它们并没有什么关系,因为它们的类型分别是Employee.ToString
和EmployeeManager.ToString
。
从这里可以看出,编译器在里面做了很多工作,导致在运行时分得特别清。我们不能将一个子类的实例赋值给父类的变量,所以想实现多态的话,需要借助接口,这个是编译器支持的,具体来讲就是:你声明一个接口类型的变量或参数,可以将任意一个实现了接口方法的类的实例赋值给它,在运行中也会执行对应的类的代码。
由于golang可以以匿名成员的方式实现继承的效果,那就意味着它可以实现多继承的效果。
方法接收者是指针类型的情况
方法的类型也可以声明为类型的指针形式,比如:
func (e *Employee) ToString() (description string) {
return fmt.Sprintf("[%d, %s], from %s", e.ID, e.Name, e.Address)
}
结构体类型与指针类型,在调用时,编译器会给予强大的支持,比如,如果方法的接收者是枚举指针类型,而实际传递的实参是枚举类型,那么编译器会隐式地通过&
操作将实参转换为指针进行传递,而如果你定义的接收者是枚举类型但实际调用时传递了个指针类型,编译器会隐式地通过*
操作将指针指向的值进行传递。
但是,与普通函数一样,如果你定义的接收者是枚举类型,那么在实际调用时会采用值传递的方式,也就意味着如果你修改了成员的值,原来的参数并不会发生变化。
func (e *EmployeeManager) Promote() {
e.ManagerLevel++
}
这里必须使用指针传递,否则成员的改动无效。
总结:指针类型或枚举类型,编译器都给予充分支持,怎么写都能行,但在运行时,指针类型会保留值的改动。
习惯:如果一个类型的任何一个方法使用了指针接受者,那么所有的方法都应该采用指针类型的接受者,即便有些只读的方法并不需要。
接收者的实参可以是nil
没错,接收者可以是nil,golang的机制会将nil传递给方法并运行,至于会不会引发宕机异常,要看你有没有引用nil的引用。
func (e *Employee) SayHello() string {
if e == nil {
return "hello, I'm nobody"
}
return fmt.Sprintf("hello, I'm %s", e.Name)
}
var nilEmployee *Employee
fmt.Println(nilEmployee.SayHello()) // 输出 hello, I'm nobody
方法变量和方法表达式
这部分的目的是将一个方法像函数那样来调用。
toString := employee.ToString
fmt.Println(toString()) // func() string
fmt.Printf("%T\n", toString)
这里的toString
是一个::方法变量::,也可以说是一个绑定了接收者的函数变量,这时它的类型等同于去掉接收者的函数类型 。
promote := (*EmployeeManager).Promote
fmt.Printf("%T\n", promote) // func(*main.EmployeeManager)
promote(&manager)
fmt.Println(manager.ToString())
这里的promote
是一个::方法表达式::,它是一个函数,这时它的类型等同于把接收者作为了函数的第一个参数。从理解上来讲,它更像一个普通函数。
注意,方法调用是变量.f()
,方法变量是变量.f
,方法表达式是T.f
或(*T).f
。
这两个机制就实现了从方法到函数的转换。
golang中的封装
golang只有一种控制可见性的机制,就是大写字母开头会导出,在包外可见,如果以小写字母开头,则在包内可见。
所以在golang中,对象的封装只能通过在结构体中定义小写字母开头的成员来实现,并且封装的单元是包,而不是类。
从封装的目的上看,封装主要是通过隐藏内部信息来降低外部使用它的复杂度,从这个意义上来讲,包内封装的必要性确实没那么大。