Go语言入门——interface

Posted bigdatazj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go语言入门——interface相关的知识,希望对你有一定的参考价值。

 1、Go如何定义interface

  Go通过type声明一个接口,形如

type geometry interface 
    area() float64
    perim() float64

  和声明一个结构体一样,接口也是通过type声明。

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

  接口里面定义的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表示实现某个接口。

 

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

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

  类比上面的代码,rect就是长得像鸭子geometry的,可以像geometry一样的area()行为,也可以像geometry一样的perim(),rect满足了geometry定义的一切行为,所以我们推断rect就是实现了接口geometry的。

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

 

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

 

2、如何判定是否是某个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 := rectwidth: 3, height: 4
	c := circleradius: 5

	measure(r)
	measure(c)
    
	var g geometry
	g = circleradius: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。

 

  类型断言的语法为

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

<目标类型的值> := <表达式>.( 目标类型 )  //非安全类型断言

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

var g geometry
if f, ok := g.(circle); ok 
	fmt.Println("circle type", f)

   

3、值接收还是指针接收

  在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 := rectwidth: 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 := rectwidth: 3, height: 4
	c := circleradius: 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 := &rects1width: 3, height: 4
	c := &circleradius: 5

	measure(r)
	measure(c)

  此时调用一切正常。

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

 

关于原理,我看了很多说法
说法一

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

 

说法二

"当实现一个接收者是值类型的方法,就可以自动生成一个接收者是对应指针类型的方法,因为两者都不会影响接收者。但是,当实现了一个接收者是指针类型的方法,如果此时自动生成一个接收者是值类型的方法,原本期望对接收者的改变(通过指针实现),现在无法实现,因为值类型会产生一个拷贝,不会真正影响调用者。"

  但是这两种说法我觉得还是没有真正说到原理上,也可能是我没有理解。

  在前面不涉及到接口的单纯方法的值接收者和指针接收者,使用值或者指针调用都是可以的,因为Go会在底层做这个类型转换。但是在接口这个背景下,如果方法有指针类型接收类型,则只能传指针类型,可能还是和Go的接口底层实现有关。如果大家有自己的理解,欢迎指教。

 

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

如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!如果您想持续关注我的文章,请扫描二维码,关注JackieZheng的微信公众号,我会将我的文章推送给您,并和您一起分享我日常阅读过的优质文章。

技术图片

 

以上是关于Go语言入门——interface的主要内容,如果未能解决你的问题,请参考以下文章

go语言入门

go语言入门

Go语言入门

GO语言(十六):模糊测试入门(上)

go语言入门详细教程:时代下的 go 语言

Go语言入门