这是我参与「第五届青训营 」伴学笔记创作活动的第4天
方法
方法一般是面向对象编程(OOP)的一个特性,在C++语言中方法对应一个类对象的成员函数,是关联到具体对象上的虚表中的。但是Go语言的方法却是关联到类型的,这样可以在编译阶段完成方法的静态绑定。一个面向对象的程序会用方法来表达其属性对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情。
// 文件对象
type File struct {
fd int
}
// 打开文件
func OpenFile(name string) (f *File, err error) {
// ...
}
// 关闭文件
func CloseFile(f *File) error {
// ...
}
// 读文件数据
func ReadFile(f *File, offset int64, data []byte) int {
// ...
}
以上的三个函数都是普通的函数,需要占用包级空间中的名字资源。不过CloseFile和ReadFile函数只是针对File类型对象的操作,这时候我们更希望这类函数和操作对象的类型紧密绑定在一起。
在go语言中修改如下:
// 关闭文件
func (f *File) CloseFile() error {
// ...
}
// 读文件数据
func (f *File) ReadFile(offset int64, data []byte) int {
// ...
}
将CloseFile和ReadFile函数的第一个参数移动到函数名的开头,这两个函数就成了File类型独有的方法了(而不是File对象方法)
从代码角度看虽然只是一个小的改动,但是从编程哲学角度来看,Go语言已经是进入面向对象语言的行列了。我们可以给任何自定义类型添加一个或多个方法。每种类型对应的方法必须和类型的定义在同一个包中,因此是无法给int这类内置类型添加方法的(因为方法的定义和类型的定义不在一个包中)。对于给定的类型,每个方法的名字必须是唯一的,同时方法和函数一样也不支持重载。
接口
定义
Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
Go的接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让对象更加灵活和更具有适应能力。很多面向对象的语言都有相似的接口概念,但Go语言中接口类型的独特之处在于它是满足隐式实现的鸭子类型。
所谓鸭子类型说的是:只要走起路来像鸭子、叫起来也像鸭子,那么就可以把它当作鸭子。Go语言中的面向对象就是如此,如果一个对象只要看起来像是某种接口类型的实现,那么它就可以作为该接口类型使用。
就比如说在c语言中,使用printf在终端输出的时候只能输出有限类型的几个变量,而在go中可以使用fmt.Printf,实际上是fmt.Fprintf向任意自定义的输出流对象打印,甚至可以打印到网络甚至是压缩文件,同时打印的数据不限于语言内置的基础类型,任意隐士满足fmt.Stringer接口的对象都可以打印,不满足fmt.Stringer接口的依然可以通过反射的技术打印。
在go语言中,接口是一种就是一种类型。同map、struct等的定义,语法是一样。
type People interface {
Speak(string) string
}
关键字是interface。此时就称People是一个接口。接口内只有方法声明,没有方法的实现。
接口的作用就是定义一些对象的共同特征,特征以接口内的方法来定义。如上例的speak方法。
具有上述特征的类型,那就称它实现了这个接口。语法如下:
// 定义类型Student,它是一种struct类型
type Student struct{}
// 类型Student实现People特征 (也就是让它实现People中的方法)
func (stu Student) Speak(think string) (talk string) {
if think == "hello world" {
talk = "你好,世界"
} else {
talk = "您好"
}
return
}
var peo People = Student{}
think := "hello"
fmt.Println(peo.Speak(think))
//上面代码输出:您好。
除了值类型能实现接口,指针类型也能实现接口,所谓指针类型实现是指如上面的speak实现,接收者改成指针声明。如下:
type Doctor struct{}
func (stu *Doctor) Speak(think string) (talk string) {
if think == "check" {
talk = "检查"
} else {
talk = "治疗"
}
return
}
同一种类型不能有两个同名的方法实现,指针和值接收者也算同一种类型,如上面有student值接收者实现了speak,不能再有student指针接收者实现speak。
值接收者实现和指针接收者实现的区别:
func main() {
var peo People = Student{}
think := "hello"
fmt.Println(peo.Speak(think))
var peo2 People = &Student{}
fmt.Println(peo2.Speak("hello world"))
//var peo3 People = Doctor{}
//fmt.Println(peo3.Speak("money"))
var peo4 People = &Doctor{}
fmt.Println(peo4.Speak("check"))
}
注意,Doctor{}不能赋值给People类型的变量,会报错:cannot use Doctor{} (type Doctor) as type People in assignment:Doctor does not implement People (Speak method has pointer receiver)。意思就是Doctor对象并没有实现People接口。也就是说:值接收者实现接口时,对象和其指针都能赋值给相应的接口类型,而指针接收者实现的对象只能把其指针赋值给相应的接口类型。 一个类型可以实现多个接口,比如学生还能实现孩子接口,拥有学习的特征
type Children interface {
Study(string) string
}
func (stu Student) Study(book string) (obtain string) {
if book == "math" {
obtain = "计算"
} else {
obtain = "其他"
}
return
}
func main(){
var peo2 People = &Student{}
fmt.Println(peo2.Speak("hello world"))
var peo5 Children = Student{}
fmt.Println(peo5.Study("math"))
}
接口嵌套
定义一个接口的时候可以嵌套别的接口,这样就会拥有该接口的特征。
type Boy interface {
People
Children
}
func main(){
var xiaoming Boy = Student{}
fmt.Println(xiaoming.Speak("hello world"))
fmt.Println(xiaoming.Study("english"))
}
如上面例子中的Boy。它嵌套了两个接口,拥有了这两个接口的特征Speak和Study。而Student类型有实现这两个特征,也就相当于实现了Boy接口,所以Student对象可以赋值给Boy类型的变量。