Go方法,接口简单总结 |青训营笔记

61 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第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类型的变量。