内容简介:接口提升了代码的弹性与拓展性,同时它也是 go 语言实现多态的一种方式。接口允许通过一些必要的行为来实现,而不再要求设置特定类型。而这个行为就是通过一些方法设置来定义的:不需要特定的实现。只要通过定义 type 中包含目标名与签名(输入与输出的参数列表)的方法来表明其实现(满足)了一个接口就足够了:
接口提升了代码的弹性与拓展性,同时它也是 go 语言实现多态的一种方式。接口允许通过一些必要的行为来实现,而不再要求设置特定类型。而这个行为就是通过一些方法设置来定义的:
type I interface { f1(name string) f2(name string) (error, float32) f3() int64 }
不需要特定的实现。只要通过定义 type 中包含目标名与签名(输入与输出的参数列表)的方法来表明其实现(满足)了一个接口就足够了:
type T int64 func (T) f1(name string) { fmt.Println(name) } func (T) f2(name string) (error, float32) { return nil, 10.2 } func (T) f3() int64 { return 10 }
类型 T 实现了第一个程序段的接口 I。举个例子,类型 T 的值可以传递给任何接受 I 作为参数的函数( 源代码 ):
type I interface { M() string } type T struct { name string } func (t T) M() string { return t.name } func Hello(i I) { fmt.Printf("Hi, my name is %s\n", i.M()) } func main() { Hello(T{name: "Michał"}) // "Hi, my name is Michał" }
在 function Hello 中,方法调用了 i.M()
。 这个过程概括一下就是,只要来自不同 type 的方法是通过 type 来实现 interface I,就可以被调用。
go 语言的突出特点就是其 interface
是隐式实现的。程序员不需要指定 type T 实现了 interface I。这个工作由 go 的编译器完成(不需要派一个人去做机器的工作)。这种行为中的实现方式之所以很赞,是因为定义 interface 这件事情是由已经写好的 type 自动实现的(不需要为之做任何改变)。
之所以 interface 可以提供弹性,是因为任意一个 type 可以实现多个 interface ( 代码 ):
type I1 interface { M1() } type I2 interface { M2() } type T struct{} func (T) M1() { fmt.Println("T.M1") } func (T) M2() { fmt.Println("T.M2") } func f1(i I1) { i.M1() } func f2(i I2) { i.M2() } func main() { t := T{} f1(t) // "T.M1" f2(t) // "T.M2" }
或者同样的 interface 可以实现多个 type ( 源代码 ):
type I interface { M() } type T1 struct{} func (T1) M() { fmt.Println("T1.M") } type T2 struct{} func (T2) M() { fmt.Println("T2.M") } func f(i I) { i.M() } func main() { f(T1{}) // "T1.M" f(T2{}) // "T2.M" }
而且除了一个或多个 interface 所需要的方法外,type 可以自由地实现其他方法
在 go 中,我们有两个与 interface 相关的概念:
- 接口-通过 关键字
interface
,实现此类接口所需要的一组方法; - 接口类型-接口类型的变量,可以保存一些实现于特定接口的值。
让我们在接下来的两节中讨论这些主题。
定义一个接口
接口类型的声明指定属于它(接口)的方法。方法是通过它的名字(方法名)和签名-输入和接口参数定义的:
type I interface { m1() m2(int) m3(int) int m4() int }
除了方法外,它还允许嵌入其他接口-在同一个包中定义或引入-通过 限定名 。它从嵌入的接口中添加所有方法:
import "fmt" type I interface { m1() } type J interface { m2() I fmt.Stringer }
接口 J 的方法组包括:
- m1() 来自嵌入的接口 I
- m2()
- String() string(来自嵌入的接口 Stringer )
顺序无关紧要,所以方法规格与嵌入的接口类型完全可以交错。
添加了来自嵌入接口类型的导出方法(以大写字母开头)和非导出方法(以小写字母开头)
如果我嵌入一个接口 J,接口 J 又嵌入接口 K,那么 K 中的所有方法也会被添加到 I 中:
type I interface { J i() } type J interface { K j() } type K interface { k() }
I 的方法组包括 i()
, j()
和 k()
( 源代码 )。
不允许循环嵌入接口(译注:即 A 嵌入 B,B 嵌入 C,C 嵌入 A),并且在编译阶段,会检测接口的循环嵌入问题( 源代码 ):
type I interface { J i() } type J interface { K j() } type K interface { k() I }
编译器会提出一个错误 interface type loop involving I
。
接口方法必须有唯一名字( 源代码 ):
type I interface { J i() } type J interface { j() i(int) }
否则将抛出编译时间错误: duplicate method i
。
接口的组成可以在标准库中找到。一个这样的例子就是 io.ReadWriter :
type ReadWriter interface { Reader Writer }
我们知道如何创建一个新的接口。现在让我们学习接口类型的值...
接口类型的值
接口类型 I 的变量可以保持任何实现 I 的值( 源代码 ):
type I interface { method1() } type T struct{} func (T) method1() {} func main() { var i I = T{} fmt.Println(i) }
这里我们有一个来自接口类型 I 的变量 i。
静态类型 VS 动态类型
在编译阶段,变量类型便已知。这是在声明时指定的,不再变化,并被称为静态类型(或只是类型)。接口类型的变量也有静态类型,其本身就是一个接口。它们还具有可以指定值的类型-动态类型( 源代码 ):
type I interface { M() } type T1 struct {} func (T1) M() {} type T2 struct {} func (T2) M() {} func main() { var i I = T1{} i = T2{} _ = i }
变量 i 的静态类型是 I。这是不变的。另一方面,动态类型是...好吧,动态的。在首次分配后,i 的动态类型是 T1。这并不是一成不变的,所以 i 的动态类型第二次赋值为 T2。当接口类型值的值 nil (接口类型的零值)时,动态类型便不设置。
如何获取接口类型值得动态类型?
fmt.Println(reflect.TypeOf(i).PkgPath(), reflect.TypeOf(i).Name()) fmt.Println(reflect.TypeOf(i).String())
通过包 fmt 以及格式动词 %d
也可以做到这点:
fmt.Printf("%T\n", i)
在 hood 下使用包 reflect 包,即便 i 是 nil 时,这个方法也有效。
空接口值
这次我们将从一个例子开始( 源代码 ):
type I interface { M() } type T struct {} func (T) M() {} func main() { var t *T if t == nil { fmt.Println("t is nil") } else { fmt.Println("t is not nil") } var i I = t if i == nil { fmt.Println("i is nil") } else { fmt.Println("i is not nil") } }
输出:
t is nil i is not nil
第一次看,会觉得很惊讶。变量 i 的值,我们明明设置为 nil,但是这里的值却不等于 nil。其实接口类型值包含两个组件:
- 动态类型
- 动态值
动态类型在之前(“静态类型VS动态类型”部分)已经讨论过了。动态值是指定的实际值。
在赋值 var i I = t
后的讨论段中,i 的动态值是 nil,但动态类型为* T 在这个复制后,函数调用 fmt.Printf("%T\n", i)
将会打印 *main.T
。 当且仅当动态值与动态类型都为 nil 时,接口类型值为 nil。
结果就是即使接口类型值包含一个 nil 指针,这样的接口值也不是 nil。已知的错误就是返回未初始化,从函数返回接口类型为非接口类型值( 源代码 ):
type I interface {} type T struct {} func F() I { var t *T if false { // not reachable but it actually sets value t = &T{} } return t } func main() { fmt.Printf("F() = %v\n", F()) fmt.Printf("F() is nil: %v\n", F() == nil) fmt.Printf("type of F(): %T", F()) }
它打印出:
F() = <nil> F() is nil: false type of F(): *main.T
只是因为从函数返回的接口类型值有动态类型集(*main.T)。它并不等于 nil。
空接口
接口的方法集不必包含至少一个成员(即方法集为空)。它完全可以是空的( 源代码 ):
type I interface {} type T struct {} func (T) M() {} func main() { var i I = T{} _ = i }
空接口可以自动满足任意类型-因此任意类型的值都可以赋值给这样的接口类型值。动态类型或静态类型的行为应用于空接口,就像应用于非空接口。 空接口的显著使用存在于参数可变函数 fmt.Println 。
满足一个接口
每个实现了接口所有方法的类型都自动满足这个接口。我们不需要在这些类型中使用任何其他关键字(如 Java 中的 implements)来表示该类型实现了接口。它是由 go 语言的编译器自动实现的,而这儿正是该语言的强大之处( 源代码 ):
import ( "fmt" "regexp" ) type I interface { Find(b []byte) []byte } func f(i I) { fmt.Printf("%s\n", i.Find([]byte("abc"))) } func main() { var re = regexp.MustCompile(`b`) f(re) }
这里我们定义了一个由 regexp.Regexp 类型实现的接口,该接口内置的 regexp 模块没有任何改变。
行为抽象
接口类型值 只 允许访问它自己的接口类型的方法。如果它是 struct, array, scalar 等,便会隐藏有关确切值的详情( 源代码 ):
type I interface { M1() } type T int64 func (T) M1() {} func (T) M2() {} func main() { var i I = T(10) i.M1() i.M2() // i.M2 undefined (type I has no field or method M2) }
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。