Go 接口
接口类型是 Go 中的一种很特别的类型。接口类型在 Go 中扮演着重要的角色。首先,在 Go 中,接口值可以用来包裹非接口值;然后,通过值包裹,反射和多态得以实现。
Go 语言的接口设计是非侵入式的,接口编写者无须知道接口被哪些类型实现。而接口实现者只需知道实现的是什么样子的接口,但无须指明实现哪一个接口。编译器知道最终编译时使用哪个类型实现哪个接口,或者接口应该由谁来实现。
一、接口类型含义
一个接口类型指定了一个 方法原型的集合 。换句话说,一个接口类型定义了一个 方法集。事实上,我们可以把一个接口类型看作是一个方法集。接口类型中指定的任何方法原型中的方法名称都不能为空标识符_。我们也常说一个接口类型规定了一个(用此接口类型指定的方法集表示的)行为规范。
// 一个非定义接口类型。
interface {
About() string
}
// ReadWriter是一个定义的接口类型。
type ReadWriter interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
}
// Runnable是一个非定义接口类型的别名。
type Runnable = interface {
Run()
}
特别地,一个没有指定任何方法原型的接口类型称为一个空接口类型。下面是两个空接口类型:
// 一个非定义空接口类型。
interface{}
// 类型I是一个定义的空接口类型。
type I interface{}
类型的方法集
每个类型都有一个方法集。
- 对于一个非接口类型,它的方法集由所有为它声明的(包括显式和隐式的)方法的原型组成。
- 对于一个接口类型,它的方法集就是它所指定的方法集。
为了解释方便,一个类型的方法集常常也可称为它的任何值的方法集。如果两个非定义接口类型指定的方法集是等价的,则这两个接口类型为同一个类型。
实现(implementation)
如果一个任意类型 T 的方法集为一个接口类型的方法集的超集,则我们说类型 T 实现了此接口类型。T 可以是一个非接口类型,也可以是一个接口类型。实现关系在 Go 中是隐式的。两个类型之间的实现关系不需要在代码中显式地表示出来。Go 中没有类似于 implements 的关键字。Go 编译器将自动在需要的时候检查两个类型之间的实现关系。
一个接口类型总是实现了它自己。两个指定了一个相同的方法集的接口类型相互实现了对方。在下面的例子中,类型 Book、MyInt 和 MyInt 都拥有一个原型为About() string
的方法,所以它们都实现了接口类型interface {About() string}
。
type Book struct {
name string
// 更多其它字段……
}
func (book *Book) About() string {
return "Book: " + book.name
}
type MyInt int
func (MyInt) About() string {
return "我是一个自定义整数类型"
}
注意:因为任何方法集都是一个空方法集的超集,所以 任何类型都实现了任何空接口类型。这是 Go 中的一个重要事实。 隐式实现关系的设计使得一个声明在另一个代码包(包括标准库包)中的类型可以被动地实现一个用户代码包中的接口类型。比如,如果我们声明一个像下面这样的接口类型,则 database/sql 标准库包中声明的 DB 和 Tx 类型都实现了这个接口类型,因为它们都拥有此接口类型指定的的三个方法。
import "database/sql"
...
type DatabaseStorer interface {
Exec(query string, args ...interface{}) (sql.Result, error)
Prepare(query string) (*sql.Stmt, error)
Query(query string, args ...interface{}) (*sql.Rows, error)
}
二、值包裹
每个接口值都可以看作是一个用来包裹一个非接口值的盒子。欲将一个非接口值包裹在一个接口值中,此非接口值的类型必须是实现了此接口值的类型。 在 Go 中,如果类型 T 实现了一个接口类型 I,则类型 T 的值都可以隐式转换到类型 I。换句话说,类型 T 的值可以赋给类型 I 的可修改值。当一个 T 值被转换到类型 I(或者赋给一个 I 值)的时候,
- 如果类型 T 是一个非接口类型,则此 T 值的一个复制将被包裹在结果(或者目标)I 值中。此操作的时间复杂度为 O(n),其中 n 为 T 值的尺寸。
- 如果类型 T 也为一个接口类型,则此 T 值中当前包裹的(非接口)值将被复制一份到结果(或者目标)I 值中。官方标准编译器为此操作做了优化,使得此操作的时间复杂度为 O(1),而不是 O(n)。 包裹在一个接口值中的非接口值的类型信息也和此接口值一起被包裹在此接口值中。
当一个非接口值被包裹在一个接口值中,此非接口值称为此接口值的 动态值 ,此非接口值的类型称为此接口值的 动态类型 。接口值的动态值的直接部分是不可修改的的,除非它的动态值被整体替换为另一个动态值。 接口类型的零值也用预声明的 nil 标识符来表示。一个 nil 接口值中什么也没包裹。将一个接口值修改为 nil 将清空包裹在此接口值中的非接口值。
注意,在 Go 中,很多其它非接口类型的零值也使用 nil 标识符来表示。非接口类型的 nil 零值也可以被包裹在接口值中。一个包裹了一个 nil 非接口值的接口值不是一个 nil 接口值,因为它并非什么都没包裹。
因为任何类型都实现了空接口类型,所以任何非接口值都可以被包裹在任何一个空接口类型的接口值中。一个空接口类型的接口值将被称为一个 空接口值 。注意 空接口值 和nil 接口值 是两个不同的概念。因为这个原因,空接口值可以被认为是很多其它语言中的 any 类型。
当一个类型不确定值(除了类型不确定的 nil)被转换为一个空接口类型(或者赋给一个空接口值),此类型不确定值将首先转换为它的默认类型。(或者说,此类型不确定值将被推断为一个它的默认类型的类型确定值。) 下面这个例子展示了一些目标值为接口类型的赋值。
package main
import "fmt"
type Aboutable interface {
About() string
}
// 类型*Book实现了接口类型Aboutable。
type Book struct {
name string
}
func (book *Book) About() string {
return "Book: " + book.name
}
func main() {
// 一个*Book值被包裹在了一个Aboutable值中。
var a Aboutable = &Book{"Go语言101"}
fmt.Println(a) // &{Go语言101}
// i是一个空接口值。类型*Book实现了任何空接口类型。
var i interface{} = &Book{"Rust 101"}
fmt.Println(i) // &{Rust 101}
// Aboutable实现了空接口类型interface{}。
i = a
fmt.Println(i) // &{Go语言101}
}
一个空接口类型的值可以包裹各种非接口类型的值
package main
import "fmt"
func main() {
var i interface{}
i = []int{1, 2, 3}
fmt.Println(i) // [1 2 3]
i = map[string]int{"Go": 2012}
fmt.Println(i) // map[Go:2012]
i = true
fmt.Println(i) // true
i = 1
fmt.Println(i) // 1
i = "abc"
fmt.Println(i) // abc
// 将接口值i中包裹的值清除掉。
i = nil
fmt.Println(i) // <nil>
}
在编译时刻,Go 编译器将构建一个全局表用来存储代码中要用到的各个类型的信息。对于一个类型来说,这些信息包括:此类型的种类(kind)、此类型的所有方法和字段信息、此类型的尺寸,等等。这个全局表将在程序启动的时候被加载到内存中。
在运行时刻,当一个非接口值被包裹到一个接口值,Go 运行时将分析和构建这两个值的类型的实现关系信息,并将此实现关系信息存入到此接口值内。对每一对这样的类型,它们的实现关系信息将仅被最多构建一次。并且为了程序效率考虑,此实现关系信息将被缓存在内存中的一个全局映射中,以备后用。所以此全局映射中的条目数永不减少。事实上,一个非零接口值 在内部只是使用一个指针字段来引用着此全局映射中的一个实现关系信息条目 。 对于一个非接口类型和接口类型对,它们的实现关系信息包括两部分的内容:
- 动态类型(即此非接口类型)的信息。
- 一个方法表(切片类型),其中存储了所有此接口类型指定的并且为此非接口类型(动态类型)声明的方法。
- 动态类型信息是实现反射的关键。
- 方法表是实现多态的关键。
三、多态(polymorphism)
多态是接口的一个关键功能和 Go 语言的一个重要特性 。当非接口类型 T 的一个值 t 被包裹在接口类型 I 的一个接口值 i 中,通过 i 调用接口类型 I 指定的一个方法时,事实上为非接口类型 T 声明的对应方法将通过非接口值 t 被调用。换句话说, 调用一个接口值的方法实际上将调用此接口值的动态值的对应方法 。比如,当方法 i.m 被调用时,其实被调用的是方法 t.m。一个接口值可以通过包裹不同动态类型的动态值来表现出各种不同的行为,这称为 多态 。
当方法 i.m 被调用时,i 存储的实现关系信息的方法表中的方法 t.m 将被找到并被调用。此方法表是一个切片,所以此寻找过程只不过是一个切片元素访问操作,不会消耗很多时间。另外,在 nil 接口值上调用方法将产生一个恐慌,因为没有具体的方法可被调用。
package main
import "fmt"
type Filter interface {
About() string
Process([]int) []int
}
// UniqueFilter用来删除重复的数字。
type UniqueFilter struct{}
func (UniqueFilter) About() string {
return "删除重复的数字"
}
func (UniqueFilter) Process(inputs []int) []int {
outs := make([]int, 0, len(inputs))
pusheds := make(map[int]bool)
for _, n := range inputs {
if !pusheds[n] {
pusheds[n] = true
outs = append(outs, n)
}
}
return outs
}
// MultipleFilter用来只保留某个整数的倍数数字。
type MultipleFilter int
func (mf MultipleFilter) About() string {
return fmt.Sprintf("保留%v的倍数", mf)
}
func (mf MultipleFilter) Process(inputs []int) []int {
var outs = make([]int, 0, len(inputs))
for _, n := range inputs {
if n % int(mf) == 0 {
outs = append(outs, n)
}
}
return outs
}
// 在多态特性的帮助下,只需要一个filteAndPrint函数。
func filteAndPrint(fltr Filter, unfiltered []int) []int {
// 在fltr参数上调用方法其实是调用fltr的动态值的方法。
filtered := fltr.Process(unfiltered)
fmt.Println(fltr.About() + ":\n\t", filtered)
return filtered
}
func main() {
numbers := []int{12, 7, 21, 12, 12, 26, 25, 21, 30}
fmt.Println("过滤之前:\n\t", numbers)
// 三个非接口值被包裹在一个Filter切片
// 的三个接口元素中。
filters := []Filter{
UniqueFilter{},
MultipleFilter(2),
MultipleFilter(3),
}
// 每个切片元素将被赋值给类型为Filter的
// 循环变量fltr。每个元素中的动态值也将
// 被同时复制并被包裹在循环变量fltr中。
for _, fltr := range filters {
numbers = filteAndPrint(fltr, numbers)
}
}
输出结果:
过滤之前:
[12 7 21 12 12 26 25 21 30]
删除重复的数字:
[12 7 21 26 25 30]
保留2的倍数:
[12 26 30]
保留3的倍数:
[12 30]
在上面这个例子中,多态使得我们不必为每个过滤器类型写一个单独的 filteAndPrint 函数。除此之外,多态也使得一个代码包的开发者可以在此代码包中声明一个接口类型并声明一个拥有此接口类型参数的函数(或者方法),从而此代码包的一个用户可以在用户包中声明一个实现了此接口类型的用户类型,并且将此用户类型的值做为实参传递给此代码包中声明的函数(或者方法)的调用。此代码包的开发者并不用关心一个用户类型具体是如何声明的,只要此用户类型满足此代码包中声明的接口类型规定的行为即可。
事实上,多态对于一个语言来说并非一个不可或缺的特性。我们可以通过其它途径来实现多态的作用。但是,多态可以使得我们的代码更加简洁和优雅。
四、反射(reflection)
在计算机科学领域,反射 (Reflection)是指一类应用,它们能够自描述和自控制。也就是说,这类应用通过采用某种机制来实现对自己行为的描述和监测,并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。在实际的编程语言中,反射就是运行时动态获取变量(这个变量可以存储多种类型的数据)的类型和值,根据不同的类型做出相应的处理。
在 Go 语言中,一个变量包括类型和值两部分,而类型又可以分为静态类型和动态类型。每个变量都有且只有一个静态类型,在编译时就已经确定,比如 int、float32、[]byte、interface。因为类型为 interface 的变量能够存储任何值,所以 interface 类型 的变量还有一个动态类型,即运行时赋值给这个变量的具体的值的类型。反射 就是检测接口变量在运行时的动态类型和值。
在 Go 中,reflect 标准包中提供了各种反射功能,且内置列反射机制,包括 类型断言 (type assertion)和type-switch 流程控制。
1、类型断言
Go 中有四种接口相关的类型转换情形:
- 将一个非接口值转换为一个接口类型。在这样的转换中,此非接口值的类型必须实现了此接口类型。
- 将一个接口值转换为另一个接口类型(前者接口值的类型实现了后者目标接口类型)。
- 将一个接口值转换为一个非接口类型(此非接口类型必须实现了此接口值的接口类型)。
- 将一个接口值转换为另一个接口类型(前者接口值的类型可以实现了也可以未实现后者目标接口类型)。
前两种情形都要求源值的类型必须实现了目标接口类型。它们都是通过普通类型转换(无论是隐式的还是显式的)来达成的。这两种情形的合法性是在编译时刻验证的。
后两种情形的合法性是在运行时刻通过类型断言来验证的。事实上,类型断言也适用于上面列出的第二种情形。一个类型断言表达式的语法为 i.(T),其中 i 为一个接口值,T 为一个类型名或者类型字面表示。类型 T 可以为 - 任意一个非接口类型。
- 或者一个任意接口类型。在一个类型断言表达式 i.(T)中,i 称为断言值,T 称为断言类型。一个断言可能成功或者失败。
- 对于 T 是一个非接口类型的情况,如果断言值 i 的动态类型存在并且此动态类型和 T 为同一类型,则此断言将成功;否则,此断言失败。当此断言成功时,此类型断言表达式的估值结果为断言值 i 的动态值的一个复制。我们可以把此种情况看作是一次拆封动态值的尝试。
对于 T 是一个接口类型的情况,当断言值 i 的动态类型存在并且此动态类型实现了接口类型 T,则此断言将成功;否则,此断言失败。当此断言成功时,此类型断言表达式的估值结果为一个包裹了断言值 i 的动态值的一个复制的 T 值。 一个失败的类型断言的估值结果为断言类型的零值。
按照上述规则,如果一个类型断言中的断言值是一个零值 nil 接口值,则此断言必定失败。对于大多数场合,一个类型断言被用做一个单值表达式。但是,当一个类型断言被用做一个赋值语句中的唯一源值时,此断言可以返回一个可选的第二个结果并被视作为一个多值表达式。此可选的第二个结果为一个类型不确定的布尔值,用来表示此断言是否成功了。如果一个断言失败并且它的可选的第二个结果未呈现,则此断言将造成一个恐慌。package main import "fmt" func main() { // 编译器将把123的类型推断为内置类型int。 var x interface{} = 123 // 情形一: n, ok := x.(int) fmt.Println(n, ok) // 123 true n = x.(int) fmt.Println(n) // 123 // 情形二: a, ok := x.(float64) fmt.Println(a, ok) // 0 false // 情形三: a = x.(float64) // 将产生一个恐慌 }
package main
import "fmt"
type Writer interface {
Write(buf []byte) (int, error)
}
type DummyWriter struct{}
func (DummyWriter) Write(buf []byte) (int, error) {
return len(buf), nil
}
func main() {
var x interface{} = DummyWriter{}
// y的动态类型为内置类型string。
var y interface{} = "abc"
var w Writer
var ok bool
// DummyWriter既实现了Writer,也实现了interface{}。
w, ok = x.(Writer)
fmt.Println(w, ok) // {} true
x, ok = w.(interface{})
fmt.Println(x, ok) // {} true
// y的动态类型为string。string类型并没有实现Writer。
w, ok = y.(Writer)
fmt.Println(w, ok) // <nil> false
w = y.(Writer) // 将产生一个恐慌
}
事实上,对于一个动态类型为 T 的接口值 i,方法调用 i.m(…) 等价于 i.(T).m(…)。
2、type-switch 流程控制代码块
type-switch 流程控制的语法或许是 Go 中最古怪的语法。它可以被看作是类型断言的增强版。它和 switch-case 流程控制代码块有些相似。一个 type-switch 流程控制代码块的语法如下所示:
switch aSimpleStatement; v := x.(type) {
case TypeA:
...
case TypeB, TypeC:
...
case nil:
...
default:
...
}
其中 aSimpleStatement; 部分是可选的。aSimpleStatement 必须是一个简单语句。x 必须为一个估值结果为接口值的表达式,它称为此代码块中的断言值。v 称为此代码块中的断言结果,它必须出现在一个短变量声明形式中。在一个 type-switch 代码块中,每个 case 关键字后可以跟随一个 nil 标识符和若干个类型名或类型字面表示形式。在同一个 type-switch 代码块中,这些跟随在所有 case 关键字后的条目不可重复。如果跟随在某个 case 关键字后的条目为一个非接口类型(用一个类型名或类型字面表示),则此非接口类型必须实现了断言值 x 的(接口)类型。
package main
import "fmt"
func main() {
values := []interface{}{
456, "abc", true, 0.33, int32(789),
[]int{1, 2, 3}, map[int]bool{}, nil,
}
for _, x := range values {
// 这里,虽然变量v只被声明了一次,但是它在
// 不同分支中可以表现为多个类型的变量值。
switch v := x.(type) {
case []int: // 一个类型字面表示
// 在此分支中,v的类型为[]int。
fmt.Println("int slice:", v)
case string: // 一个类型名
// 在此分支中,v的类型为string。
fmt.Println("string:", v)
case int, float64, int32: // 多个类型名
// 在此分支中,v的类型为x的类型interface{}。
fmt.Println("number:", v)
case nil:
// 在此分支中,v的类型为x的类型interface{}。
fmt.Println(v)
default:
// 在此分支中,v的类型为x的类型interface{}。
fmt.Println("others:", v)
}
// 注意:在最后的三个分支中,v均为接口值x的一个复制。
}
}
输出结果:
number: 456
string: abc
others: true
number: 0.33
number: 789
int slice: [1 2 3]
others: map[]
<nil>
上面这个例子程序在逻辑上等价于下面这个:
package main
import "fmt"
func main() {
values := []interface{}{
456, "abc", true, 0.33, int32(789),
[]int{1, 2, 3}, map[int]bool{}, nil,
}
for _, x := range values {
if v, ok := x.([]int); ok {
fmt.Println("int slice:", v)
} else if v, ok := x.(string); ok {
fmt.Println("string:", v)
} else if x == nil {
v := x
fmt.Println(v)
} else {
_, isInt := x.(int)
_, isFloat64 := x.(float64)
_, isInt32 := x.(int32)
if isInt || isFloat64 || isInt32 {
v := x
fmt.Println("number:", v)
} else {
v := x
fmt.Println("others:", v)
}
}
}
}
如果我们不关心一个 type-switch 代码块中的断言结果,则此 type-switch 代码块可简写为 switch x.(type) {…}。 type-switch 代码块和 switch-case 代码块有很多共同点:
- 在一个 type-switch 代码块中,最多只能有一个 default 分支存在。
- 在一个 type-switch 代码块中,如果 default 分支存在,它可以为最后一个分支、第一个分支或者中间的某个分支。
- 一个 type-switch 代码块可以不包含任何分支,它可以被视为一个空操作。 但是,和 switch-case 代码块不一样,fallthrough 语句不能使用在 type-switch 代码块中。
3、reflect 包
主要包括以下三个功能:
1、将“接口类型变量”转换为“反射类型对象”
注:这里反射类型指 reflect.Type 和 reflect.Value。
从用法上来讲,反射提供了一种机制,允许程序在运行时检查接口变量内部实际存储的数据。在最开始,我们先了解下 reflect 包的两种类型:reflect.Type 和 reflect.Value。这两种类型使访问接口内的数据成为可能。它们对应两个简单的方法,分别是 reflect.TypeOf 和 reflect.ValueOf,分别用来读取接口变量的 reflect.Type 和 reflect.Value 部分。
func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value
2、将“反射类型对象”转换为“接口类型变量”
和物理学中的反射类似,Go 语言中的反射也能创造自己反面类型的对象。根据一个 reflect.Value 类型的变量,我们可以使用 Interface 方法恢复其接口类型的值。事实上,这个方法会把类型和值信息打包并填充到一个接口变量中,然后返回。简单来说,Interface 方法和 ValueOf 函数作用恰好相反。
func (v Value) Interface() interface{}
3、修改“反射类型对象”
可以通过 reflect.Value 修改原接口类型变量的数据,其值必须是“可写的”。可写性”是反射类型变量的一个属性,但不是所有的反射类型变量都拥有这个属性,可以通过 CanSet 方法检查一个 reflect.Value 类型变量的“可写性”。“可写性”最终是由一个事实决定的:反射对象是否存储了原始值。特别的,修改一个结构体的某些字段时,字段首字母必须大写的,因为结构体中只有暴露到外部的字段才是“可写的”。
五、接口类型内嵌
一个接口类型可以内嵌另一个接口类型的名称。此内嵌的效果相当于将此被内嵌的接口类型所指定的所有方法原型展开到内嵌它的接口类型的定义体内。在下面的例子中,接口类型 Ic、Id 和 Ie 的所指定的方法集完全一样。
type Ia interface {
fa()
}
type Ib = interface {
fb()
}
type Ic interface {
fa()
fb()
}
type Id = interface {
Ia // 内嵌Ia
Ib // 内嵌Ib
}
type Ie interface {
Ia // 内嵌Ia
fb()
}
如果两个接口类型都指定了一个同名方法,则这两个接口类型不可被同时被内嵌在同一个接口类型中,即使它们指定的同名方法的原型也一致(此限制可能会从以后的 Go 版本中移除)。比如,下面的代码中假设的接口类型都是非法的。
type Ix interface {
Ia
Ic
}
type Iy = interface {
Ib
Ic
}
type Iz interface {
Ic
fa()
}
一个接口类型不能内嵌(无论是直接还是间接)它自己。
六、接口值相关的比较
接口值相关的比较有两种情形:
- 比较一个非接口值和接口值;
- 比较两个接口值。
对于第一种情形,非接口值的类型必须实现了接口值的类型(假设此接口类型为 I),所以此非接口值可以被隐式转化为(包裹到)一个 I 值中。这意味着非接口值和接口值的比较可以转化为两个接口值的比较。
比较两个接口值其实是比较这两个接口值的动态类型和和动态值。 下面是(使用 == 比较运算符)比较两个接口值的步骤: - 如果其中一个接口值是一个 nil 接口值,则比较结果为另一个接口值是否也为一个 nil 接口值。
- 如果这两个接口值的动态类型不一样,则比较结果为 false。
对于这两个接口值的动态类型一样的情形,
- 如果它们的动态类型为一个不可比较类型,则将产生一个恐慌。
- 否则,比较结果为它们的动态值的比较结果。简而言之,两个接口值的比较结果只有在下面两种任一情况下才为 true:
- 这两个接口值都为 nil 接口值。
这两个接口值的动态类型相同、动态类型为可比较类型、并且动态值相等。根据此规则,两个包裹了不同非接口类型的 nil 零值的接口值是不相等的。
package main
import "fmt"
func main() {var a, b, c interface{} = "abc", 123, "a"+"b"+"c" fmt.Println(a == b) // 第二步的情形。输出"false"。 fmt.Println(a == c) // 第三步的情形。输出"true"。 var x *int = nil var y *bool = nil var ix, iy interface{} = x, y var i interface{} = nil fmt.Println(ix == iy) // 第二步的情形。输出"false"。 fmt.Println(ix == i) // 第一步的情形。输出"false"。 fmt.Println(iy == i) // 第一步的情形。输出"false"。 var s []int = nil // []int为一个不可比较类型。 i = s fmt.Println(i == nil) // 第一步的情形。输出"false"。 fmt.Println(i == i) // 第三步的情形。将产生一个恐慌。
}
七、接口技巧
1、指针动态值和非指针动态值
标准编译器 / 运行时对接口值的动态值为指针类型的情况做了特别的优化。此优化使得接口值包裹指针动态值比包裹非指针动态值的效率更高。对于小尺寸值,此优化的作用不大;但是对于大尺寸值,包裹它的指针比包裹此值本身的效率高得多。对于类型断言,此结论亦成立。
所以 尽量避免在接口值中包裹大尺寸值。对于大尺寸值,应该尽量包裹它的指针 。
2、一个[]T 类型的值不能直接被转换为类型[]I,即使类型 T 实现了接口类型 I
比如,我们不能直接将一个[]string 值转换为类型[]interface{}。我们必须使用一个循环来实现此转换:
package main
import "fmt"
func main() {
words := []string{
"Go", "is", "a", "high",
"efficient", "language.",
}
// fmt.Println函数的原型为:
// func Println(a ...interface{}) (n int, err error)
// 所以words...不能传递给此函数的调用。
// fmt.Println(words...) // 编译不通过
// 将[]string值转换为类型[]interface{}。
iw := make([]interface{}, 0, len(words))
for _, w := range words {
iw = append(iw, w)
}
fmt.Println(iw...) // 编译没问题
}
3、一个接口类型每个指定的每一个方法都对应着一个隐式声明的函数
如果接口类型 I 指定了一个名为 m 的方法原型,则编译器将隐式声明一个与之对应的函数名为 I.m 的函数。此函数比 m 的方法原型中的参数多一个。此多出来的参数为函数 I.m 的第一个参数,它的类型为 I。对于一个类型为 I 的值 i,方法调用 i.m(…)和函数调用 I.m(i, …)是等价的。
package main
import "fmt"
type I interface {
m(int)bool
}
type T string
func (t T) m(n int) bool {
return len(t) > n
}
func main() {
var i I = T("gopher")
fmt.Println(i.m(5)) // true
fmt.Println(I.m(i, 5)) // true
fmt.Println(interface {m(int) bool}.m(i, 5)) // true
// 下面这几行被执行的时候都将会产生一个恐慌。
I(nil).m(5)
I.m(nil, 5)
interface {m(int) bool}.m(nil, 5)
}