• 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 := rect{ 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。

     

      类型断言的语法为

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

      上面的写法是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 := rect{ 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{ 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{ 3, height: 4}
    	c := &circle{radius: 5}
    
    	measure(r)
    	measure(c)
    }

      此时调用一切正常。

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

     

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

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

    说法二

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

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

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

     

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

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

     

  • 相关阅读:
    ORM
    优酷:exec 补充,元类,优酷构架,ORM。
    数据库 事务。
    Python操作mysql
    数据库对表操作的练习题总结。
    ACM-ICPC 2018 南京赛区网络预赛 E. AC Challenge (状压DP)
    CodeForces
    CodeForces
    AcWing 314. 低买 (线性DP)打卡
    AcWing 313. 花店橱窗 (线性DP)打卡
  • 原文地址:https://www.cnblogs.com/bigdataZJ/p/go-interface.html
Copyright © 2020-2023  润新知