独特的“非侵入式”接口设计是 Go 语言的一亮点。接口使得 Go 这种静态型语言有了动态型语言的特性,提供了非常大的灵活性。Go 语言的成功,接口功不可没。
Ducking Typing ,鸭子类型,是动态编程语言的一种对象推断策略,它主要关注于对象如何被使用,而不是对象类型的本身。Go 语言作为一门静态语言,它通过 interface 的方式完美支持鸭子类型。
Go 语言作为一门现代静态语言,吸取了“前辈”们的经验和教训,有很大的后发优势。它引入了动态语言的便利,同时又会进行静态语言的类型检查。Go 不要求类型显示的实现的某个接口,只要求实现了接口相关方法即可。
让我们看一个例子:
package main
import "fmt"
// Greeting 定义一个接口
type Greeting interface {
sayHello()
}
// 使用接口作为函数参数
func sayHello(g Greeting) {
g.sayHello()
}
// Go 定义结构体,并且实现该接口
type Go struct{}
func (g Go) sayHello() {
fmt.Println("hello,i am go")
}
type Java struct{}
func (j Java) sayHello() {
fmt.Println("hello, i am java")
}
func main() {
golang := Go{}
java := Java{}
sayHello(golang)
sayHello(java)
}
在 main 函数中,调用 sayHello 函数时,传入结构体的实例化对象,它们并没有显示的实现接口。实际上,编译器在调用 sayHello()时,会隐式的将 golang,java 对象转换为 Greeting 类型,这其实也是静态语言的类型检查功能。
方法能给用户自定义的类型添加新的行为。它和函数的区别在于方法有一个接收者,给一个函数添加一个接收者,那么它就变成了方法。接收者可以是值接收者,也可以是指针接收者。
在调用方法的时候,值类型既可以调用值接收者的方法,也可以调用指针接收者的方法;指针类型既可以调用指针接收者的方法,也可以调用值接收者的方法。
总而言之,不管方法的接收者是什么类型,该类型的值和指针都可以调用,不必严格符合接收者的类型。
让我们看一个例子:
package main
import "fmt"
type Person struct {
age int
}
func (p Person) howOld() int {
return p.age
}
func (p *Person) growUp() {
p.age += 1
}
func main() {
// tom 是值类型
tom := Person{age: 18}
// 值类型 调用接收者也是值类型的方法
fmt.Println(tom.howOld())
// 值类型 调用接收者是指针类型的方法
tom.growUp()
fmt.Println(tom.howOld())
// ----------------------
// steven是指针类型
steven := &Person{age: 100}
// 指针类型 调用接收者是值类型的方法
fmt.Println(steven.howOld())
// 指针类型 调用接收者也是指针类型的方法
steven.growUp()
fmt.Println(steven.howOld())
}
实际上,当类型和方法的接受者类型不同时,其实是编译器替我们“负重前行”了。
上文说过,无论接受者是值类型还是指针类型,都可以通过值类型和指针类型调用,这就是语法糖起了作用。
结论就是,实现了接收者是值类型的方法,相当于自动实现了接收者是指针类型的方法。而实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法。 让我们来看一个例子:
package main
import "fmt"
type coder interface {
code()
debug()
}
type Gopher struct {
language string
}
func (p Gopher) code() {
fmt.Printf("I am coding %s language\n", p.language)
}
func (p *Gopher) debug() {
fmt.Printf("I am debuging %s language\n", p.language)
}
func main() {
var c coder = &Gopher{"Go"}
c.code()
c.debug()
}
上述代码运行结果如下
从表面上看,*Gopher 类型并没有实现 code 方法,但是因为 Gopher 类型实现了 code 方法,所以让 *Gopher 类型自动拥有了 code 方法。
但是我们把main函数的第一条语句换一下
func main() {
var c coder = Gopher{"Go"}
c.code()
c.debug()
}
运行一下,报如下错误
看出这两处代码的差别了吗?第一次是将 &Gopher 赋给了 coder;第二次则是将 Gopher 赋给了 coder。
第二次报错是说,Gopher 没有实现 coder。很明显了吧,因为 Gopher 类型并没有实现 debug 方法。
其实这个地方隐藏着一个"玄机",这段错误的官方解释为:“接收者是指针类型的方法,很可能在方法中会对接收者的属性进行更改操作,从而影响接收者;而对于接收者是值类型的方法,在方法中不会对接收者本身产生影响。”
通俗点讲就是:“当实现了一个接收者是值类型的方法,就可以自动生成一个接收者是对应指针类型的方法,因为两者都不会影响接收者。但是,当实现了一个接收者是指针类型的方法,如果此时自动生成一个接收者是值类型的方法,原本期望对接收者的改变(通过指针实现),现在无法实现,因为值类型会产生一个拷贝,不会真正影响调用者。”
其实关于这个解释,我个人觉得太繁琐,让我们仔细看一下main函数里的代码,我们可以发现,结构是赋值给了interface,所以我总结了一下: 1: 类型 *T 赋值给interface的可调用方法集包含接受者为 *T 或 T 的所有方法。 2: 类型T赋值给interface的可调用方法集包含接受者为T的所有方法。
如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。
总的来说如何选择就是以下两点: 1.设计不可变对象,用值接收。 2.其它用指针。 Tips:遇事不决用指针!!!
严格意义上讲,Golang并不是一门面向对象语言,它没有其它语言所谓的面向对象三要素,但是Golang通过了interface优雅的实现了面向对象的特性。
多态上一种运行期的行为,有以下几个特点。 1.一种类型具有多种类型的能力。 2.允许不同的对象对同一消息做出灵活的反应。 3.以一种通用的方式对待使用的对象。 4.非动态语言必须通过继承和接口的方式来实现。
让我们来看一个例子:
package main
import "fmt"
type Person interface {
job()
growUp()
}
type Student struct {
age int
}
type Programmer struct {
age int
}
func (p Student) job() {
fmt.Println("I am a student.")
return
}
func (p *Student) growUp() {
p.age += 1
return
}
func (p Programmer) job() {
fmt.Println("I am a programmer.")
return
}
func (p Programmer) growUp() {
// 程序员老得太快 ^_^
p.age += 10
return
}
func whatJob(p Person) {
p.job()
}
func growUp(p Person) {
p.growUp()
}
func main() {
tom := Student{age: 18}
whatJob(&tom)
growUp(&tom)
fmt.Println(tom)
sam := Programmer{age: 100}
whatJob(sam)
growUp(sam)
fmt.Println(sam)
}
首先定义了一个接口Person,该接口包含两个方法 job() 和 growUp()。然后定义了两个结构体,其都实现了了person接口。
main函数里,生成Student和Programmer 的对象,再将它们分别传入到函数whatJob和growUp。函数中,直接调用接口函数,实际执行的时候是看最终传入的实体类型是什么,调用的是实体类型实现的函数。于是,不同对象针对同一消息就有多种表现,多态就实现了。