Go语言入门——interface

一、Go如何定义interface

Go经过type声明一个接口,形如编程

type geometry interface {
    area() float64
    perim() float64
}
复制代码

和声明一个结构体同样,接口也是经过type声明。安全

type后面是接口名称,紧挨着是关键字interface。bash

接口里面定义的area()和perim是接口geometry的方法。框架

有了接口,那应该如何实现接口呢?编程语言

type rect struct {
    width, height float64
}

func (r rect) area() float64 {
    return r*width*height
}

func (r rect) perim() float64 {
    return 2*r*width + 2*r*height
}
复制代码

上面就是rect实现接口geometry的代码。不一样于Java这些语言,有显式的关键字如implement表示实现某个接口。ui

和Java接口的契约精神有些不一样的是,Go里面的接口实现更像是组合的概念。spa

这里要提一个”鸭子类型“的概念。鸭子类型是动态编程语言的一种对象推断策略,它更关注对象能如何被使用,而不是对象的类型自己。即一个东西若是长得像鸭子,会像鸭子同样嘎嘎叫、走路、游泳,那么咱们就能够推断这个小东西就是鸭子。指针

类比上面的代码,rect就是长得像鸭子geometry的,能够像geometry同样的area()行为,也能够像geometry同样的perim(),rect知足了geometry定义的一切行为,因此咱们推断rect就是实现了接口geometry的。code

这样,咱们不用再去写implement xxx这样的代码了。由原来一个类的粒度细化到类里面方法的粒度了。对象

顺便提一句,以前在作Java开发的时候,因为是协同开发,都是用统一的框架,加上面向接口编程的思想深刻人心,以致于成为这样的一种条件反射:在写一个service的时候,第一反应是新建一个接口,而后定义接口中方法,以后再是编写实现类,绝大多数状况,都是只会用到这一个实现类,将来很长时间都没有看到这个接口的其余实现类。这种为了实现接口而编写接口,有时候在中小型项目中让代码显得很死板。

二、如何断定是不是某个interface的实现

上面咱们介绍了Go是如何定义一个接口并”实现“接口的。上面代码只有一个rect结构体,若是有多个呢

type rect struct {
    width, height float64
}

func (r rect) area() float64 {
    return r*width*height
}

func (r rect) perim() float64 {
    return 2*r*width + 2*r*height
}

type circle struct {
    radius float64
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
    return 2 * math.Pi * c.radius
}
复制代码

对于这种状况,咱们总不能一个个肉眼比对,看看rect、circle是否实现了geometry中定义的全部方法吧

Go能够经过类型断言来断定。

func main() {
	r := rect{width: 3, height: 4}
	c := circle{radius: 5}

	measure(r)
	measure(c)
    
	var g geometry
	g = circle{radius:10}
	switch t := g.(type) {
	case circle:
		fmt.Println("circle type", t)
	case rect:
		fmt.Println("rect type", t)
	}
}

复制代码

执行结果为

{3 4}
12
14
{5}
78.53981633974483
31.41592653589793
circle type {10}
复制代码

能够看出,Go能够推断g是实现了geometry接口的circle。

类型断言的语法为

<目标类型的值>,<布尔参数> := <表达式>.( 目标类型 ) // 安全类型断言

<目标类型的值> := <表达式>.( 目标类型 )&emsp;&emsp;//非安全类型断言
复制代码

上面的写法是switch语法,即第二种。第一种举例以下

var g geometry
if f, ok := g.(circle); ok {
		fmt.Println("circle type", f)
}
复制代码

三、值接收仍是指针接收

在Go中一个方法,咱们能够定义一个方法是用某个struct的值来接收仍是指针接收,形如

type rect struct {
	width, height int
}

func (r *rect) area() int {
	return r.width * r.height
}

func (r rect) perim() int {
	return 2*r.width + 2*r.height
}

func main() {
	r := rect{width: 10, height: 20}
	fmt.Println("area:", r.area())
	fmt.Println("perim:", r.perim())

	rp := &r
	fmt.Println("area:", rp.area())
	fmt.Println("perim:", rp.perim())
}
复制代码

这里定义结构体rect,同时定义两个方法area()和perim()。在这两个方法左边定义的即为方法的接收者,其中area()由rect的指针类型接收,perim()则由rect值类型接收。

这样表示area()和perim()是rect的两个方法。从代码咱们能够看出,该种形式不论是传入值类型仍是传入rect的指针,执行都正常返回结果。

area: 200
perim: 60
area: 200
perim: 60
复制代码

对于r.area()能够调通的背后Go作了什么?

此时r是一个值类型,为了实现调用即便是指针接收类型的area()方法,Go实际是先找到r的地址,而后经过一个指针指向它,即r.area()转化成了(&r).area(),从而知足了area()方法是指针接收者的约束。

对于rp.perim()能够调通的背后Go作了什么?

此时rp是一个指针类型。在调用时,指针被解引用为值,这样便符合perim()方法定义的接收者类型的约束。解引用的过程咱们能够认为Go把rp.perim()转化为于(*rp).perim()。可是注意perim()方法是值接收类型,因此操做的是rect的副本。

因此,综上,对于普通方法的调用,无论接收者是值类型仍是指针类型,调用者是值类型仍是指针类型,均可以调通。

上面是针对纯粹的方法而言的,若是在接口的背景下,状况是否一致呢?

type geometry interface {
	area() float64
	perim() float64
}

type rect struct {
	width, height float64
}

type circle struct {
	radius float64
}

func (r rect) area() float64 {
	return r.width * r.height
}

func (r rect) perim() float64 {
	return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
	return math.Pi * c.radius * c.radius
}

func (c *circle) perim() float64 {
	return 2 * math.Pi * c.radius
}

func measure(g geometry) {
	fmt.Println(g)
	fmt.Println(g.area())
	fmt.Println(g.perim())
}

func main() {
	r := rect{width: 3, height: 4}
	c := circle{radius: 5}

	measure(r)
	measure(c)
}
复制代码

这段的代码与上面的惟一不一样的地方在于将perim接收者类型由circle改成了*circle类型,致使在运行程序时报错

# command-line-arguments
main/src/examples/interfaces.go:48:9: cannot use c (type circle) as type geometry in argument to measure:
	circle does not implement geometry (perim method has pointer receiver)
复制代码

意思是说circle没有实现geometry接口。

若是反过来

type geometry interface {
	area() float64
	perim() float64
}

type rect struct {
	width, height float64
}

type circle struct {
	radius float64
}

func (r rect) area() float64 {
	return r.width * r.height
}

func (r rect) perim() float64 {
	return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
	return math.Pi * c.radius * c.radius
}

func (c *circle) perim() float64 {
	return 2 * math.Pi * c.radius
}

func measure(g geometry) {
	fmt.Println(g)
	fmt.Println(g.area())
	fmt.Println(g.perim())
}

func main() {
	r := &rects1{width: 3, height: 4}
	c := &circle{radius: 5}

	measure(r)
	measure(c)
}
复制代码

此时调用一切正常。

因此对比看下来发现,对于值接收者,传如值或者指针均可以正常调用;对于指针接收者,则只能传入指针类型,不然会报未实现接口的错误。

关于原理,我看了不少说法 说法一

对于指针类型,Go会自动转换,由于有了指针老是能获得指针指向的值是什么,若是是 value 调用,go 将无从得知 value 的原始值是什么,由于 value 是份拷贝。go 会把指针进行隐式转换获得 value,但反过来则不行。

说法二

当实现一个接收者是值类型的方法,就能够自动生成一个接收者是对应指针类型的方法,由于二者都不会影响接收者。可是,当实现了一个接收者是指针类型的方法,若是此时自动生成一个接收者是值类型的方法,本来指望对接收者的改变(经过指针实现),如今没法实现,由于值类型会产生一个拷贝,不会真正影响调用者。

可是这两种说法我以为仍是没有真正说到原理上,也多是我没有理解。

在前面不涉及到接口的单纯方法的值接收者和指针接收者,使用值或者指针调用都是能够的,由于Go会在底层作这个类型转换。可是在接口这个背景下,若是方法有指针类型接收类型,则只能传指针类型,我以为仍是和接口有关。若是你们有本身的理解,欢迎指教。

今天主要介绍了Go语言中的接口的定义和实现以及如何使用,还有一些小知识点好比空interface的做用和使用就再也不赘述。

相关文章
相关标签/搜索