赞
踩
接口是双方约定的一种合作协议。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构。
接口内部存放的具体类型变量被称为接口指向的“实例”。接口只有声明没有实现,所以定义一个新接口,通常又变成声明一个新接口, 定义接口和声明接口二者通用,代表相同的意思。
最常使用的接口字面量类型就是空接口 interface{}
,由于空接口的方法集为空,所以任意类型都被认为实现了空接口,任意类型的实例都可以赋值或传递给空接口,包括非命名类型的实例。
Go
接口背后的本质是一种“契约”,通过契约我们可以将代码双方的耦合降至最低。Go
惯例上推荐尽量定义小接口,一般而言接口方法集合中的方法个数不要超过三个,单一方法的接口更受 Go
社区青睐。小接口有诸多优点,比如,抽象程度高、易于测试与实现、与组合的设计思想一脉相承、鼓励你编写组合的代码,等等。
这种“小接口”的 Go
惯例也已经被 Go
社区项目广泛采用。作者统计了早期版本的 Go
标准库(Go 1.13
版本)、Docker
项目(Docker 19.03 版本)以及 Kubernetes
项目(Kubernetes 1.17 版本)中定义的接口类型方法集合中方法数量,你可以看下:
从图中我们可以看到,无论是 Go
标准库,还是 Go
社区知名项目,它们基本都遵循了“尽量定义小接口”的惯例,接口方法数量在 1~3 范围内的接口占了绝大多数。
图片来源:https://time.geekbang.org/column/article/471952
注意:非命名类型由于不能定义自己的方法, 所以方法集为空,因此其类型变量除了传递给空接口,不能传递给任何其他接口。
Go
语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
/* 定义接口 */ type interface_name interface { method_name1 [return_type] method_name2 [return_type] method_name3 [return_type] ... method_namen [return_type] } /* 定义结构体 */ type struct_name struct { /* variables */ } /* 实现接口方法 */ func (struct_name_variable struct_name) method_name1() [return_t ype] { /* 方法实现 */ } ... func (struct_name_variable struct_name) method_namen() [return_t ype] { /* 方法实现*/ }
使用示例:
package main import ( "fmt" ) type Phone interface { call() } type NokiaPhone struct { } type IPhone struct { } func (nokiaPhone NokiaPhone) call() { fmt.Println("I am Nokia, I can call you!") } func (iPhone IPhone) call() { fmt.Println("I am iPhone, I can call you!") } func main() { var phone Phone phone = new(NokiaPhone) phone.call() // I am Nokia, I can call you! phone = new(IPhone) phone.call() // I am iPhone, I can call you! }
Go
语言的接口分为接口字面量类型和接口命名类型, 接口的声明使用 interface
关键字。
接口字面量类型的声明语法如下:
interface {
MethodSignature1
MethodSignature2
}
使用接口字面量的场景很少,一般只有空接口 interface{}
类型变量的声明才会使用。
接口命名类型使用 type
关键字声明,语法如下:
type InterfaceName interface {
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
}
对各个部分的说明:
type
将接口定义为自定义的类型名。 Go
语言的接口在命名时,一般会在单词后面添加 er
,如有写操作的接口叫 Writer
,有字符串功能的接口叫 Stringer
,有关闭功能的接口叫 Closer
等;接口定义使用方法声明,而不是方法签名,因为方法名是接口的组成部分。例如:
// 方法声明=方法名+方法签名
MethodName (InputTypeList )OutputTypeList
接口中的“方法声明”非常类似于 C
语言中的函数声明的概念, Go
编译器在做接口匹配判断时是严格校验方法名称和方法签名的。
接口定义大括号内可以是方法声明的集合,也可以嵌入另一个接口类型匿名字段,还可以是二者的混合。接口支持嵌入匿名接口宇段,就是一个接口定义里面可以包括其他接口, Go
编译器会自动进行展开处理,有点类似 C
语言中宏的概念。例如:
type Reader interface { Read(p []byte) (n int , err error) } type Writer interface { Write(p []byte) (n int , err error) } // 如下3 种声明是等价的,最终的展开模式都是第 3 种格式 type ReadWriter interface { Reader Writer } type ReadWriter interface { Reader Write(p []byte) (n int , err error) } type ReadWriter interface { Read(p []byte) (n int, err error) Write(p []byte) (n int, err error) }
Writer
这个接口可以调用 Write()
方法写入一个字节数组( []byte
),返回值告知写入字节数( n int
)和可能发生的错误( err error
)
我们在接口类型的方法集合中声明的方法,它的参数列表不需要写出形参名字,返回值列表也是如此。也就是说,方法的参数列表中形参名字与返回值列表中的具名返回值,都不作为区分两个方法的凭据。
type MyInterface interface {
M1(int) error
M2(io.Writer, ...string)
}
比如下面的 MyInterface
接口类型的定义与上面的 MyInterface
接口类型定义都是等价的:
type MyInterface interface {
M1(a int) error
M2(w io.Writer, strs ...string)
}
type MyInterface interface {
M1(n int) error
M2(w io.Writer, args ...string)
}
不过,Go
语言要求接口类型声明中的方法必须是具名的,并且方法名字在这个接口类型的方法集合中是唯一的。
Go 1.14
版本以后,Go
接口类型允许嵌入的不同接口类型的方法集合存在交集,但前提是交集中的方法不仅名字要一样,它的函数签名部分也要保持一致,也就是参数列表与返回值列表也要相同,否则 Go
编译器照样会报错。
比如下面示例中 Interface3
嵌入了 Interface1
和 Interface2
,但后两者交集中的 M1
方法的函数签名不同,导致了编译出错:
type Interface1 interface {
M1()
}
type Interface2 interface {
M1(string)
M2()
}
type Interface3 interface{
Interface1
Interface2 // 编译器报错:duplicate method M1
M3()
}
er
”结尾;func
引导;接口类型一旦被定义后,它就和其他 Go
类型一样可以用于声明变量,比如:
var err error // err是一个error接口类型的实例变量
var r io.Reader // r是一个io.Reader接口类型的实例变量
接口只有被初始化为具体的类型时才有意义。没有初始化的接口变量,其默认值是 nil
。
var i io.Reader
fmt.Printf("%T\n", i) // nil
这些类型为接口类型的变量被称为接口类型变量,如果没有被显式赋予初值,接口类型变量的默认值为 nil
。如果要为接口类型变量显式赋予初值,我们就要为接口类型变量选择合法的右值。
接口绑定具体类型的实例的过程称为接口初始化。接口变量支持两种直接初始化方法, 具体如下。
如果具体类型实例的方法集是某个接口的方法集的超集,则称该具体类型实现了接口,可以将该具体类型的实例直接赋值给接口类型的变量,此时编译器会进行静态的类型检查。接口被初始化后,调用接口的方法就相当于调用接口绑定的具体类型的方法,这就是接口调用的语义。
已经初始化的接口类型变量 a
直接赋值给另一种接口变量 b
,要求 b
的方法集是 a
的方法集的子集。此时 Go
编译器会在编译时进行方法集静态检查。这个过程也是接口初始化的一种方式,此时接口变量 b
绑定的具体实例是接口变量 a
绑定的具体实例的副本。例如:
file , := os .OpenFile ( "notes.txt", os .O_RDWR | os .O_CREATE , 0755 )
var rw io.ReadWriter = file
//io.ReadWriter 接口可以直接赋值给 io.Writer 接口变量
var w io.Writer = rw
接口方法调用和普通的函数调用是有区别的。接口方法调用的最终地址是在运行期决定的,将具体类型变量赋值给接口后,会使用具体类型的方法指针初始化接口变量,当调用接口变量的方法时,实际上是间接地调用实例的方法。接口方法调用不是一种直接的调用,有一定的运行时开销。
直接调用未初始化的接口变量的方法会产生 panic
。例如:
package main type Printer interface { Print() } type S struct{} func (s S) Print() { println("print") } func main() { var i Printer // 没有初始化的接口调用其方法会产生 panic // panic: runtime error: invalid memory address or nil pointer dereference // i.Print() // i 必须初始化 i = S{} i.Print() }
编程过程中有时需要确认已经初始化的接口变量指向实例的具体类型是什么,也需要检查运行时的接口类型。 Go
语言提供两种语法结构来支持这两种需求,分别是类型断言和类型查询。
Go
的语言中提供了断言的功能。Go
中的所有程序都实现了 interface{}
的接口,这意味着,所有的类型如 string
, int
, int64
甚至是自定义的 struct
类型都就此拥有了 interface{}
的接口,那么在一个数据通过 func funcName(interface{})
的方式传进来的时候,也就意味着这个参数被自动的转为 interface{}
的类型。
func funcName(a interface{}) string {
return string(a)
}
编译器报错:
cannot convert a (type interface{}) to type string: need type as
sertion
此时,意味着整个转化的过程需要类型断言。
接口类型断言的语法形式如下:
var i interface
i.(T)
i
必须是接口变量,如果是具体类型变量,则编译器会报 non - interface type xxx on left
T
可以是接口类型名,也可以是具体类型名。那么这句代码的含义就是断言存储在接口类型变量 i
中的值的类型为 T
。
func main() {
a := 1
v := a.(int)
fmt.Println(a)
}
报错:
invalid type assertion: a.(int) (non-interface type int on left)
修改后的代码:
func main() {
a := 1
v, ok := interface{}(a).(int) // 将 a 转换为接口类型
if ok {
fmt.Printf("v type is %T\n", v)
}
fmt.Println(a)
}
在 Go
语言中,interface{}
代表空接口,任何类型都是它的实现类型。现在你只要知道,任何类型的值都可以很方便地被转换成空接口的值就行了。
你可能会对这里的 {}
产生疑惑,为什么在关键字 interface
的右边还要加上这个东西?
请记住,一对不包裹任何东西的花括号,除了可以代表空的代码块之外,还可以用于表示不包含任何内容的数据结构(或者说数据类型)。
比如你今后肯定会遇到的 struct{}
,它就代表了不包含任何字段和方法的、空的结构体类型。而空接口 interface{}
则代表了不包含任何方法定义的、空的接口类型。
当然了,对于一些集合类的数据类型来说,{}
还可以用来表示其值不包含任何元素,比如空的切片值 []string{}
,以及空的字典值 map[int]string{}
。
接口查询的两层语义
TypeNname
是一个具体类型名,则类型断言用于判断接口变量 i
绑定的实例类型是否就是具体类型 TypeNname
。TypeName
是一个接口类型名,则类型断言用于判断接口变量 i
绑定的实例类型是否同时实现了 TypeName
接口。Go
中的 interface
类型是不能直接转换成其他类型的,需要使用到断言。
package main
func main() {
var itf interface{} = 1
i, ok := itf.(string)
println("值:", i, "; 断言结果", ok)
j, ok := itf.(int)
println("值:", j, "; 断言结果", ok)
}
接口断言的两种语法表现:
o := i.(TypeName)
分析:
TypeName
是具体类型名,此时如果接口 i
绑定的实例类型就是具体类型 TypeName
,则变量 o
的类型就是 TypeName
, 变量 o
的值就是接口绑定的实例值的副本(当然实例可能是指针值,那就是指针值的副本) 。TypeName
是接口类型名, 如果接口 i
绑定的实例类型满足接口类型 TypeName
,则变量 o
的类型就是接口类型TypeName
, o
底层绑定的具体类型实例是 i
绑定的实例的副本(当然实例可能是指针值,那就是指针值的副本〉。panic
。示例代码:
package main import "fmt" type Inter interface { Ping() Pang() } type Anter interface { Inter String() } type St struct { Name string } func (St) Ping() { println("ping") } func (*St) Pang() { println("pang") } func main() { st := &St{"abcd"} var i interface{} = st // 判断i 绑定的实例是否实现了接口类型Inter o := i.(Inter) o.Ping() o.Pang() /* 如下语句会引发 panic ,因为 i 没有实现接口Anter p := i.(Anter) p.String() */ // 判断 i 绑定的实例是否就是具体类型 St s := i.(*St) fmt.Printf("%s", s.Name) }
由于可能出现 panic
,所以我们并不推荐使用这种类型断言的语法形式。
关于类型断言,需要注意两点:
i
是一个非接口值,那么必须在做类型断言之前把它转换为接口值。因为 Go
中的任何类型都是空接口类型的实现类型,所以一般会这样做: interface{}(i).(TypeNname)
。var i1, ok := interface{}(i).(TypeNname)
其中 ok 值体现了类型断言的成败,如果成功,i1
就会是经过类型转换后的 TypeNname
类型的值,否则它将会是 TypeNname
类型的零值(或称为默认值)
func main() {
a := "1"
// var b int
b, ok := interface{}(a).(int)
if ok {
fmt.Printf("b type is %v", b)
} else {
fmt.Printf("b is %v", b) // b is 0
}
}
if o, ok := i.(TypeName); ok {
}
语义分析:
TypeName
是具体类型名,此时如果接口 i
绑定的实例类型就是具体类型 TypeName
,则 ok
为 true
, 变量 o
的类型就是 TypeName
,变量 o
的值就是接口绑定的实例值的副本(当然实例可能是指针值,那就是指针值的副本) 。TypeName
是接口类型名, 此时如果接口 i
绑定的实例的类型满足接口类型 TypeName
, 则 ok
为 true
,变量 o
的类型就是接口类型 TypeName
, o
底层绑定的具体类型实例是 i
绑定的实例的副本(当然实例可能是指针值,那就是指针值的副本)。ok
为 false
, 变量 o
是 TypeName
类型的“零值”,此种条件分支下程序逻辑不应该再去引用。因为此时的。没有意义。value, ok := a.(string)
总的来说:如果断言失败,那么 ok
的值将会是 false
,但是如果断言成功 ok
的值将会是 true
,同时value
将会得到所期待的正确的值。
var a int64 = 13
var i interface{} = a
v1, ok := i.(int64)
fmt.Printf("v1=%d, the type of v1 is %T, ok=%t\n", v1, v1, ok) // v1=13, the type of v1 is int64, ok=true
v2, ok := i.(string)
fmt.Printf("v2=%s, the type of v2 is %T, ok=%t\n", v2, v2, ok) // v2=, the type of v2 is string, ok=false
v3 := i.(int64)
fmt.Printf("v3=%d, the type of v3 is %T\n", v3, v3) // v3=13, the type of v3 is int64
v4 := i.([]int) // panic: interface conversion: interface {} is int64, not []int
fmt.Printf("the type of v4 is %T\n", v4)
修改上述代码:
func main() { st := &St{"abcd"} var i interface{} = st // 判断i 绑定的实例是否实现了接口类型Inter if o, ok := i.(Inter); ok { o.Ping() o.Pang() } // i 没有实现接口 Anter if p, ok := i.(Anter); ok { p.String() } // 判断 i 绑定的实例是否就是具体类型 St if s, ok := i.(*St); ok { fmt.Printf("%s", s.Name) } }
另外一个完整的示例如下:
package main import "fmt" /* func funcName(a interface{}) string { return string(a) } */ func funcName(a interface{}) string { value, ok := a.(string) if !ok { fmt.Println("It is not ok for type string") return "" } fmt.Println("The value is ", value) return value } func main() { // str := "123" // funcName(str) //var a interface{} //var a string = "123" var a int = 10 funcName(a) }
类型查询,就是根据变量,查询这个变量的类型。为什么会有这样的需求呢?
Go
中有一个特殊的类型 interface{}
,这个类型可以被任何类型的变量赋值,如果想要知道到底是哪个类型的变量赋值给了 interface{}
类型变量,就需要使用类型查询来解决这个需求,示例代码如下:
func main() {
var x interface{} = 13
switch x.(type) {
case nil:
println("x is nil")
case int:
println("the type of x is int")
case string:
println("the type of x is string")
case bool:
println("the type of x is string")
default:
println("don't support the type")
}
}
输出结果:
the type of x is int
不过,通过 x.(type)
,我们除了可以获得变量 x
的动态类型信息之外,也能获得其动态类型对应的值信息,现在我们把上面的例子改造一下:
func main() { var x interface{} = 13 switch v := x.(type) { case nil: println("v is nil") case int: println("the type of v is int, v =", v) case string: println("the type of v is string, v =", v) case bool: println("the type of v is bool, v =", v) default: println("don't support the type") } }
这里我们将 switch
后面的表达式由 x.(type)
换成了 v := x.(type)
。对于后者,你千万不要认为变量 v
存储的是类型信息,其实 v
存储的是变量 x
的动态类型对应的值信息,这样我们在接下来的 case
执行路径中就可以使用变量 v
中的值信息了。
输出结果
the type of v is int, v = 13
package main import ( "fmt" ) func main() { // 定义一个interface{}类型变量,并使用string类型值”abc“初始化 var a interface{} = "abc" // 在switch中使用 变量名.(type) 查询变量是由哪个类型数据赋值。 switch v := a.(type) { case string: fmt.Println("字符串") case int: fmt.Println("整型") default: fmt.Println("其他类型", v) } }
如果使用 .(type)
查询类型的变量不是 interface{}
类型,则在编译时会报如下错误:
cannot type switch on non-interface value a (type string)
如果在 switch
以外地方使用 .(type)
,则在编译时会提示如下错误:
use of .(type) outside type switch
所以,使用 type
进行类型查询时,只能在 switch
中使用,且使用类型查询的变量类型必须是 interface{}
。
接口类型查询的语法格式如下:
switch v := i.(type) {
case typel :
xx xx
case type2 :
xx xx
default :
xx xx
}
类型查询和类型断言
case
字句一次判断多个类型,类型断言一次只能判断一个类型,当然类型断言也可以使用 if-else-if
语句达到同样的效果。示例如下:
func main() { var t interface{} t = functionOfSomeType() switch t := t.(type) { default: fmt.Printf("unexpected type %T", t) // %T prints whatever type t has case bool: fmt.Printf("boolean %t\n", t) // t has type bool case int: fmt.Printf("integer %d\n", t) // t has type int case *bool: fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool case *int: fmt.Printf("pointer to integer %d\n", *t) // t has type *int } }
或者使用 if-else-if
代替
func sqlQuote(x interface{}) string { if x == nil { return "NULL" } else if _, ok := x.(int); ok { return fmt.Sprintf("%d", x) } else if _, ok := x.(uint); ok { return fmt.Sprintf("%d", x) } else if b, ok := x.(bool); ok { if b { return "TRUE" } return "FALSE" } else if s, ok := x.(string); ok { return sqlQuoteString(s) // (not shown) } else { panic(fmt.Sprintf("unexpected type %T: %v", x, x)) } }
package main import ( "fmt" ) var container = []string{"aaa", "bbb", "ccc"} func main() { container := map[string]string{"a": "aaa", "b": "bbb", "c": "ccc"} // 方式1。类型断言 _, ok1 := interface{}(container).([]string) _, ok2 := interface{}(container).(map[string]string) // %T 表示该值的 Go 类型 if !(ok1 || ok2) { fmt.Printf("Error: unsupported container type: %T\n", container) return } fmt.Printf("The element is %#v , (container type: %T)\n", container["a"], container) // 方式2。 elem, err := getElement(container) if err != nil { fmt.Printf("Error: %s\n", err) return } fmt.Printf("The element is %#v , (container type: %T)\n", elem, container) } // 空接口包含所有的类型,输入的参数均会被转换为空接口 // 函数入参已经声明 containerI 为 interface 类型,所以不需要再次进行 interface{}(container) 转换 func getElement(containerI interface{}) (elem string, err error) { // 变量类型会被保存在t中 // 方式2。类型查询 switch t := containerI.(type) { case []string: elem = t[1] case map[string]string: elem = t["a"] default: err = fmt.Errorf("unsupported container type: %T", containerI) return } return }
如果一个接口类型定义中没有一个方法,那么它的方法集合就为空,比如下面的 EmptyInterface
接口类型:
type EmptyInterface interface {
}
这个方法集合为空的接口类型就被称为空接口类型,但通常我们不需要自己显式定义这类空接口类型,我们直接使用 interface{}
这个类型字面值作为所有空接口类型的代表就可以了。
由于 Go
是强类型的语言,使用空接口可以为外界提供一个更加通用的能力。然而在处理接口的过程中却需要默默承受解析空接口带来的痛苦。通过使用空接口,常见的 fmt.Println
函数提供了打印任何类型的功能。
func Println(a ...interface{}) (n int, err error) {
return Fprintln(os.Stdout, a...)
}
如果不使用空接口,那么每一个类型都需要实现一个对应的 Println
函数,是非常不方便的。
不过,空接口带来便利的同时,也意味着我们必须在内部解析接口的类型,并对不同的类型进行相应的处理。以 fmt.Println
为例,Println
函数内部通过检测接口的具体类型来调用不同的处理函数。如果是自定义类型,还需要使用反射、递归等手段完成复杂类型的打印功能。
func (p *pp) printArg(arg interface{}, verb rune) {
switch f := arg.(type) {
case bool:
p.fmtBool(f, verb)
case float32:
p.fmtFloat(float64(f), 32, verb)
case float64:
p.fmtFloat(f, 64, verb)
case complex64:
p.fmtComplex(complex128(f), 64, verb)
....
}
类似的 API
设计还有用于序列化与反序列化的 JSON
标准库等。
JSON
标准库内部使用了反射来判断接口中存储的实际类型,以此分配不同的序列化器。
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
...
switch t.Kind() {
case reflect.Bool:
return boolEncoder
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return intEncoder
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return uintEncoder
...
}
除此之外,对于跨服务调用的 API
,使用空接口可以提高它们的扩展性。因为在这种场景下,修改 API
的成本通常比较高,服务器需要改造并发布新的 SDK
,客服端还需要适配新的 SDK
并联调测试。
如下所示,在 Info
结构体中增加扩展类型 map[string]interface{}
,新的功能如果需要传递新的信息,当前服务甚至可以不用修改 API
。
type info struct{
ExtraData map[string]interface{} `json:"extra_data"`
...
}
可以看出,空接口为 API
带来了扩展性和灵活性,但是也为模块的内部处理增加了额外的成本。因为 API
内部处理空接口时使用了大量的反射,而反射通常比较消耗性能。
Go
语言没有泛型, 如果一个函数需要接收任意类型的参数, 则参数类型可以使用空接口类型,空接口不是真的为空,接口有类型和值两个概念。
package main import "fmt" type Inter interface { Ping() Pang() } type St struct{} func (St) Ping() { println("ping") } func (*St) Pang() { println("pang") } func main() { var st *St = nil var it Inter = st fmt.Printf("%p\n", st) // 0x0 fmt.Printf("%p\n", it) // 0x0 if it != nil { it.Pang() // pang // 下面的语句会导致panic // 方法转换为函数调用,第一个参数是St 类型,由于 *St 是nil ,无法获取指针所指的对象值,所以导致panic // it.Ping() } }
这个程序暴露出 Go
语言的一点瑕疵, fmt.Printf("%p\n", it)
的结果是 0x0
,但 it! = nil
的判断结果却是 true
。
空接口有两个字段, 一个是实例类型, 另一个是指向绑定实例的指针,只有两个都为 nil
时,空接口才为 nil
。
Go
规定:如果一个类型 T
的方法集合是某接口类型 I
的方法集合的等价集合或超集,我们就说类型 T
实现了接口类型 I
,那么类型 T
的变量就可以作为合法的右值赋值给接口类型 I
的变量。
如果一个变量的类型是空接口类型,由于空接口类型的方法集合为空,这就意味着任何类型都实现了空接口的方法集合,所以我们可以将任何类型的值作为右值,赋值给空接口类型的变量,
var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)
// or
var i interface{} = 15 // ok
i = "hello, golang" // ok
type T struct{}
var t T
i = t // ok
i = &t // ok
参考书籍:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。