7

Go 学习笔记8-Go接口

 1 year ago
source link: https://codeshellme.github.io/2022/06/go8/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

接口本质上是一种抽象,它的功能是解耦。尽管接口不是 Go 独有的,但专注于接口是编写强大而灵活的 Go 代码的关键。

Go 中的接口是非入侵性的,实现这不需要依赖(implement)接口定义,只需要实现接口中的方法即可。

Go 中的接口类型是由 type 和 interface 关键字定义的一组方法集合。在 Go 接口类型的方法集合中放入首字母小写的非导出方法也是合法的(很少使用)。

type MyInterface interface {
M1(int) error
M2(io.Writer, ...string)

1,空接口

如果一个接口类型定义中没有一个方法,那么它的方法集合就为空,称为空接口

type EmptyInterface interface {

通常不需要自己显式定义这类空接口类型,使用 interface{} 这个类型字面值作为所有空接口类型的代表就可以。

这些类型为接口类型的变量被称为接口类型变量,如果没有被显式赋予初值,接口类型变量的默认值为 nil

var err error // err 是一个 error 接口类型的实例变量
var r io.Reader // r 是一个 io.Reader 接口类型的实例变量

如果一个类型 T 的方法集合是某接口类型 I 的方法集合的等价集合或超集,我们就说类型 T 实现了接口类型 I,那么类型 T 的变量就可以作为合法的右值赋值给接口类型 I 的变量。

如果一个变量的类型是空接口类型,由于空接口类型的方法集合为空,这就意味着任何类型都实现了空接口的方法集合,所以我们可以将任何类型的值作为右值,赋值给空接口类型的变量,比如:

var i interface{} = 15 // ok
i = "hello, golang" // ok
type T struct{}
var t T
i = t // ok
i = &t // ok

空接口类型的这一可接受任意类型变量值作为右值的特性,让他成为 Go 加入泛型语法之前唯一一种具有“泛型”能力的语法元素,包括 Go 标准库在内的一些通用数据结构与算法的实现,都使用了空类型interface{} 作为数据元素的类型,这样我们就无需为每种支持的元素类型单独做一份代码拷贝了。

go1.18 增加了 any 关键字,用以替代现在的 interface{} 空接口类型:type any = interface{},实际上是 interface{} 的别名。

尽量避免使用空接口作为函数参数类型

我们要尽量避免使用空接口作为函数参数类型。一旦使用空接口作为函数参数类型,你将失去编译器为你提供的类型安全保护屏障。尽可能地抽象出带有一定行为契约的接口,并将它作为函数参数类型,尽量不要使用可以“逃过”编译器类型安全检查的空接口类型(interface{})。

Go 标准库以空接口 interface{} 为参数类型的方法和函数少之甚少,但也有少量,主要有两类:

  • 容器算法类,比如:container 下的 heap、list 和 ring 包、sort 包、sync.Map 等;
  • 格式化 / 日志类,比如:fmt 包、log 包等。

这些使用interface{}作为参数类型的函数或方法都有一个共同特点,就是它们面对的都是未知类型的数据,所以在这里使用具有“泛型”能力的interface{}类型。

我们也可以理解为是在 Go 语言尚未支持泛型的这个阶段的权宜之计。等 Go 泛型落地后,很多场合下 interface{}就可以被泛型替代了。

2,类型断言

通过接口类型变量“还原”它的右值的类型与值信息,这个过程被称为类型断言

语法如下:

// 其中 i 是某一个接口类型变量
// 如果 T 是一个非接口类型且 T 是想要还原的类型
// 那么这句代码的含义就是断言存储在接口类型变量 i 中的值的类型为 T
v, ok := i.(T)
  • 如果接口类型变量 i 之前被赋予的值确为 T 类型的值,变量 ok 的值将为 true,变量 v 的类型为 T,它的值会是之前变量 i 的右值
  • 如果 i 之前被赋予的值不是 T 类型的值,变量 ok 的值为 false,变量 v 的类型还是那个要还原的类型,但它的值是类型 T 的零值
// 如果 T 是一个接口类型,那么类型断言的语义就会变成:
// 断言 i 的值实现了接口类型 T。
v, ok := i.(T)
  • 如果断言成功,变量 v 的类型为 i 的值的类型,而并非接口类型 T
  • 如果断言失败,v 的类型信息为接口类型 T,它的值为 nil

不推荐的断言语法

// Go 也支持这种断言语法
v := i.(T)

在这种形式下:

  • 如果接口变量 i 之前被赋予的值不是 T 类型的值,那么这个语句将抛出 panic。
  • 如果变量 i 被赋予的值是 T 类型的值,那么变量 v 的类型为 T,它的值就会是之前变量 i 的右值。

由于可能出现 panic,所以不推荐使用这种类型断言的语法形式

非接口类型断言示例:

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)

接口类型断言示例:

type MyInterface interface {
type T int
func (T) M1() {
println("T's M1")
func main() {
var t T
var i interface{} = t
v1, ok := i.(MyInterface)
if !ok {
panic("the value of i is not MyInterface")
v1.M1()
fmt.Printf("the type of v1 is %T\n", v1) // the type of v1 is main.T
i = int64(13)
v2, ok := i.(MyInterface)
fmt.Printf("the type of v2 is %T\n", v2) // the type of v2 is <nil>
// v2 = 13 // cannot use 1 (type int) as type MyInterface in assignment: int does not implement MyInterface (missing M1 method)

3,尽量定义小接口

Go 中接口的特点:

  • 隐式契约,无需签署,自动生效
    • Go 语言中接口类型与它的实现者之间的关系是隐式的,不需要像其他语言(比如 Java)那样要求实现者显式放置“implements”进行修饰
    • 实现者只需要实现接口方法集合中的全部方法便算是遵守了契约,并立即生效了
  • 更倾向于“小接口”
    • Go 语言之父 Rob Pike 曾说“接口越大,抽象程度越弱
    • 如果契约太繁杂就会束缚了手脚,缺少了灵活性
    • 尽量定义小接口,即方法个数在 1~3 个之间的接口

如下示例:

// $GOROOT/src/builtin/builtin.go
type error interface {
Error() string
// $GOROOT/src/io/io.go
type Reader interface {
Read(p []byte) (n int, err error)
// $GOROOT/src/net/http/server.go
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(int)

4,接口的动静兼备特性

接口是 Go 这门静态语言中唯一“动静兼备”的语法特性。

接口类型变量在程序运行时可以被赋值为不同的动态类型变量,每次赋值后,接口类型变量中存储的动态类型信息都会发生变化。

接口的静态特性体现在接口类型变量具有静态类型,比如 var err error 中变量 err 的静态类型为 error。拥有静态类型,那就意味着编译器会在编译阶段对所有接口类型变量的赋值操作进行类型检查,编译器会检查右值的类型是否实现了该接口方法集合中的所有方法。如果不满足,就会报错。

var err error = 1
// cannot use 1 (type int) as type error in assignment: int does not implement error (missing Error method)

接口的动态特性,就体现在接口类型变量在运行时还存储了右值的真实类型信息,这个右值的真实类型被称为接口类型变量的动态类型。

接口类型变量在程序运行时可以被赋值为不同的动态类型变量,每次赋值后,接口类型变量中存储的动态类型信息都会发生变化。

Go 中的鸭子类型:

// 一个接口
type QuackableAnimal interface {
Quack()
// 一个 Duck 类型,并实现了 Quack 方法
type Duck struct{}
func (Duck) Quack() {
println("duck quack!")
// 一个 Dog 类型,并实现了 Quack 方法
type Dog struct{}
func (Dog) Quack() {
println("dog quack!")
// 一个 Bird 类型,并实现了 Quack 方法
type Bird struct{}
func (Bird) Quack() {
println("bird quack!")
// 该函数的参数类型使用的是接口类型
func AnimalQuackInForest(a QuackableAnimal) {
a.Quack()
func main() {
animals := []QuackableAnimal{new(Duck), new(Dog), new(Bird)}
for _, animal := range animals {
AnimalQuackInForest(animal)

5,接口类型的内部表示

接口类型的内部表示

接口类型“动静兼备”的特性也决定了它的变量的内部表示绝不像一个静态类型变量(如 int、float64)那样简单。

Go 接口类型的内部表示源码:

// $GOROOT/src/runtime/runtime2.go
type iface struct {
tab *itab
data unsafe.Pointer
type eface struct {
_type *_type
data unsafe.Pointer

在运行时层面,接口类型变量有两种内部表示:iface和eface,这两种表示分别用于不同的接口类型变量

  • eface 用于表示没有方法的空接口类型变量,也就是 interface{} 类型的变量
    • eface 表示的空接口类型并没有方法列表,因此它的第一个指针字段指向一个_type 类型结构,这个结构为该接口类型变量的动态类型的信息
    • 创建 eface 时,一般会为 data 重新分配新内存空间,将动态类型变量的值复制到这块内存空间,并将 data 指针指向这块内存空间。因此我们多数情况下看到的 data 指针值都是不同的。
  • iface 用于表示其余拥有方法的接口 interface 类型变量
    • iface 除了要存储动态类型信息之外,还要存储接口本身的信息(接口的类型信息、方法列表信息等)以及动态类型所实现的方法的信息,因此 iface 的第一个字段指向一个itab类型结构。

每个接口类型变量在运行时的表示都是由两部分组成的,针对不同接口类型,可以简化记作:

  • eface(_type, data)
  • iface(tab, data)

其中的 tab 和 _type 可以统一看作是动态类型的类型信息。

要判断两个接口类型变量是否相同,需要判断 _type/tab (指向的值)是否相同,以及 data 指针指向的内存空间存储的数据值是否相同(这里要注意不是 data 指针的值相同)。

在 Go 语言中,将任意类型赋值给一个接口类型变量是一个装箱操作,装箱实际就是创建一个 eface 或 iface 的过程。

一个用 eface 表示的空接口类型变量的例子:

type T struct {
s string
func main() {
var t = T {
n: 17,
s: "hello, interface",
var ei interface{} = t // Go运行时使用eface结构表示ei
  • ei 是一个空接口类型
  • 所以 ei 的类型是 eface

一个用 iface 表示的非空接口类型变量的例子:

type T struct {
s string
func (T) M1() {}
func (T) M2() {}
type NonEmptyInterface interface {
func main() {
var t = T{
n: 18,
s: "hello, interface",
var i NonEmptyInterface = t
  • NonEmptyInterface 是一个非空接口
  • i 是非空接口变量
  • 所以 i 的类型是 iface

Go 语言提供了 println 预定义函数,可以用来输出 eface 或 iface 的两个指针字段的值。

未赋初值的接口类型变量的值为 nil

func printNilInterface() {
// nil接口变量
var i interface{} // 空接口类型
var err error // 非空接口类型
println(i) // (0x0,0x0)
println(err) // (0x0,0x0)
println("i = nil:", i == nil) // true
println("err = nil:", err == nil) // true
println("i = err:", i == err) // true

无论是空接口类型还是非空接口类型变量,一旦变量值为 nil,那么它们内部表示均为(0x0,0x0),也就是类型信息、数据值信息均为空。

空接口类型变量的内部表示例子

func printEmptyInterface() {
var eif1 interface{} // 空接口类型
var eif2 interface{} // 空接口类型
var n, m int = 17, 18
eif1 = n
eif2 = m
println("eif1:", eif1) // eif1: (0x10ac580,0xc00007ef48)
println("eif2:", eif2) // eif2: (0x10ac580,0xc00007ef40)
println("eif1 = eif2:", eif1 == eif2) // false,类型相同,但值不相同,所以为 false
eif2 = 17
println("eif1:", eif1) // eif1: (0x10ac580,0xc00007ef48)
println("eif2:", eif2) // eif2: (0x10ac580,0x10eb3d0)
println("eif1 = eif2:", eif1 == eif2) // true,类型相同,值也相同
// 0xc00007ef48 和 0x10eb3d0是值的地址,而不是值
eif2 = int64(17)
println("eif1:", eif1) // eif1: (0x10ac580,0xc00007ef48)
println("eif2:", eif2) // eif2: (0x10ac640,0x10eb3d8)
println("eif1 = eif2:", eif1 == eif2) // false,值相同,但类型不同,所以为 false

Go 在创建 eface 时一般会为 data 重新分配新内存空间,将动态类型变量的值复制到这块内存空间,并将 data 指针指向这块内存空间。因此我们多数情况下看到的 data 指针值都是不同的(即使指针指向的内容是相同的)。

非空接口类型变量例子

type T int
func (t T) Error() string {
return "bad error"
func printNonEmptyInterface() {
var err1 error // 非空接口类型
var err2 error // 非空接口类型
err1 = (*T)(nil)
println("err1:", err1) // err1: (0x10ed120,0x0)
println("err1 = nil:", err1 == nil) // err1 = nil: false
err1 = T(5)
err2 = T(6)
println("err1:", err1) // err1: (0x10ed1a0,0x10eb310)
println("err2:", err2) // err2: (0x10ed1a0,0x10eb318)
println("err1 = err2:", err1 == err2) // err1 = err2: false 类型相同,值不同
err2 = fmt.Errorf("%d\n", 5)
println("err1:", err1) // err1: (0x10ed1a0,0x10eb310)
println("err2:", err2) // err2: (0x10ed0c0,0xc000010050)
println("err1 = err2:", err1 == err2) // err1 = err2: false 值相同,但类型不同

空接口类型变量与非空接口类型变量的等值比较

func printEmptyInterfaceAndNonEmptyInterface() {
var eif interface{} = T(5)
var err error = T(5)
println("eif:", eif) // eif: (0x10b3b00,0x10eb4d0)
println("err:", err) // err: (0x10ed380,0x10eb4d8)
println("eif = err:", eif == err) // eif = err: true
// 虽然 0x10b3b00 与 0x10ed380 不相等,但是它表示的只是地址,其地址指向的值是相等的
err = T(6)
println("eif:", eif) // eif: (0x10b3b00,0x10eb4d0)
println("err:", err) // err: (0x10ed380,0x10eb4e0)
println("eif = err:", eif == err) // eif = err: false
  • Go 在进行等值比较时,类型比较使用的是 eface 的 _type 和 iface 的 tab._type
  • 因此在这个例子中,当 eif 和 err 都被赋值为T(5)时,两者之间是划等号的

所以我们进行接口等值比较是一定要注意:

  • println 输出的 _type 和 tab 相等时,那么指向的值一定相等
  • println 输出的 _type 和 tab 不等时,那么指向的值不一定不相等
  • println 输出的 data 值,一般都是不等的,即使指向的值是相等的

6,为什么 nil error 值 != nil

一段 Go 代码:

type MyError struct {
error
func returnsError() error {
var p *MyError = nil
return p
func main() {
err := returnsError()
if err != nil {
// 代码走到这里,因为 err 类型信息与 nil 的类型信息不一样
// 用 println 打一下 err 和 nil 就行
fmt.Printf("err != nil, err:%#v %T\n", err, err)
println(err)
} else {
fmt.Printf("err == nil, err:%#v %T\n", err, err)
println(err)

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK