[golang]语法基础之接口

Posted liujunhang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[golang]语法基础之接口相关的知识,希望对你有一定的参考价值。

说明

接口可以理解为是定了一种约定,是一个较为抽象的类型。和之前说过的具体的类型例如string、map等是不一样的。一般来说,具体的类型,我们可以知道它是什么,并且可以知道它可以用来做什么。

但是对于接口来说,接口是抽象的,只有一组接口方法,我们完全不需要关心接口当中的这些方法是如何实现的,我们只需要知道这些方法可以做什么就好。

抽象两个字就可以形容接口的优势,定义和使用接口不需要和具体的实现细节绑定到一起,我们只需要定义接口,告诉开发的人它可以做什么,这样就可以将具体的实现拆分开,让编码变得更加的灵活。

例如:

func main() {
    var b bytes.Buffer
    fmt.Fprint(&b,"Hello World")
    fmt.Println(b.String())
}

以上的代码就是一个使用接口的例子,我们可以看下fmt.Fprint函数的实现。

func Fprint(w io.Writer, a ...interface{}) (n int, err error) {
    p := newPrinter()
    p.doPrint(a)
    n, err = w.Write(p.buf)
    p.free()
    return
}

从上面的源代码中,我们可以看到,fmt.Fprint函数的第一个参数是io.Writer这个接口,所以只要实现了这个接口的具体类型都可以作为参数传递给fmt.Fprint函数,而bytes.Buffer恰恰实现了io.Writer接口,所以可以作为参数传递给fmt.Fprint函数。

内部实现

接口主要是用来定义行为的类型,它是抽象的,这些定义的行为不是由接口直接实现,而是通过方法由用户定义的类型实现。如果用户定义的类型,实现了接口类型声明的所有方法,那么这个用户定义的类型就实现了这个接口,所以这个用户定义类型的值就可以赋值给接口类型的值。

// 定义一个human接口 包括run方法和say方法
type human interface {
    run()
    say()
}

// 定义一个person结构体
type person struct {
    name string
}
// 实现person结构体的run方法
func (p person) run() {
    fmt.Println("跑步中...")
}
// 实现person结构体的say方法
func (p person) say() {
    fmt.Println("聊天中....")
}

func main() {
    // 创建一个p1变量类型为person并且同时初始化
    p1 := person{name:"张三"}
    // 因为person结构体已经实现了human接口定义的两个方法,所以可以把p1 这个变量赋值给zhangsan这个hunman类型的变量
    var zhangsan human = p1
    // 通过zhangsan这个变量可以调用run方法
    zhangsan.run()
}

在上面的这个例子中,因为person实现了接口human定义的方法,所以我们可以通过var zhangsan human=p1 赋值。这个赋值的操作会把定义类型的值存入接口类型的值。

赋值操作成功后,如果我们对接口方法执行调用,其实是调用存储的用户定义类型的对应方法(在上面的例子中是调用p1当中的方法),这里我们可以把用户定义的类型称之为实体类型

我们可以定义很多的类型,让他们实现一个接口,那么这些类型都可以赋值给这个接口,这时候接口方法的调用,其实就是对应实体类型对应方法的调用,这就是多态。

type cat struct {}

type dog struct {}

// 定义一个接口
type animal interface {
    run()
}

func (c cat) run() {
    fmt.Println("猫在跑")
}

func (d dog) run() {
    fmt.Println("狗在跑")
}

func main() {
    var c cat
    var d dog

    var a animal

    a = c
    a.run() // 猫在跑

    a = d
    a.run() // 狗在跑

}

上面的例子中演示了一个多态,我们定义了一个接口animal,然后定义了两种类型cat 和 dog,并且让这两种类型实现了接口animal。在使用的时候,分别把类型cat 的值c 、类型dog的值d赋值给接口animal的值a ,然后分别执行a的run方法,可以得到不同的结果。

需要知道的是,接口的值是一个两个字长度的数据结构,第一个字包含一个指向内部表结构的指针,这个内部表里存储的是有实体类型的信息以及相关联的方法集;第二个字包含的是一个指向存储的实体类型值的指针。所以接口的值结构其实是两个指针,这也可以说明接口其实是一个引用类型。

方法集

我们都知道,如果要实现一个接口,必须实现这个接口提供的所有方法,但是实现方法的时候,我们可以使用指针接收者实现,也可以使用值接收者实现,这两者是有区别的,下面我们就好好分析下这两者的区别。

package main

import "fmt"

// 定义一个接口
type animal interface {
    run()
}

type cat struct {}

func (c cat) run() {
    fmt.Println("running...")
}


func myAnimal(a animal) {
    a.run()
}

func main() {
    var c cat
    myAnimal(c)
}

在上面的代码中,和上面的示例类似,但是增加了一个myAnimal函数,这个函数接收一个animal接口类型的参数。例子中传递参数的时候,也是以类型cat的值c传递的,运行程序可以正常执行。

我们再来把代码稍微改一下:

func main() {
    var c cat
    myAnimal(&c)
}

修改了这一处之后,运行程序,程序依然可以执行。从而,我们可以得出一个结论:实体类型以值接收者实现接口的时候,不管是实体类型的值,还是实体类型的指针,都实现了该接口

接下来我们尝试把接收者改为指针试试:

package main

import "fmt"

// 定义一个接口
type animal interface {
    run()
}

type cat struct {}

func (c *cat) run() {
    fmt.Println("running...")
}


func myAnimal(a animal) {
    a.run()
}

func main() {
    var c cat
    myAnimal(c)
}

在上面的代码中,我们把实现接口的接收者改为了指针,但是传递参数的时候,我们还是按照值进行传递,运行程序,会出现以下的异常提示:

cannot use c (type cat) as type animal in argument to myAnimal:
cat does not implement animal (run method has pointer receiver)

在上面的提示中,已经告诉我们,说cat没有实现animal接口,因为run方法有一个指针接收者,所以cat类型的值c不能作为接口类型animal传参使用。

我们可以把程序稍稍修改,改为以指针作为参数传递。

func main() {
    var c cat
    myAnimal(&c)
}

上面的代码更改完毕之后,程序就可以正常运行了。

由此可见实体类型以指针接收者实现接口的时候,只有指向这个类型的指针才被认为实现了该接口。

下面我们来总结下两种规则,首先以方法接收者是值还是指针的角度看。

Methods Receivers Values
(t T) T and *T
(t *T) *T

上面的表格可以解读为:如果是值接收者,实体类型的值和指针都可以实现对应的接口;如果是指针接收者,那么只有类型的指针能够实现对应的接口。

其次我们我们以实体类型是值还是指针的角度看。

Values Methods Receivers
T (t T)
*T (t T) and (t *T)

上面的表格可以解读为:类型的值只能实现值接收者的接口;指向类型的指针,既可以实现值接收者的接口,也可以实现指针接收者的接口。

以上是关于[golang]语法基础之接口的主要内容,如果未能解决你的问题,请参考以下文章

[golang] 语法基础之结构体

[golang]语法基础之构造函数

Golang 学习之路

Golang 序列化之 ProtoBuf

golang 目录

Golang基础教程