1. 接口概念
接口是双方约定的一种合作协议。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构。
接口内部存放的具体类型变量被称为接口指向的“实例”。接口只有声明没有实现,所以定义一个新接口,通常又变成声明一个新接口, 定义接口和声明接口二者通用,代表相同的意思。
最常使用的接口字面量类型就是空接口interface{}
,由于空接口的方法集为空,所以任意类型都被认为实现了空接口,任意类型的实例都可以赋值或传递给空接口,包括非命名类型的实例。
Go
接口背后的本质是一种“契约”,通过契约我们可以将代码双方的耦合降至最低。Go
惯例上推荐尽量定义小接口,一般而言接口方法集合中的方法个数不要超过三个,单一方法的接口更受Go
社区青睐。小接口有诸多优点,比如,抽象程度高、易于测试与实现、与组合的设计思想一脉相承、鼓励你编写组合的代码,等等。
这种“小接口”的Go
惯例也已经被Go
社区项目广泛采用。作者统计了早期版本的Go
标准库(Go 1.13
版本)、Docker
项目(Docker 19.03 版本)以及Kubernetes
项目(Kubernetes 1.17 版本)中定义的接口类型方法集合中方法数量,你可以看下:
从图中我们可以看到,无论是Go
标准库,还是Go
社区知名项目,它们基本都遵循了“尽量定义小接口”的惯例,接口方法数量在 1~3 范围内的接口占了绝大多数。
图片来源:/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_type] {/* 方法实现 */}...func (struct_name_variable struct_name) method_namen() [return_type] {/* 方法实现*/}
使用示例:
package mainimport ("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 Phonephone = new(NokiaPhone)phone.call()// I am Nokia, I can call you!phone = new(IPhone)phone.call()// I am iPhone, I can call you!}
2. 接口声明
Go
语言的接口分为接口字面量类型和接口命名类型, 接口的声明使用interface
关键字。
接口字面量类型的声明语法如下:
interface {MethodSignature1MethodSignature2}
使用接口字面量的场景很少,一般只有空接口interface{}
类型变量的声明才会使用。
接口命名类型使用type
关键字声明,语法如下:
type InterfaceName interface {方法名1( 参数列表1 ) 返回值列表1方法名2( 参数列表2 ) 返回值列表2}
对各个部分的说明:
接口类型名:使用type
将接口定义为自定义的类型名。Go
语言的接口在命名时,一般会在单词后面添加er
,如有写操作的接口叫Writer
,有字符串功能的接口叫Stringer
,有关闭功能的接口叫Closer
等;方法名:当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问;参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略;
2.1 方法声明
接口定义使用方法声明,而不是方法签名,因为方法名是接口的组成部分。例如:
// 方法声明=方法名+方法签名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 {ReaderWriter}type ReadWriter interface {ReaderWrite(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) errorM2(io.Writer, ...string)}
比如下面的MyInterface
接口类型的定义与上面的MyInterface
接口类型定义都是等价的:
type MyInterface interface {M1(a int) errorM2(w io.Writer, strs ...string)}type MyInterface interface {M1(n int) errorM2(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{Interface1Interface2 // 编译器报错:duplicate method M1M3()}
2.2 新接口类型声明特点
接口的命名一般以“er
”结尾;接口定义的内部方法声明不需要func
引导;在接口定义中,只有方法声明没有方法实现;3. 接口初始化
接口类型一旦被定义后,它就和其他Go
类型一样可以用于声明变量,比如:
var err error // err是一个error接口类型的实例变量var r io.Reader // r是一个io.Reader接口类型的实例变量
接口只有被初始化为具体的类型时才有意义。没有初始化的接口变量,其默认值是nil
。
var i io.Readerfmt.Printf("%T\n", i)// nil
这些类型为接口类型的变量被称为接口类型变量,如果没有被显式赋予初值,接口类型变量的默认值为nil
。如果要为接口类型变量显式赋予初值,我们就要为接口类型变量选择合法的右值。
接口绑定具体类型的实例的过程称为接口初始化。接口变量支持两种直接初始化方法, 具体如下。
3.1 实例赋值接口
如果具体类型实例的方法集是某个接口的方法集的超集,则称该具体类型实现了接口,可以将该具体类型的实例直接赋值给接口类型的变量,此时编译器会进行静态的类型检查。接口被初始化后,调用接口的方法就相当于调用接口绑定的具体类型的方法,这就是接口调用的语义。
3.2 接口变量赋值接口变量
已经初始化的接口类型变量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
4. 接口方法调用
接口方法调用和普通的函数调用是有区别的。接口方法调用的最终地址是在运行期决定的,将具体类型变量赋值给接口后,会使用具体类型的方法指针初始化接口变量,当调用接口变量的方法时,实际上是间接地调用实例的方法。接口方法调用不是一种直接的调用,有一定的运行时开销。
直接调用未初始化的接口变量的方法会产生panic
。例如:
package maintype 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()}
5. 接口运算
编程过程中有时需要确认已经初始化的接口变量指向实例的具体类型是什么,也需要检查运行时的接口类型。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 assertion
此时,意味着整个转化的过程需要类型断言。
5.1 类型断言
接口类型断言的语法形式如下:
var i interfacei.(T)
i
必须是接口变量,如果是具体类型变量,则编译器会报non - interface type xxx on left
T
可以是接口类型名,也可以是具体类型名。
那么这句代码的含义就是断言存储在接口类型变量i
中的值的类型为T
。
func main() {a := 1v := a.(int)fmt.Println(a)}
报错:
invalid type assertion: a.(int) (non-interface type int on left)
修改后的代码:
func main() {a := 1v, 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 mainfunc main() {var itf interface{} = 1i, ok := itf.(string)println("值:", i, "; 断言结果", ok)j, ok := itf.(int)println("值:", j, "; 断言结果", ok)}
接口断言的两种语法表现:
5.1.1 直接赋值模式
o := i.(TypeName)
分析:
TypeName
是具体类型名,此时如果接口i
绑定的实例类型就是具体类型TypeName
,则变量o
的类型就是TypeName
, 变量o
的值就是接口绑定的实例值的副本(当然实例可能是指针值,那就是指针值的副本) 。TypeName
是接口类型名, 如果接口i
绑定的实例类型满足接口类型TypeName
,则变量o
的类型就是接口类型TypeName
,o
底层绑定的具体类型实例是i
绑定的实例的副本(当然实例可能是指针值,那就是指针值的副本〉。如果上述两种情况都不满足, 则程序抛出panic
。
示例代码:
package mainimport "fmt"type Inter interface {Ping()Pang()}type Anter interface {InterString()}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 绑定的实例是否实现了接口类型Intero := i.(Inter)o.Ping()o.Pang()/*如下语句会引发 panic ,因为 i 没有实现接口Anterp := i.(Anter)p.String()*/// 判断 i 绑定的实例是否就是具体类型 Sts := i.(*St)fmt.Printf("%s", s.Name)}
由于可能出现panic
,所以我们并不推荐使用这种类型断言的语法形式。
关于类型断言,需要注意两点:
如果i
是一个非接口值,那么必须在做类型断言之前把它转换为接口值。因为Go
中的任何类型都是空接口类型的实现类型,所以一般会这样做:interface{}(i).(TypeNname)
。如果类型断言的结果为否,就意味着该类型断言是失败的,失败的类型断言会引发 panic(运行时异常),解决方法是:
var i1, ok := interface{}(i).(TypeNname)
其中 ok 值体现了类型断言的成败,如果成功,i1
就会是经过类型转换后的TypeNname
类型的值,否则它将会是TypeNname
类型的零值(或称为默认值)
func main() {a := "1"// var b intb, ok := interface{}(a).(int)if ok {fmt.Printf("b type is %v", b)} else {fmt.Printf("b is %v", b)// b is 0}}
5.1.2 comma,ok 表达式模式
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 = 13var i interface{} = av1, 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=truev2, 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=falsev3 := i.(int64) fmt.Printf("v3=%d, the type of v3 is %T\n", v3, v3) // v3=13, the type of v3 is int64v4 := i.([]int) // panic: interface conversion: interface {} is int64, not []intfmt.Printf("the type of v4 is %T\n", v4)
修改上述代码:
func main() {st := &St{"abcd"}var i interface{} = st// 判断i 绑定的实例是否实现了接口类型Interif o, ok := i.(Inter); ok {o.Ping()o.Pang()}// i 没有实现接口 Anterif p, ok := i.(Anter); ok {p.String()}// 判断 i 绑定的实例是否就是具体类型 Stif s, ok := i.(*St); ok {fmt.Printf("%s", s.Name)}}
另外一个完整的示例如下:
package mainimport "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 = 10funcName(a)}
5.2 类型查询
类型查询,就是根据变量,查询这个变量的类型。为什么会有这样的需求呢?
Go
中有一个特殊的类型interface{}
,这个类型可以被任何类型的变量赋值,如果想要知道到底是哪个类型的变量赋值给了interface{}
类型变量,就需要使用类型查询来解决这个需求,示例代码如下:
func main() {var x interface{} = 13switch 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{} = 13switch 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 mainimport ("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 xxcase type2 :xx xxdefault :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 hascase bool:fmt.Printf("boolean %t\n", t) // t has type boolcase int:fmt.Printf("integer %d\n", t) // t has type intcase *bool:fmt.Printf("pointer to boolean %t\n", *t) // t has type *boolcase *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))}}
5.3 类型断言和查询总结
package mainimport ("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}
6. 空接口
如果一个接口类型定义中没有一个方法,那么它的方法集合就为空,比如下面的EmptyInterface
接口类型:
type EmptyInterface interface {}
这个方法集合为空的接口类型就被称为空接口类型,但通常我们不需要自己显式定义这类空接口类型,我们直接使用interface{}
这个类型字面值作为所有空接口类型的代表就可以了。
Go
语言没有泛型, 如果一个函数需要接收任意类型的参数, 则参数类型可以使用空接口类型,空接口不是真的为空,接口有类型和值两个概念。
package mainimport "fmt"type Inter interface {Ping()Pang()}type St struct{}func (St) Ping() {println("ping")}func (*St) Pang() {println("pang")}func main() {var st *St = nilvar it Inter = stfmt.Printf("%p\n", st) // 0x0fmt.Printf("%p\n", it) // 0x0if 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 = trueany = 12.34any = "hello"any = map[string]int{"one": 1}any = new(bytes.Buffer)// orvar i interface{} = 15 // oki = "hello, golang" // oktype T struct{}var t Ti = t // oki = &t // ok
参考书籍:
Go 语言核心编程Go 语言圣经
Go 学习笔记(35)— Go 接口 interface (接口声明 接口初始化 接口方法调用 接口运算 类型断言 类型查询 空接口)