#GO语言面向对象编程之接口(下)#
Go's interfaces—static, checked at compile time, dynamic when asked for—are, for me, the most exciting part of Go from a language design point of view. If I could export one feature of Go into other languages, it would be interfaces.
接口在GO语言的地位从上面作者的原话就可以看出来了,所以对于接口的学习,直接去读了一些英文的文献。下面的介绍,主要根据这篇文献来解释:
> [http://jordanorelli.com/post/32665860244/how-to-use-interfaces-in-go](http://jordanorelli.com/post/32665860244/how-to-use-interfaces-in-go)
***
##接口的介绍
接口是什么?
接口包含两部分:接口是一组方法,但同时也是一种类型,首先我们从一组方法来解释。
<font size = 5 color =red>we’ll define an Animal as being anything that can speak. This is a core concept in Go’s type system。 instead of designing our abstractions in terms of what kind of data our types can hold, we design our abstractions in terms of what actions our types can execute.</font>
这段话实在不知道怎么翻译才能翻译出精髓的含义。大体意思就是我们定义了一个Animal的接口,只要拥有说话功能的对象都是Aniamal。不再去设计我们可以拥有哪些类型的数据,而是去关注我们有哪些方法可以执行。
我们设计的接口如下:
type Animal interface {
Speak() string
}
Speak 函数没有参数,返回string类型。任何拥有Speak函数的类型都可以说实现了Animal接口。在Go语言里没有Implements关键字;一个数据类型是否实现接口自动决定,来看下面实现了接口的例子:
type Dog struct {
}
func (d Dog) Speak() string {
return "Woof!"
}
type Cat struct {
}
func (c Cat) Speak() string {
return "Meow!"
}
type Llama struct {
}
func (l Llama) Speak() string {
return "?????"
}
type JavaProgrammer struct {
}
func (j JavaProgrammer) Speak() string {
return "Design patterns!"
}
在这里我们定义了四种类型,每种类型都拥有自己的Speak函数。所以四种类型都实现了接口Animal。所以我们可以直接创建一个Animal的slice然后看每个类型的输出:
func main() {
animals := []Animal{Cat{}, Dog{}, Llama{}, JavaProgrammer{}}
for _, animal := range animals {
fmt.Println(animal.Speak())
}
}
***
##interface{} 类型
interface{}是一个空的Interface,也是很容易产生疑惑的地方。因为空的interface没有任何的函数,又因为Go语言里面没有Implements关键字来标识接口的实现关系,所以所有的类型都可以认为是实现了interface{}.这意味着如果你定义了一个函数,函数参数是空的Interface的话,那么函数可以接受任意类型的参数。
func DoSomething(v interface{}) {
// ...
}
函数DoSomething可以接受任何的参数,那么在函数里面v的数据类型是什么?因为前面说过interface{}可以被任意类型实现,所以有的人认为v是“任意类型”。事实上v是interface类型。GO在运行的过程中会做类型的转换,每一个变量在运行的时候都会有一个类型。这里的v就是interface{}类型。如果对上述的函数调用如下:
data := []int{1, 2, 3, 4, 5}
DoSomeThing(data)
这时候编译有出现如下的错误:
.\main.go:48: cannot use data (type []int) as type []interface {} in argument to DoSomeThing
这里证明接口是一种类型,而不是所谓的“任意类型”。
看到这里如果你还是疑惑的,那么是正常的,如果没任何的疑惑,那就再读一遍前面的东西,或者直接读上面的英文原文。
**接口的由2个words组成:一个指向了接口中存数的数据类型,另外一个指向是该接口的方法列表,注意是该接口的,不是存放的数据类型的。**
![](http://i.imgur.com/0tXStyB.png)
接口的实现可以参照这篇文章来看:
[https://research.swtch.com/interfaces](https://research.swtch.com/interfaces)
对于开始的Animal的实例,我们发现我们定义的Animal接口切片的时候采用的如下的方式:
animals := []Animal{Cat{}, Dog{}, Llama{}, JavaProgrammer{}}
所以这里的Animal[0]里面的两个words一个指向是Cat这个变量,另外一个Words指向的是Animal这个接口的函数列表。
但细心的人可能会发现,为什么我们没有进行任何的强制转化,比如Animal(Cat{})就可以直接将Cat变量存放到接口里面那,原因就是默认的进行了强制转化。所以我们不需要再对其进行强转。而对于上面的DoSomething的函数的调用,我们需要将切片显式的转化为切片类型才可以调用它,所以代码如下:
func DoSomeThing(v []interface{}) {
for _, data := range v {
fmt.Println(data)
}
}
data := []int{1, 2, 3, 4, 5}
value := make([]interface{}, len(data))
for i := 0; i < len(data); i++ {
value[i] = data[i]
}
DoSomeThing(value)
这代码看起来很丑,但没有完美的东西。[]interface{}也并没有你想的那么经常的出现,因为它的作用并没有你一开始想的那么有用。(文章的原话,个人觉得真心丑o(╯□╰)o)。
****
##Pointers and interfaces
关于接口的另外一个小技巧在于,接口定义并没有规定实现接口的接收者是指针还是类型。我们前面的Animal采用的都是类型作为接收者,如果把类型改为指针:
func (c *Cat) Speak() string {
return "Meow!"
}
编译会有以下错误:
.\main.go:41: cannot use Cat literal (type Cat) as type Animal in array or slice literal:
Cat does not implement Animal (Speak method has pointer receiver)
错误的意思就是你定义的函数Speak是一个指针作为接收者,但你是将一个Cat struct转换成为了Animal。所以你可以将初始化的地方变成指针如下:
animals := []Animal{new(Cat), Dog{}, Llama{}, JavaProgrammer{}}
animals := []Animal{&Cat{}, Dog{}, Llama{}, JavaProgrammer{}}
两种方法都可以使用。
但如果我们将Speak()函数改为类型作为接收者,但用指针作为参数。就不会出现错误,因为指针可以解析到它关联的数据类型。代码如下:
func (c Cat) Speak() string {
return "Meow!"
}
animals := []Animal{new(Cat), Dog{}, Llama{}, JavaProgrammer{}}
animals := []Animal{&Cat{}, Dog{}, Llama{}, JavaProgrammer{}}
这种情况看起来挺难理解的,前面的时候我们讲过:**everything in Go is passed by value. Every time you call a function, the data you’re passing into it is copied.**简单的理解就是,我们调用函数的时候参数都是拷贝的,所以你对形参做的操作在外面是看不到,因为形参只是实参的复制。
再看一下我们前面说的两种情况:
1.指针作为接收的时候只能用指针去调用。
2.类型作为接收的时候可以用指针和类型去调用。
下面的例子:
func (t T)MyMethod(s string) {
// ...
}
函数MyMethod函数是func(T, string)类型,函数的接收者会和其它参数一样传入函数内部。所以对于第一种情况,先看看作者的解释:
Since everything is passed by value, it should be obvious why a *Cat method is not usable by a Cat value; any one Cat value may have any number of *Cat pointers that point to it. If we try to call a *Cat method by using a Cat value, we never had a *Cat pointer to begin with。
意思是Cat 有很多指向它的指针,所以当你用cat 变量去调用*Cat的方法的时候,Cat指针不知道指向哪。
对于第二种,是因为当你用指针Cat去调用Cat的时候,因为指针可以知道自己指向的地址,所以Go语言在运行的过程中会把*Cat去重新定义到它指向的变量。所以Go语言里面给个Dog类型的指针 *d,我们可以直接调用d.Speaker,而不用像其它语言d->Speaker。
其实不懂的话就住:<font color = "red">指针接收者的函数只能指针调用,但类型接收者的函数,指针和类型都可以调用</font>
***
##接口的应用-从http request里面得到对象
假设要设计一个接口来解决web开发问题,我们需要将http的request解析到一些对象里面(比如struct来提取自己想要的信息)。一开始的时候没有明确的设计,但知道的是需要得到一些数据资源从reques里面,所以一开始的设计如下:
GetEntity(*http.Request) (interface{}, error)
返回Interface{},因为它可以代表任意的类型。但这的确不是一个很好的设计,因为对于不同的结构的话,我们都需要更改GetEntity函数。在GO语言里面有个不成文的规则:
<font color = "red">it’s typically better to take in an interface{} value as a parameter than it is to return an interface{} value. (Postel’s Law, applied to interfaces)</font>
或者可以设计成这种模式:
GetUser(*http.Request) (User, error)
这样的话每一个类型的User都对应一个函数,虽然这样也可以实现,但会显得代码很难形成统一的概念。(可以理解代码不够优美,毕竟面向对象的语言,如果每个对象都对应自己的函数,没有统一的管理,那么代码的可读性和维护性都会很差),所以我们可以设计成:
type Entity interface {
UnmarshalHTTP(*http.Request) error
}
func GetEntity(r *http.Request, v Entity) error {
return v.UnmarshalHTTP(r)
}
这样的话 GetEntity 函数可以接收任意拥有UnmarshalHTTP函数的类型参数, 首先声明一个以User point作为接收者的UnmarshalHTTP函数。
func (u *User) UnmarshalHTTP(r *http.Request) error {
// ...
}
然后我们需要声明一个User的变量,然后讲其指针传递给函数GetEntity。
var u User
if err := GetEntity(req, &u); err != nil {
// ...
}
这样每次增加一种类型,都只需定义其内容和UnmarshalHTTP函数,然后调用统一的函数GetEntity来获取自己想要的数据。我们只需要将重点放在HTTP handlers的编写上。
GO语言和其它语言还有一个不同点,var u User 的时候,GO语言将会自动的初始化所有的变量,比如User是个结构体,那么结构体里的数据都将被初始化,不会造成有的语言不初始化出现乱值的情况。
***
##结束语
请记住以下几点:
1. 设计的时候要根据类型的函数的共同点设计,而不是根据数据的共同点。
2. interface{} 不是任意的类型,而是interface{}类型
3. 接口由两个words组成;一个指向数据,一个指向方法的表
4. 用interface{}最为参数比作为返回值好
5. 变量作为接收者的函数,可以用指针调用,但反之不行
6. 所有的变量做为参数的时候都以值得形式传递,包括函数的接收者
7. 一个interface变量不能严格意义的说是指针或者不是指针类型,它只是interface
8. 如果你想在函数内部想完全重写一个值,使用*操作来解除指针的引用.
有疑问加站长微信联系(非本文作者))