interface是go语言定义的一种类型,通常用于定义一些方法的集合。但是在go语言里面,interface又与其他类似的语言概念有些区别,如Java里的接口。
什么是 interface
在go中,interface包含两个部分:方法的集合和类型。这里接口是一些列方法的集合很容易理解,比如下面的例子:
type Speakable interface {
speak() string
}
如上,我们定义了一个名为Speakable的interface,内部包含一个func() string类型speak方法,因而interface是一些列方法的集合。但是方法的数量可以是0,比如内置的interface{},对该interface将稍候进行介绍。
但是interface是一个类型,这个定义如何理解?我将在后文进行介绍。
举个栗子
首先,来看几个interface在go语言中的例子。 比如就以上面的Speakable interface为例:
type Human struct{}
type Dog struct{}
type Cat struct{}
type PythonProgrammer{}
// implementation Speakable interface for Human
func (human Human) speak() string {
return "I can speak in many languages!"
}
// implementation Speakable interface for Dog
func (dog Dog) speak() string {
return "Woof!"
}
// implementation Speakable interface for Cat
func (cat Cat) speak() string {
return "Meow!"
}
// implementation Speakable interface for PythonProgrammer
func (p PythonProgrammer) speak() string {
return "Fxxk, what is the type?"
}
func main() {
speakers := []Speakable{Human{}, Dog{}, Cat{}, PythonProgrammer{}}
for _, speaker := range speakers {
fmt.Println(speaker.speak())
}
// output is
// I can speak in man languages!
// Woof!
// Meow!
// Fxxk, what is the type?
}
再来一个栗子,比如通过请求某api返回的是json数据,我们需要把json转换为具体的对象。假设json数据的结构如下:
{
ip: "192.168.137.1"
}
我们先看一下如何利用encoding/json
内置的方法解析json数据:
var obj map[string]interface{}
var ipt = `
{
"ip": "192.168.137.1"
}
`
if err := json.Unmarshal([]byte(ipt), &val); err != nil {
panic(err)
}
fmt.Println(obj)
for k, v := range obj {
fmt.Println(k, reflect.TypeOf(v))
}
// output is
// map[ip:192.168.137.1]
// ip string
上面的代码完成了对json 的解析,但是对ip字段的解析类型为string,string类型对于ip数据的处理肯定是不方便的。那如何将ip字段转换成更方便处理的类型呢?
首先,我们先根据需要自定义一个IP类型。
type IP struct {
// A,B,C,D are for four areas of a ip
// such as 0.0.0.0
A uint8
B uint8
C uint8
D uint8
}
然后我们修改一下之前代码obj的类型定义:
var obj map[string]IP
再次尝试运行,输出结构如下:
panic: json: cannot unmarshal string into Go value of type main.IP
goroutine 1 [running]:
main.main()
引发了一个panic,看来上面的代码并没有得到我们所预料的结果。为什么会这样呢?引发的错误指出无法将string类型转换为IP类型。是的,Unmarshal方法不并不知道如何把string转换成IP。
为了解决这个问题,我们要让Unmarshal方法直到如何将stirng转换为IP类型。其实encoding/json
包定义了Unmarshaler接口,该借口约定了json的转换方法,其定义如下:
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
因此,我们只需要在IP类型上实现Unmarshaler接口,即可让Unmarshal方法为我们完成IP数据的转换。
// define ip error
type IPError struct {
msg string
}
// implement Error interface
func (ipErr IPError) Error() string {
return ipErr.msg
}
// parse a ip str to IP struct
// ipStr must be formatted like 122.132.141.124,
// when format of ipStr is invalid, an IPError occurs.
func strToIp(ipStr string) (ip IPS, err error) {
seg := strings.Split(ipStr, ".")
// check whether str is a valid ip
if len(seg) != 4 {
return IP{}, IPError{"Invalid ip"}
}
var newIp IP
if a, err := strconv.ParseUint(seg[0], 10, 8); err != nil {
return IP{}, err
} else {
newIp.A = uint8(a)
}
if b, err := strconv.ParseUint(seg[1], 10, 8); err != nil {
return IP{}, err
} else {
newIp.B = uint8(b)
}
if c, err := strconv.ParseUint(seg[2], 10, 8); err != nil {
return IP{}, err
} else {
newIp.C = uint8(c)
}
if d, err := strconv.ParseUint(seg[3], 10, 8); err != nil {
return IP{}, err
} else {
newIp.D = uint8(d)
}
return newIp, nil
}
// implement Unmarshaler interface
func (ip *IP) UnmarshalJSON(data []byte) error {
str := string(data)
str = strings.Replace(str, "\"", "", -1)
if newIp, err := strToIp(str); err != nil {
return nil
} else {
*ip = newIp
}
return nil
}
添加如上代码后,再次运行程序,其输出如下:
// output
map[ip:{192 168 137 1}]
ip main.IP
可以看到,通过实现Unmarshaler interface,内置的json解析方法可以将数据转换成我们期望的对象。
以上,就是interface所谓一系列方法集合的定义。这也是在其他语言(比如Java)里面大家常见的用法。对于一个没有方法的interface,这样的interface可以用来定义类型;对于传统的OOP语言来说,定义类型可能大家想到的是使用抽象类,但是抽象类对于不支持多继承的语言来说,存在一个致命缺陷。由于某些语言只支持单继承,继承了某个抽象类后无法在继承其他类,导致了对该类型的扩展受限;而使用接口则不一样,接口可以方便的实现混合类型的定义[Effect Java:接口优先于抽象类]。
接下来,开始进入go语言里interface神奇的一面,我们先从interface{}说起。
interface{} Type
首先,来看一下大家最常用的fmt.Println方法的声明:
// source code in print.go
func Println(a ...interface{}) (n int, err error) {
return Fprintln(os.Stdout, a...)
}
思考一下,slice a里面的每一个元素,是什么类型呢?
Any Type?
go语言的接口实现机制
标题是H3 ,这不是新的一节,而是在 interface{} Type之下的。这里插入说明一下go语言的接口实现机制,第一次在《Go in practice》里读到之后,感觉这个思想太厉害了。
同学们有没有发现,在之前的例子中,我们说实现某个接口只是声明了和某个接口包含的方法签名一样的方法,没有使用任何关键字。可能来自Java的小朋友要疑惑了,没有使用类似implements这样的关键字诶。
对于传统的oop语言,对于实现某个接口都需要在类型后面写出该接口的名称,比如Java:
package api.entity;
import api.interfaces.Speakable;
/**
* Human
*
* @author hercat
* @date 2019-01-31 09:51
*/
public class Human implements Speakable {
}
这样的做法有一个什么弊端呢?当Human实现Speakable接口的时候,必须显示的声明接口的名称(Speakable)以及接口存在的命名空间(api.interfaces.Speakable)。那么问题来了,要是我压根没定义Speakable或者我将Speakable移动到其他地方去了,会发生什么呢?一般来说会有两种情况。
一是在编译阶段编译器无法找到Speakable接口,导致编译失败;这种情况下出现的影响尚还在可控范围内,因为我们只需要提供Speakable的定义或修改正确的命名空间即可。第二种情况就是编译的时候正常,但是在打包的时候由于某些资源没有被打包到runtime环境中,此时就可能会产生一个runtime error。runtime error就是完全不可控的范围了,可能导致程序直接crash。
go的思路则不一样,go设计了一种隐式实现接口的方法。也就是不需要关键字,也不需要声明接口名称。这样做的好处是解除了接口实现和接口定义的耦合关系。因此我们在go里实现接口的时候直接为某个类型定义和某个接口内方法签名一致的方法即可,剩下的东西交给编译器处理就好了。
Any Type?
由于interface{}是一个没有定义任何方法的接口,而任何的类型都必定包含零个以上的方法。由于go语言的接口实现机制,因此Any Type都是符合interface{}的。但fmt.Println方法里a的元素是不是Any Type呢?其实不是,而是静态的interface{}类型。这就是interface定义的第二部分,类型。这确实让人很费解!但是就是这样的。虽然fmt.Println方法在调用的过程中能够传入任意类型的参数,但是这些参数到Println方法内部的时候,就会被自动转换为interface{}类型。
Interface 类型的结构
前面说我们传入的参数自动被转换成interface{}类型,那实际传入给方法处理的参数是什么样子的呢?一个interface{}类型的值也包含两部分,一是该参数实际的值,一是改参数基础类型所包含的方法表。
假设一上述的IP类型为例,该类型实现了Unmarshaler结构的UnmarshalJSON方法。如下,当一个IP类型的变量传入Println方法:
ip := IP{192, 168, 132, 2}
fmt.Println(ip)
则Println方法接受到的参数其实是包含了ip自身的值和IP类型下的方法。如下图:
structure of interface type
interface和interface{}type总结
- interface
包含两个方面:『一系列方法的集合』和『类型信息』。 - interface{} type
包含两个方面:『数据』和『数据值原始类型下的方法表』。
如果对interface{}类型的值结构还不了解的同学,建议阅读Russ Cox的原文,地址可在参考里找到!
interface和指针
go语言也支持指针,但这里不对go语言的指针机制做太多介绍。主要是说明某个类型实现某个接口方法声明的问题。以前面的Speakable接口为例,假设Human类型定义如下:
type Human struct{}
// implement Speakable interface
func (human Human) speak() string {
return "I can speak in many languages!"
}
如上的定义说明我们为Human类型提供了speak方法的定义,也就是实现了Speakable接口。现在考虑下面的代码:
var speak Speakable
speak = Human{}
fmt.Println(speak.speak())
// output is
// I can speak in many languages!
上面的代码如我们说预期的执行了,那如果把Human类型的指针引用赋值给speak变量呢?会发生什么情况呢?
var speak Speakable
speak = new(Human) // equal to speak = &Human{}
fmt.Println(speak.speak())
其输出和我们预期的也是一样:
// output is
// I can speak in many languages!
到此,现在我们有了一个结论:如果某类型实现某接口的时候,receiver(如果不了解receiver术语的同学,可以查看go关于method的介绍)若是为值类型,那意味着同时为值类型的变量和指针类型的变量实现了该接口。
接下来,我们修改一下接口实现方法中receiver的定义,将其修改为指针类型,如下:
// implement Speakable interface
func (human *Human) speak() string {
return "I can speak in many languages!"
}
接下来,我们尝试运行如下代码:
var speak Speakable
speak = new(Human) // equal to speak = &Human{}
fmt.Println(speak.speak())
// output is
// I can speak in many languages!
结果如预期。而如下的代码会产生错误:
var speak Speakable
speak = Human{}
fmt.Println(speak.speak())
错误信息为:
Cannot use 'Human{}' (type Human) as type Speakable in assignment Type does not implement 'Speakable' as 'speak' method has a pointer receiver.
什么意思?就是只提供了指针类型的实现,值类型不被兼容。为什么会这样呢?定义在值类型上的方法值和指针都可以访问;而定义在指针类型上的方法却只有指针能访问。要解释这个道理,其实很容易,先看下图,下面说明了指针和值之间的关系:
很简单,值类型是一个萝卜一个坑,直接就是值本身;而指针存储的是存储值的内存块的地址。举个不恰当的例子,值类型就像蜗牛,成天背着自己的房子去各个地方;而指针就像一个门牌号,通过这个门牌号也能找到房子。
现在可以说清楚receiver为值类型或指针类型的差异了。
当为定义在值类型上的方法时,值很自然的可以访问。指针为啥也可以访问呢?因为指针可以根据内存地址找到值呀~,而且无论是哪个指针,只要指向的内存地址是一样的,那么找打的值就是一样的。
当为定义在指针类型上时,指针自然可以访问。那为什么值无法访问呢?因为值的背后可能要千千万万个指针,这些指针在外部是有却别的,因此编译器并不知道选择哪一个指针来访问你调用的方法。
Everything is a copy
还记得定义在IP类型上的UnmarshalJSON方法吗:
// implement Unmarshaler interface
func (ip *IP) UnmarshalJSON(data []byte) error {
str := string(data)
str = strings.Replace(str, "\"", "", -1)
if newIp, err := strToIp(str); err != nil {
return nil
} else {
*ip = newIp
}
return nil
}
重点看*ip = newIp
这句代码。思考一下赋值的时候可以写成ip = &newIp
吗?
由于go语言中,任何时候(传参、赋值)都是新建一份拷贝,哪怕是对于指针类型,仍然是传递的原指针的拷贝。当我们使用引用类型(指针)作为参数时,期望的结果是在方法内部进行的修改能够被方法的调用者可看见,当然完全可以使用返回值技术,但这里我们重点讨论指针。
当你在你所定义的方法内部处理一个指针参数时,该指针其实已经是调用者在调用时传入的那个指针的拷贝了,此时的运行时可能存在多个指向某个值的指针。为了实现你在方法内部的修改能被外部调用者看见,你应该怎么做呢?
正确的做法是透过传入进来的指针进行逆向引用(解引用),找到该指针指向的值,然后修改该值。也就是追本溯源,直接修改万物的根源。因此对于上面的那个问题,答案是赋值的时候不可以写成ip = &newIp
。
reference 对比 dereference
Reference(引用):就是获得某个值得内存地址,并把该地址赋值给一个指针变量,使得该指针变量获得对该值的引用。比如 p = &a。
Dereference(解引用):就是通过指针找到该指针指向的值。比如*p。
- 又被称为 Dereference operator。
总结
这里全面总结的go语言里的接口。本以为接口就是简单的定义类型或者约束子类型的结构,如同在Java语言里的那样。
而当我看到go语言接口的隐式实现机制后,才觉得go语言的确实在许多地方做到了简洁。
当我尝试在go语言里使用泛型功能的时候,才发现go不支持泛型。通过查看fmt.Println等方法的声明后,也尝试使用interface{}类型实现泛型,但是对interface{}的一知半解也导致无法实现我预期的效果。但是通过看一下资料后,再加上这篇文章的书写。对interface的理解又更多了一点!虽然现在任然无法实现如同的java里的泛型编程,但毕竟是受限于go语言自身的原因。
网友评论