理解 go interface 的 5 个关键点
LilHuondeKe
7年前
<h2>1、interface 是一种类型</h2> <pre> <code class="language-go">type I interface { Get() int }</code></pre> <p>首先 interface 是一种类型,从它的定义可以看出来用了 type 关键字,更准确的说 interface 是一种具有一组方法的类型,这些方法定义了 interface 的行为。</p> <p>go 允许不带任何方法的 interface ,这种类型的 interface 叫 empty interface。</p> <p>如果一个类型实现了一个 interface 中所有方法,我们说类型实现了该 interface,所以所有类型都实现了 empty interface,因为任何一种类型至少实现了 0 个方法。go 没有显式的关键字用来实现 interface,只需要实现 interface 包含的方法即可。</p> <h2>2、interface 变量存储的是实现者的值</h2> <pre> <code class="language-go">//1 type I interface { Get() int Set(int) } //2 type S struct { Age int } func(s S) Get()int { return s.Age } func(s *S) Set(age int) { s.Age = age } //3 func f(i I){ i.Set(10) fmt.Println(i.Get()) } func main() { s := S{} f(&s) //4 }</code></pre> <p>这段代码在 <code>#1</code> 定义了 interface I,在 <code>#2</code> 用 struct S 实现了 I 定义的两个方法,接着在 <code>#3</code> 定义了一个函数 f 参数类型是 I,S 实现了 I 的两个方法就说 S 是 I 的实现者,执行 <code>f(&s)</code> 就完了一次 interface 类型的使用。</p> <p>interface 的重要用途就体现在函数 f 的参数中,如果有多种类型实现了某个 interface,这些类型的值都可以直接使用 interface 的变量存储。</p> <pre> <code class="language-go">s := S{} var i I //声明 i i = &s //赋值 s 到 i fmt.Println(i.Get())</code></pre> <p>不难看出 interface 的变量中存储的是实现了 interface 的类型的对象值,这种能力是 duck typing。在使用 interface 时不需要显式在 struct 上声明要实现哪个 interface ,只需要实现对应 interface 中的方法即可,go 会自动进行 interface 的检查,并在运行时执行从其他类型到 interface 的自动转换,即使实现了多个 interface,go 也会在使用对应 interface 时实现自动转换,这就是 interface 的魔力所在。</p> <h2>3、如何判断 interface 变量存储的是哪种类型</h2> <p>一个 interface 被多种类型实现时,有时候我们需要区分 interface 的变量究竟存储哪种类型的值,go 可以使用 <code>comma, ok</code> 的形式做区分 <code>value, ok := em.(T)</code>:em 是 interface 类型的变量,T代表要断言的类型,value 是 interface 变量存储的值,ok 是 bool 类型表示是否为该断言的类型 T。</p> <pre> <code class="language-go">if t, ok := i.(*S); ok { fmt.Println("s implements I", t) }</code></pre> <p>ok 是 true 表明 i 存储的是 *S 类型的值,false 则不是,这种区分能力叫 Type assertions (类型断言)。</p> <p>如果需要区分多种类型,可以使用 switch 断言,更简单直接,这种断言方式只能在 switch 语句中使用。</p> <pre> <code class="language-go">switch t := i.(type) { case *S: fmt.Println("i store *S", t) case *R: fmt.Println("i store *R", t) }</code></pre> <h2>4、空的 interface</h2> <p><code>interface{}</code> 是一个空的 interface 类型,根据前文的定义:一个类型如果实现了一个 interface 的所有方法就说该类型实现了这个 interface,空的 interface 没有方法,所以可以认为所有的类型都实现了 <code>interface{}</code>。如果定义一个函数参数是 <code>interface{}</code> 类型,这个函数应该可以接受任何类型作为它的参数。</p> <pre> <code class="language-go">func doSomething(v interface{}){ }</code></pre> <p>如果函数的参数 v 可以接受任何类型,那么函数被调用时在函数内部 v 是不是表示的是任何类型?并不是,虽然函数的参数可以接受任何类型,并不表示 v 就是任何类型,在函数 doSomething 内部 v 仅仅是一个 interface 类型,之所以函数可以接受任何类型是在 go 执行时传递到函数的任何类型都被自动转换成 <code>interface{}</code>。go 是如何进行转换的,以及 v 存储的值究竟是怎么做到可以接受任何类型的,感兴趣的可以看看 Russ Cox 关于 interface 的实现 。</p> <p>既然空的 interface 可以接受任何类型的参数,那么一个 <code>interface{}</code>类型的 slice 是不是就可以接受任何类型的 slice ?</p> <pre> <code class="language-go">func printAll(vals []interface{}) { //1 for _, val := range vals { fmt.Println(val) } } func main(){ names := []string{"stanley", "david", "oscar"} printAll(names) }</code></pre> <p>上面的代码是按照我们的假设修改的,执行之后竟然会报 </p> <p><code>cannot use names (type []string) as type []interface {} in argument to printAll</code> 错误,why?</p> <p>这个错误说明 go 没有帮助我们自动把 slice 转换成 <code>interface{}</code> 类型的 slice,所以出错了。go 不会对 类型是<code>interface{}</code> 的 slice 进行转换 。为什么 go 不帮我们自动转换,一开始我也很好奇,最后终于在 go 的 wiki 中找到了答案 https://github.com/golang/go/wiki/InterfaceSlice 大意是 <code>interface{}</code> 会占用两个字长的存储空间,一个是自身的 methods 数据,一个是指向其存储值的指针,也就是 interface 变量存储的值,因而 slice []interface{} 其长度是固定的<code>N*2</code>,但是 []T 的长度是<code>N*sizeof(T)</code>,两种 slice 实际存储值的大小是有区别的(文中只介绍两种 slice 的不同,至于为什么不能转换猜测可能是 runtime 转换代价比较大)。</p> <p>但是我们可以手动进行转换来达到我们的目的。</p> <pre> <code class="language-go">var dataSlice []int = foo() var interfaceSlice []interface{} = make([]interface{}, len(dataSlice)) for i, d := range dataSlice { interfaceSlice[i] = d }</code></pre> <h2>5、interface 的实现者的 receiver 如何选择</h2> <p>在我们上文的例子中调用 f 是 <code>f(&s)</code> 也就是 S 的指针类型,为什么不能是 <code>f(s)</code> 呢,如果是 s 会有什么问题?改成 f(s) 然后执行代码。</p> <pre> <code class="language-go">cannot use s (type S) as type I in argument to f: S does not implement I (Set method has pointer receiver)</code></pre> <p>这个错误的意思是 S 没有实现 I,哪里出了问题?关键点是 S 中 set 方法的 receiver 是个 pointer *S 。</p> <p>interface 定义时并没有严格规定实现者的方法 receiver 是个 value receiver 还是 pointer receiver,上面代码中的 S 的 Set receiver 是 pointer,也就是实现 I 的两个方法的 receiver 一个是 value 一个是 pointer,使用 <code>f(s)</code>的形势调用,传递给 f 的是个 s 的一份拷贝,在进行 s 的拷贝到 I 的转换时,s 的拷贝不满足 Set 方法的 receiver 是个 pointer,也就没有实现 I。go 中函数都是按值传递即 passed by value。</p> <p>那反过来会怎样,如果 receiver 是 value,函数用 pointer 的形式调用?</p> <pre> <code class="language-go">type I interface { Get() int Set(int) } type SS struct { Age int } func (s SS) Get() int { return s.Age } func (s SS) Set(age int) { s.Age = age } func f(i I) { i.Set(10) fmt.Println(i.Get()) } func main(){ ss := SS{} f(&ss) //ponter f(ss) //value }</code></pre> <p>I 的实现者 SS 的方法 receiver 都是 value receiver,执行代码可以看到无论是 pointer 还是 value 都可以正确执行。</p> <p>导致这一现象的原因是什么?</p> <p>如果是按 pointer 调用,go 会自动进行转换,因为有了指针总是能得到指针指向的值是什么,如果是 value 调用,go 将无从得知 value 的原始值是什么,因为 value 是份拷贝。go 会把指针进行隐式转换得到 value,但反过来则不行。</p> <p>对于 receiver 是 value 的 method,任何在 method 内部对 value 做出的改变都不影响调用者看到的 value,这就是按值传递。</p> <p>另一个说明上述现象的例子是这样的来自 https://play.golang.org/p/TvR758rfre</p> <pre> <code class="language-go">package main import ( "fmt" ) type Animal interface { Speak() string } type Dog struct { } func (d Dog) Speak() string { return "Woof!" } type Cat struct { } //1 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!" } func main() { animals := []Animal{Dog{}, Cat{}, Llama{}, JavaProgrammer{}} for _, animal := range animals { fmt.Println(animal.Speak()) } }</code></pre> <p><code>#1</code> Cat 的 speak receiver 是 pointer,interface Animal 的 slice,Cat 的值是一个 value,同样会因为 receiver 不一致而导致无法执行。</p> <h2>参考资料</h2> <ul> <li> <p>https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/02.6.md</p> </li> <li> <p>http://jordanorelli.com/post/32665860244/how-to-use-interfaces-in-go</p> </li> <li> <p>https://tour.golang.org/methods/15</p> </li> <li> <p>https://www.miek.nl/go/#interfaces</p> </li> <li> <p>https://github.com/golang/go/wiki/InterfaceSlice</p> </li> <li> <p>https://play.golang.org/p/TvR758rfre</p> </li> <li> <p>https://golang.org/doc/effective_go.html#interfaces</p> </li> <li> <p>http://en.wikipedia.org/wiki/Duck_typing</p> </li> </ul> <p> </p> <blockquote> <p>从本期开始我们将在一段时间内持续介绍有关学习 go 的过程中遇到的困惑和难点,欢迎关注讨论。</p> <p> </p> <p>点击阅读原文查看文章的最新更新和文中的所有代码。</p> </blockquote> <p>来自:https://mp.weixin.qq.com/s/lViWx3EFMOdIkijX7w7U-w</p> <p> </p>