python生成c语言代码_《go语言从入门到入坟》聊一聊go语言中的接口

楔子

如果用了go一段时间之后,肯定会有人发现一个问题:那就是go对类型的检查太严格了。当然这是一件好事,可以避免我们犯错误,但是有些时候我们需要一个变量能够接收不同类型的值。比如在定义函数参数的时候,我们希望这个参数可以接收多种类型的值,那么这个时候该怎么做呢?很简单,go为我们提供了interface{}。

鸭子类型

先来看看鸭子类型的定义:

如果某个东西长得像鸭子,像鸭子一样游泳,像鸭子一样嘎嘎叫,那它就可以被看成是一只鸭子。

Duck Typing,鸭子类型,是动态编程语言的一种对象推断策略,它更关注对象能如何被使用,而不是对象的类型本身。Go 语言作为一门静态语言,它通过通过接口interface{}的方式完美支持鸭子类型。

笔者本人是主Python的,如果在Python中我们可以定义一个这样的函数:

def say_hi(obj):

return obj.hi()

在调用该函数的时候,你可以传入任意类型,那么Python解释器是如何做的呢?

1. 首先Python中的变量都是一个泛型指针PyObject *;

2. 当执行obj.hi的时候, 解释器会调用PyObject_GenericGetAttr(obj, "hi")在obj的属性字典中进行属性查找, 返回"hi"对应的value, 结果也是一个PyObject *; 如果没有找到的话, 那么会抛出AttributeError;

3. 找到之后再通过PyObject_CallObject进行调用;

所以我们看到给obj参数传递什么数据根本无关紧要,只要传递的变量可以调用hi即可,而且这一步是在运行时才发生的;换言之,如果报属性错误一定是在运行时报的错。

而对于静态语言而言,比如:C++,必须要显示地声明实现了某个接口之后,才能用在任何需要这个接口的地方。如果你在程序中调用 hello_world 函数,却传入了一个根本就没有实现 hello_world() 的类型,那在编译阶段就不会通过。这也是静态语言比动态语言更安全的原因。

动态语言和静态语言的差别在此就有所体现:静态语言在编译期间就能发现类型不匹配的错误,不像动态语言,必须要运行到那一行代码才会报错。当然,静态语言要求程序员在编码阶段就要按照规定来编写程序,为每个变量规定数据类型,这在某种程度上,加大了工作量,也加长了代码量。动态语言则没有这些要求,可以让人更专注在业务上,代码也更短,写起来更快。所以有优势就有劣势,鱼和熊掌往往是不可兼得的。

Go 语言作为一门现代静态语言,是有后发优势的。它引入了动态语言的便利,同时又会进行静态语言的类型检查,写起来是非常 Happy 的。Go 采用了折中的做法:不要求类型显示地声明实现了某个接口,只要实现了相关的方法,编译器就能检测到。

我们举个栗子:

package main

import "fmt"

// 定义一个接口people, 和接口中的函数:

type people interface {

say(word string)

}

// 定义两个结构体

type girl struct {name string}

type boy struct {name string}

// 我们并没有将girl显示地声明为借口people类型, 只要实现了接口内部的方法

// 那么编译器就会隐式地将其转成people类型

func (g girl) say(s string) {

fmt.Printf("%s say\n", g.name)

}

func (b boy) say(s string) {

fmt.Printf("%s say\n", b.name)

}

// 声明一个函数

func common_say(p people, word string) {

p.say(word)

}

func main() {

g := girl{"mashiro"}

b := boy{"sorata"}

g.say("hello") // mashiro say

b.say("hello") // sorata say

}

所以go里面如果想实现某个接口,不需要显示的声明,只需要实现对应接口中的方法即可。

顺带再提一下动态语言的特点:

变量指向的对象的类型是不确定的, 在运行期间才能确定;

函数和方法可以接收任何类型的参数, 且调用时不检查参数类型、不需要实现接口;

总结一下,鸭子类型是一种动态语言的风格,在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口决定,而是由它 "当前方法和属性的集合" 决定。Go 作为一种静态语言,通过接口实现了 鸭子类型,实际上是 Go 的编译器在其中作了隐匿的转换工作。

值接收者和指针接收者的区别

方法能给用户自定义的类型添加新的行为。它和函数的区别在于方法有一个接收者,给一个函数添加一个接收者,那么它就变成了方法。接收者可以是值接收者,也可以是指针接收者。

在调用方法的时候,值类型既可以调用值接收者的方法,也可以调用指针接收者的方法;指针类型既可以调用指针接收者的方法,也可以调用值接收者的方法。也就是说,不管方法的接收者是什么类型,该类型的值和指针都可以调用,不必严格符合接收者的类型。

举个栗子:

package main

import "fmt"

type girl struct {

age int

}

func (g girl) ageIncr1() {

g.age++

}

func (g *girl) ageDecr1() {

g.age--

}

func main() {

g1 := girl{16}

//值类型调用者 调用 值类型接收者的方法

g1.ageIncr1()

fmt.Println(g1.age) // 16

g2 := girl{16}

//值类型调用者 调用 指针类型接收者的方法

g2.ageDecr1()

fmt.Println(g2.age) // 15

g3 := &girl{16}

//指针类型调用者 调用 值类型接收者的方法

g3.ageIncr1()

fmt.Println(g3.age) // 16

g4 := &girl{16}

//指针类型调用者 调用 值类型接收者的方法

g4.ageDecr1()

fmt.Println(g4.age) // 15

}

我们暂时先不看结果,总之我们目前可以得出:不管接收者是什么类型,该类型的值和指针都可以调用。

但是为什么一个变了,一个没变呢?这是因为go里面变量传递都是值传递,我们还是先来说说调用方法时背后都发生了些什么吧。

实际上,当 调用方法的类型 和 方法的接收者类型 不同时,其实是编译器在背后做了一些工作,用一个表格来呈现:

1229382-20201101000548973-1660175850.png

所以正如前面所说,不管接收者类型是值类型还是指针类型,都可以通过 值调用者 或 指针调用者 进行调用,这里面实际上通过语法糖起作用的。

如果是值接收者, 指针类型调用, 那么会通过 *指针 的方式调用, 并将值拷贝一份;

如果是指针接收者, 值类型调用, 那么会通过 &值 的方式调用, 并将指针拷贝一份;

因此在调用之后age是否改变,取决于接收者到底是值类型还是指针类型,与调用者无关,因为go编译器会进行转化。

但是问题来了,如果值接收者实现了一个方法,那么相同类型的指针接收者可不可以实现相同的方法呢?可以自己测试一下,答案是不行的。

因为不管是值还是指针,它们都是同一类型的值和指针。

在实现接口的时候,它们也是有区别的,举个栗子:

package main

import "fmt"

type people interface {

a()

b()

}

type girl struct {}

// 如果想实现某个接口, 那么只需要实现该接口中的方法即可

func (g girl) a() {

fmt.Println("girl -> a")

}

func (g *girl) b() {

fmt.Println("girl -> b")

}

// 但是我们看到方法a是值接收者实现的, 方法b是指针接收者实现的

func main() {

g := girl{}

var p people

// 但是此时将g赋值给p是报错的, 因为没有实现people中的所有方法

// 而指针可以

p = &g

p.a() //girl -> a

p.b() //girl -> b

}

所以区别如下:

实现了接收者是值类型的方法, 相当于自动实现了接口中接收者是指针类型的方法;

而实现了接收者是指针类型的方法, 不会自动生成接口中对应接收者是值类型的方法;

因此 girl 实现了 方法 a,会自动让 *girl 也实现了 方法 a;但是*girl实现了 方法 b,不代表 girl 也实现了 方法 b。

p = g

如果此时将g强行赋值给p,那么会出现如下编译错误。

# command-line-arguments

.\1.go:28:7: cannot use g (type girl) as type people in assignment:

girl does not implement people (b method has pointer receiver)

报错信息给的很详细,不能将 girl类型的 变量g 作为people类型进行赋值,因为它没有实现people这个接口,括号里面提示:方法b的接收者是指针,并不会让值接收也拥有方法b。因此girl只实现了接口中的一个方法,所以它没有实现该接口的全部方法,因此不能赋值。

至于为什么会有这么一个设计,原因很简单:

接收者是指针类型的方法,很可能在方法中会对接收者的属性进行更改操作,从而影响接收者;而对于接收者是值类型的方法,在方法中不会对接收者本身产生影响。

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

因此,只要记住下面这点就可以了:

如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。

那么问题又来了,我实现方法的时候到底应该采用值接收者还是指针接收者呢?

如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。

使用指针作为方法的接收者的理由:

方法能够修改接收者指向的值;

避免在每次调用方法时复制该值, 在值的类型为大型结构体时, 这样做会更加高效;

但是判断使用值接收者还是指针接收者,不是由该方法是否修改了调用者(也就是接收者)来决定,而是应该基于该类型的本质。

如果类型具备 "原始的本质",也就是说它的成员都是由 go 语言里内置的原始类型,如字符串,整型值等,那就定义值接收者类型的方法。像内置的符合类型,如 slice,map,interface,channel,这些类型比较特殊,声明他们的时候,实际上是创建了一个 header, 对于他们也是直接定义值接收者类型的方法。这样,调用函数时,是直接 copy 了这些类型的 header,而 header 本身就是为复制设计的。

如果类型具备非原始的本质,不能被安全地复制,这种类型总是应该被共享,那就定义指针接收者的方法。比如 go 源码里的文件结构体(struct File)就不应该被复制,应该只有一份实体。

类型转换与断言

我们知道,Go 语言中不允许隐式类型转换,也就是说 = 两边,不允许出现类型不相同的变量。类型转换、类型断言本质都是把一个类型转换成另外一个类型。不同之处在于,类型断言是对接口变量进行的操作。

类型转换

对于类型转换而言,转换前后的两个类型要相互兼容才行。类型转换的语法为:<结果类型> := <目标类型> ( <表达式> )

package main

import "fmt"

func main() {

var i int = 9

var j float64

// 这个时候直接把i赋值给j是非法的, 因为go的=两边不允许出现类型不同的变量

// 我们需要进行类型转化

j = float64(i)

fmt.Println(j)

// 但是注意: int(3.14) 则不行, 因为会涉及截断, 前后值发生了改变

// 将一个float64类型转化为int, 除非这个float64的小数点后面是0

// 那如果遇见小数点后面不是0的浮点数该咋办呢? 可以使用math.Floor(), 会返回一个小数点后面是0的浮点数, 此时再转成int即可

}

断言

前面说过,因为空接口 interface{} 没有定义任何函数,因此 go 中所有类型都实现了空接口。当一个函数的形参是 interface{},那么在函数中,需要对形参进行断言,从而得到它的真实类型。

断言的语法为:

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

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

类型转换和类型断言有些相似,不同之处,在于类型断言是对接口进行的操作。举个栗子:

package main

import "fmt"

func main() {

var i interface{} = 123

// 这个i是一个空接口, 任何类型都实现了空接口

// 这个时候我们赋了一个值, 虽然我们知道这个是123, 但如果是在函数中呢?

// 我们要如何得知这个变量的类型呢?我们可以使用断言的方式, 我们就认定它是整型, 那么就可以这么做

var num = i.(int)

fmt.Println(num) // 123

// 但如果断言是一个字符串的话, 显然是会报错的

// 这个时候可以使用安全断言, 也就是采用两个变量来接收

s, flag := i.(string)

fmt.Printf("%q %t\n", s, flag) // "" false

// 如果不能成功转换, 那么会得到 零值 和 false

// 成功转换会得到 对应的值 和 true

// 因此我们还可以使用switch语句

switch i.(type) {

case int:

fmt.Println("int", i) // int 123

case float64:

fmt.Println("float64", i)

default:

fmt.Println("Unknown type")

}

}

所以我们最开始说的如果函数需要接收不同类型的值,就可以使用interface{}, 然后进行断言。

如果不是空接口也可以这么做,举个栗子:

package main

import "fmt"

type people interface {

say() string

}

func f(p people) {

// 一旦断言成功, 就会将转换成对应类型的值,并赋值给t

switch t := p.(type) {

case girl:

fmt.Println("1.", t, t.say())

case *girl:

fmt.Println("2.", t, t.say())

case boy:

fmt.Println("3.", t, t.say())

case *boy:

fmt.Println("4.", t, t.say())

default:

panic("不支持的类型")

}

//fmt.Println(s.say())

}

type girl struct {age int}

type boy struct {age int}

func (g girl) say() string {

return "girl -> say"

}

func (b boy) say() string {

return "boy -> say"

}

func main() {

g := &girl{16}

f(g) // 2. &{16} girl -> say

}

因此我们便使用 interface 实现了多态,Go 语言并没有设计诸如虚函数、纯虚函数、继承、多重继承等概念,但它通过接口却非常优雅地支持了面向对象的特性。多态是一种运行期的行为,它有以下几个特点:

一种类型具有多种类型的能力;

允许不同的对象对同一消息做出灵活的反应;

以一种通用的方式对待使用的对象;

非动态语言必须通过继承和接口的方式来实现

小结

这次我们介绍了go中的接口,但这只是其中的一部分,我们再后续介绍反射的时候会继续聊。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值