Go语言接口interface深入理解

接口是什么?

Go 语言不是一种 “传统” 的面向对象编程语言:它里面没有类和继承的概念。

但是 Go 语言里有非常灵活的 接口 概念,通过它可以实现很多面向对象的特性。接口提供了一种方式来 说明 对象的行为:如果谁能搞定这件事,它就可以用在这儿。

简单的说,interface是一组method的组合,我们通过interface来定义对象的一组行为,但是这些method接口不需要去实现,并且interface不能包含任何变量。到某个自定义类型(比如结构体Human)要使用的时候,再根据具体情况把这些方法写出来(实现这个方法)。

接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。

通过如下格式定义接口:

type 接口名 interface {
    Method1(param_list) return_type
    Method2(param_list) return_type
    ...
}

示例:

//定义一个人类接口
type Human interface {
    Run() //奔跑
    Laugh() //笑
    Speak(words string) //说话
}

interface类型

interface类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了该接口。

示例:

package main

import (
    "fmt"
)
//定义三个接口
type Men interface {
    SayHi()
    Sing(lyrics string)
    Guzzle(beerStein string)
}

type YoungChap interface {
    SayHi()
    Sing(song string)
    BorrowMoney(amount float32)
}

type ElderlyGent interface {
    SayHi()
    Sing(song string)
    SpendSalary(amount float32)
}

//人类
type Human struct {
    name  string
    age   int
    phone string
}

func (h *Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

func (h *Human) Sing(lyrics string) {
    fmt.Println("La la, la la la, la la la la...", lyrics)
}

func (h *Human) Guzzle(beerStein string) {
    fmt.Println("Guzzle Guzzle Guzzle...", beerStein)
}

//学生
type Student struct {
    Human
    school string
    loan   float32
}
func (s *Student) BorrowMoney(amount float32) {
    s.loan += amount
}

//雇员
type Employee struct {
    Human
    company string
    money   float32
}

func (e *Employee) SayHi() {
    fmt.Printf("Hi, I am %s, I work at %s, Call me on %s", e.name, e.company, e.phone)
}

func (e *Employee) SpendSalary(amount float32) {
    e.money -= amount
}


func main() {

    var tmp Men = &Student{Human{"itbsl", 23, "176xxxx3456"}, "北京邮电大学", 34}

    fmt.Println(tmp.(*Student).name)
}

输出结果:

Hi, I am itbsl you can call me on 176xxxx3456

通过上面的代码我们可以看到,interface可以被任意的对象实现。我们看到上面的Men interface被Human、Student和Employee实现。同理,一个对象可以实现任意多个interface,例如上面的Student实现了Men和YoungChap两个interface。

注: 上面实现接口类型的都是各个结构体的指针类型,所以接口变量存储的也只能是各个结构体类型的指针类型。如果实现接口的是各个结构体的值类型,那么接口变量能存储的就是指类型。

只要某个对象实现某个接口,那么该接口类型的变量可以用来存储该对象。上面我们把Student实例赋值给了一个Men类型的接口变量。

不仅仅是结构体类型,只要是自定义数据类型,就可以实现接口

typ Test interface {
    Say()
}
type integer int

func(i integer) Say() {
    fmt.Println("integer Say i = ", i)
}

var i integer = 10
var b Test = i
i.Say()

空接口(interface{})

空interface(interface{})不包含任何的method,正因为如此,所有的类型都实现了空interface。空interface对于描述起不到任何的作用(因为它不包含任何的method),但是空interface在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*类型。

示例:

// 定义a为空接口
var a interface{}
var i int = 5
s := "Hello world"
// a可以存储任意类型的数值
a = i
a = s

一个函数把interface{}作为参数,那么他可以接受任意类型的值作为参数,如果一个函数返回interface{},那么也就可以返回任意类型的值。是不是很有用啊!

interface函数参数

interface的变量可以持有任意实现该interface类型的对象,这给我们编写函数(包括method)提供了一些额外的思考,我们是不是可以通过定义interface参数,让函数接受各种类型的参数。

举个例子:fmt.Println是我们常用的一个函数,但是你是否注意到它可以接受任意类型的数据。打开fmt的源码文件,你会看到这样一个定义:

type Stringer interface {
     String() string
}

也就是说,任何实现了String方法的类型都能作为参数被fmt.Println调用,让我们来试一试

package main
import (
    "fmt"
    "strconv"
)
type Human struct {
    name string
    age int
    phone string
}
// 通过这个方法 Human 实现了 fmt.Stringer
func (h Human) String() string {
    return "❰"+h.name+" - "+strconv.Itoa(h.age)+" years -  ✆ " +h.phone+"❱"
}
func main() {
    Bob := Human{"Bob", 39, "000-7777-XXX"}
    fmt.Println("This Human is : ", Bob)
}

现在我们再回顾一下前面的Box示例,你会发现Color结构也定义了一个method:String。其实这也是实现了fmt.Stringer这个interface,即如果需要某个类型能被fmt包以特殊的格式输出,你就必须实现Stringer这个接口。如果没有实现这个接口,fmt将以默认的方式输出。

//实现同样的功能
fmt.Println("The biggest one is", boxes.BiggestsColor().String())
fmt.Println("The biggest one is", boxes.BiggestsColor())

注: 实现了error接口的对象(即实现了Error() string的对象),使用fmt输出时,会调用Error()方法,因此不必再定义String()方法了。

interface变量存储的类型

我们知道interface的变量里面可以存储任意类型的数值(该类型实现了interface)。那么我们怎么反向知道这个变量里面实际保存了的是哪个类型的对象呢?

类型断言

类型断言的语法就是用一个接口类型变量采用接口变量.(具体类型)的方式进行转换

类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言, 具体的如下:

package main

import "fmt"

func main() {

    var x interface{}

    var flo float32 = 6.66

    x = flo

    y := x.(float32)

    fmt.Printf("y的类型是 %T 值是 %v", y, y)

}
//输出结果如下
//y的类型是 float32 值是 6.66

在进行类型断言时,如果类型不匹配,就会报 panic, 因此进行类型断言时,要确保原来的空接口指向的就是断言的类型.
如何在进行断言时,带上检测机制,如果成功就 ok,否则也不要报 panic,代码如下

package main

import "fmt"

func main() {

    var x interface{}

    var flo float32 = 6.66

    x = flo

    if y, ok := x.(float64); ok {
        fmt.Println("convert success")
        fmt.Printf("y的类型是 %T 值是 %v", y, y)
    } else {
        fmt.Println("convert fail")
    }

    fmt.Println("继续执行...")
}

我们之前常用的typ-switch就是采用的类型断言

switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。

package main

import "fmt"

func main() {
   var x interface{}
     
   switch i := x.(type) {
      case nil:   
         fmt.Printf(" x 的类型 :%T",i)                
      case int:   
         fmt.Printf("x 是 int 型")                       
      case float64:
         fmt.Printf("x 是 float64 型")           
      case func(int) float64:
         fmt.Printf("x 是 func(int) 型")                      
      case bool, string:
         fmt.Printf("x 是 bool 或 string 型" )       
      default:
         fmt.Printf("未知型")     
   }   
}

嵌入interface

Go里面真正吸引人的是它内置的逻辑语法,就像我们在学习Struct时学习的匿名字段,多么的优雅啊,那么相同的逻辑引入到interface里面,那不是更加完美了。如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method。

我们可以看到源码包container/heap里面有这样的一个定义

type Interface interface {
    sort.Interface //嵌入字段sort.Interface
    Push(x interface{}) //a Push method to push elements into the heap
    Pop() interface{} //a Pop elements that pops elements from the heap
}

我们看到sort.Interface其实就是嵌入字段,把sort.Interface的所有method给隐式的包含进来了。也就是下面三个方法:

type Interface interface {
    // Len is the number of elements in the collection.
    Len() int
    // Less returns whether the element with index i should sort
    // before the element with index j.
    Less(i, j int) bool
    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)
}

另一个例子就是io包下面的 io.ReadWriter ,它包含了io包下面的Reader和Writer两个interface:

相关推荐