9,Go 函数

函数在 Go 语言中属于“一等公民”,拥有“一等公民”待遇的语法元素可以存储在变量中,可以作为参数传递给函数,可以在函数内部创建并可以作为返回值从函数返回。

1,函数的定义

Go 函数样式:

在这里插入图片描述

函数的变量声明形式:

在这里插入图片描述

在 Go 语言中,每个函数声明所定义的函数,仅仅是对应的函数类型的一个实例,即函数也是变量,函数名就是变量名,等号后边就是对应的函数的类型。

比如上面函数的类型就是:

func(io.Writer, string, ...interface{}) (int, error)

... 是 Go 中的变长参数变长参数是通过切片来实现的。所以,在函数体中,就可以使用切片支持的所有操作来操作变长参数

Go 语言支持函数多返回值:

func foo()                       // 无返回值
func foo() error                 // 仅有一个返回值
func foo() (int, string, error)  // 有 2 或 2 个以上返回值
func foo() func()                // 函数的返回值类型是一个函数
func foo(func())                 // 函数的参数类型是一个函数

函数返回值可以声明变量名,这种带有名字的返回值被称为具名返回值,其可以像函数体中声明的局部变量一样在函数体内使用。

Go 标准库以及大多数项目代码中的函数,都选择了使用普通的非具名返回值形式。但在一些特定场景下,具名返回值也会得到应用。

比如,当函数的返回值个数较多时,每次显式使用 return 语句时都会接一长串返回值,这时,我们用具名返回值可以让函数实现的可读性更好一些。

基于函数自定义类型

// $GOROOT/src/net/http/server.go
type HandlerFunc func(ResponseWriter, *Request)

// $GOROOT/src/sort/genzfunc.go
type visitFunc func(ast.Node) ast.Visitor

2,设计健壮函数的三原则

  • 不要相信任何外部输入的参数:对输入做合法性校验
  • 不要忽略任何一个错误:判断每一个调用的返回值
  • 不要假定异常不会发生:对可能发生异常的地方做异常处理

3,fmt.Print 函数

format 分类:

format 含义
一般类型
%v 值的默认格式,输出结构体
%+v 输出结构体显示字段名
%#v 输出结构体源代码片段
%T 输出值的类型
布尔类型
%t 输出布尔类型值
整数类型
%b 输出标准的二进制格式化
%c 输出对应的unicode码的一个字符
%d 输出标准的十进制格式化
%o 输出标准的八进制格式化
%q 要输出的值是双引号输出就是双引号字符串;另外一种是自转义的 unicode 单引号字符
%x 输出十六进制编码,字母形式为小写 a-f
%X 输出十六进制编码,字母形式为大写 A-F
%U 输出Unicode格式
浮点数与复数
%b 无小数部分、二进制指数的科学计数法
%e 科学计数法(小写e)
%E 科学计数法(小写E)
%f 十进制小数
%F 和 %f 一样
%g 根据实际情况采用%e或%f格式(以获得更简洁、准确的输出)
%G 根据实际情况采用%E或%F格式(以获得更简洁、准确的输出)
字符串和 []byte
%s 输出字符串
%q 源代码中那样带有双引号的输出
%x 每个字节用两字符十六进制数表示(使用a-f)
%X 每个字节用两字符十六进制数表示(使用A-F)
指针
%p 输出十六进制,并加上前导的 0x

10,Go 方法

Go 语言中的方法和面向对象中的方法并不是一样的。Go 引入方法这一元素,并不是要支持面向对象编程范式,而是 Go 践行组合设计哲学的一种实现层面的需要。

Go 语言中的方法的本质就是,一个以方法的 receiver 参数作为第一个参数的普通函数

1,Go 方法的定义

Go 方法的形式,比函数多了一个 receiver

在这里插入图片描述

receiver 参数也是方法与类型之间的纽带,也是方法与函数的最大不同。

Go 中的方法必须是归属于一个类型的,而 receiver 参数的类型就是这个方法归属的类型。

注意 ListenAndServeTLS 是 *Server 类型的方法,而不是 Server 类型的方法。

方法的一般声明形式:

func (t *T或T) MethodName(参数列表) (返回值列表) {
    // 方法体
}
  • 无论 receiver 参数的类型为 *T 还是 T,我们都将 T 叫做 t 的基类型
  • 如果 t 的类型为 T,那么说这个方法是类型 T 的一个方法
  • 如果 t 的类型为 *T,那么就说这个方法是类型 *T 的一个方法
  • receiver 参数的基类型本身不能为指针类型或接口类型
  • 每个方法只能有一个 receiver 参数
    • Go 不支持在方法的 receiver 部分放置包含多个 receiver 参数的参数列表,或者变长 receiver 参数
  • 方法声明要与 receiver 参数的基类型声明放在同一个包内
    • 我们无法为原生类型(诸如 int、float64、map 等)添加方法
    • 不能跨越 Go 包为其他包的类型声明新方法

Go 方法的调用形式:

type T struct{}

func (t T) M(n int) {
}

func main() {
    var t T
    t.M(1) // 通过类型T的变量实例调用方法M

    p := &T{}
    p.M(2) // 通过类型*T的变量实例调用方法M
}

我们可以将方法作为右值,赋值给一个函数类型的变量,比如下:

type T struct { 
    a int
}

func (t T) Get() int {  
    return t.a 
}

func (t *T) Set(a int) int { 
    t.a = a 
    return t.a 
}

func main() {
    var t T
    f1 := (*T).Set // f1的类型,也是*T类型Set方法的类型:func (t *T, int)int
    f2 := T.Get    // f2的类型,也是T类型Get方法的类型:func(t T)int
    
    fmt.Printf("the type of f1 is %T\n", f1) // the type of f1 is func(*main.T, int) int
    fmt.Printf("the type of f2 is %T\n", f2) // the type of f2 is func(main.T) int
    
    f1(&t, 3)
    fmt.Println(f2(t)) // 3
}

2,receiver 参数的类型问题

来看下面例子中的两个 Go 方法,以及它们等价转换后的函数:

func (t T) M1() <=> F1(t T)
func (t *T) M2() <=> F2(t *T)
  • M1 方法是 receiver 参数类型为 T 的一类方法的代表
    • T 类型实例的 receiver 参数以值传递方式传递到 M1 方法体中
    • 实际上是 T 类型实例的副本,M1 方法体中对副本的任何修改操作,都不会影响到原 T 类型实例
  • M2 方法是 receiver 参数类型为 *T 的另一类
    • *T 类型实例的 receiver 参数以值传递方式传递到 M2 方法体中
    • 实际上是 T 类型实例的地址,M2 方法体通过该地址可以对原 T 类型实例进行任何修改操作

一个示例:

package main
  
type T struct {
    a int
}

func (t T) M1() {
    t.a = 10
}

func (t *T) M2() {
    t.a = 11
}

func main() {
    var t T
    println(t.a) // 0

    t.M1()
    println(t.a) // 0

    p := &t
    p.M2()
    println(t.a) // 11
}

选择 receiver 参数类型的原则:

  1. 如果 Go 方法要把对 receiver 参数代表的类型实例的修改,反映到原类型实例上,那么我们应该选择 *T 作为 receiver 参数的类型。
  2. 如果我们不需要在方法中对类型实例进行修改呢?这个时候我们是为 receiver 参数选择 T 类型还是 *T 类型呢?
    1. 一般情况下,我们会为 receiver 参数选择 T 类型,这样可以减少外部修改类型实例内部状态的“渠道”
    2. 如果 receiver 参数类型的 size 较大,以值拷贝形式传入就会导致较大的性能开销,这时我们选择 *T 作为 receiver 类型可能更好些
  3. T 类型是否要实现某一接口
    1. 如果 T 类型需要实现某一接口的全部方法,那么我们就需要使用 T 作为 receiver 参数的类型来满足接口类型方法集合中的所有方法。
    2. 如果 T 类型不需要实现某一接口,那么我们就可以参考原则一和原则二来为 receiver 参数选择类型了。

无论是 T 类型实例,还是 *T 类型实例,都既可以调用 receiver 为 T 类型的方法,也可以调用 receiver 为 *T 类型的方法 。这都是 Go 编译器在背后做的转换,当 Go 发现类型不一致时,会自动转换。

示例如下:

  type T struct {
      a int
  }
  
  func (t T) M1() {
      t.a = 10
  }
 
 func (t *T) M2() {
     t.a = 11
 }
 
 func main() {
     var t1 T
     println(t1.a) // 0
     t1.M1()
     println(t1.a) // 0
     t1.M2()
     println(t1.a) // 11
 
     var t2 = &T{}
     println(t2.a) // 0
     t2.M1()
     println(t2.a) // 0
     t2.M2()
     println(t2.a) // 11
 }

3,一个思考题

第一个:

type field struct {
    name string
}

func (p *field) print() {
    fmt.Println(p.name)
}

func main() {
    data1 := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data1 {
        go v.print()
    }

    data2 := []field{{"four"}, {"five"}, {"six"}}
    for _, v := range data2 {
        go v.print()
    }

    time.Sleep(3 * time.Second)
}

// 其结果是
one
two
three
six
six
six

第二个:

type field struct {
    name string
}

func (p field) print() {
    fmt.Println(p.name)
}

func main() {
    data1 := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data1 {
        go v.print()
    }

    data2 := []field{{"four"}, {"five"}, {"six"}}
    for _, v := range data2 {
        go v.print()
    }

    time.Sleep(3 * time.Second)
}
// 其结果是
one
two
three
four
five
six

4,方法集合

Go 中任何一个类型都有属于自己的方法集合,或者说方法集合是 Go 类型的一个“属性”。

方法集合是用来判断一个类型是否实现了某接口类型的唯一手段。

但不是所有类型都有自己的方法,比如 int 类型就没有。所以,对于没有定义方法的 Go 类型,我们称其拥有空方法集合。

方法集合可以分两种来讨论:

  • 接口类型
  • 非接口类型

接口类型相对特殊,它只会列出代表接口的方法列表,不会具体定义某个方法,它的方法集合就是它的方法列表中的所有方法。

函数 dumpMethodSet,用于输出一个非接口类型的方法集合:

func dumpMethodSet(i interface{}) {
    dynTyp := reflect.TypeOf(i)

    if dynTyp == nil {
        fmt.Printf("there is no dynamic type\n")
        return
    }

    n := dynTyp.NumMethod()
    if n == 0 {
        fmt.Printf("%s's method set is empty!\n", dynTyp)
        return
    }

    fmt.Printf("%s's method set:\n", dynTyp)
    for j := 0; j < n; j++ {
        fmt.Println("-", dynTyp.Method(j).Name)
    }
    fmt.Printf("\n")
}

再看下面代码:

type T struct{}

func (T) M1() {}
func (T) M2() {}

func (*T) M3() {}
func (*T) M4() {}

func main() {
    var n int
    dumpMethodSet(n)
    dumpMethodSet(&n)

    var t T
    dumpMethodSet(t)
    dumpMethodSet(&t)
}

输出如下:

int's method set is empty!
*int's method set is empty!
main.T's method set:
- M1
- M2

*main.T's method set:
- M1  // 初学者不容易理解的地方
- M2  // 初学者不容易理解的地方
- M3
- M4

Go 语言规定,*T 类型的方法集合包含所有以 *T 为 receiver 参数类型的方法,以及所有以 T 为 receiver 参数类型的方法。

方法集合决定接口实现的含义就是:如果某类型 T 的方法集合与某接口类型的方法集合相同,或者类型 T 的方法集合是接口类型 I 方法集合的超集,那么我们就说这个类型 T 实现了接口 I。或者说,方法集合这个概念在 Go 语言中的主要用途,就是用来判断某个类型是否实现了某个接口

... method has pointer receiver 问题

看代码:

type QuackableAnimal interface {
	Quack()
}

type Duck struct{}

// d 的类型是 Duck
func (d Duck) Quack() {
}

func AnimalQuackInForest(a QuackableAnimal) {
}

func main() {
	AnimalQuackInForest(Duck{})	    // 传  T 类型没问题
	AnimalQuackInForest(&Duck{})        // 传 *T 类型没问题
}

查看方法集:

dumpMethodSet(Duck{})
dumpMethodSet(&Duck{})

// 输出如下
main.Duck's method set:
- Quack
*main.Duck's method set:
- Quack

再看代码:

type QuackableAnimal interface {
  Quack()
}

type Duck struct{}

// d 的类型是 *Duck
func (d *Duck) Quack() {
}

func AnimalQuackInForest(a QuackableAnimal) {
}

func main() {
  AnimalQuackInForest(Duck{})	// 传  T 类型有问题,编译异常:Quack method has pointer receiver
  AnimalQuackInForest(&Duck{})	// 传 *T 类型没问题
}

此时 AnimalQuackInForest(Duck{}) 出问题的原因是,Duck 类型没有实现 QuackableAnimal 接口。

通过 dumpMethodSet(Duck{}) 查看 Duck 的方法集,可知 Duck 的方法集为空。

dumpMethodSet(Duck{})
dumpMethodSet(&Duck{})

// 输出如下
main.Duck's method set is empty!
*main.Duck's method set:
- Quack

11,Go 中的“继承”:类型嵌入

类型嵌入指的就是在一个类型的定义中嵌入了其他类型。Go 语言支持两种类型嵌入:

  • 接口类型的类型嵌入
  • 结构体类型的类型嵌入

接口类型只能嵌入接口类型,而结构体类型对嵌入类型的要求就比较宽泛了,可以是任意自定义类型或接口类型。

1,接口类型的类型嵌入

接口类型声明了由一个方法集合代表的接口,比如下面接口类型 E:

type E interface {
    M1()
    M2()
}

如果某个类型实现了方法 M1 和 M2,我们就说这个类型实现了 E 所代表的接口。

再定义另外一个接口类型 I,它的方法集合中包含了三个方法 M1、M2 和 M3,如下面代码:

type I interface {
    M1()
    M2()
    M3()
}

我们看到接口类型 I 方法集合中的 M1 和 M2,与接口类型 E 的方法集合中的方法完全相同。在这种情况下,我们可以用接口类型 E 替代上面接口类型 I 定义中 M1 和 M2,如下面代码:

type I interface {
    E
    M3()
}

像这种在一个接口类型(I)定义中,嵌入另外一个接口类型(E)的方式,就是我们说的接口类型的类型嵌入

而且,这个带有类型嵌入的接口类型 I 的定义与上面那个包含 M1、M2 和 M3 的接口类型 I 的定义,是等价的。

通过在接口类型中嵌入其他接口类型可以实现接口的组合,这也是 Go 语言中基于已有接口类型构建新接口类型的惯用法。

接口类型的类型嵌入比较简单,我们只要把握好它的语义,也就是“方法集合并入”就可以了。

2,结构体类型的类型嵌入

常规的结构体类型:

type S struct {
    A int
    b string
    c T
    p *P
    _ [10]int8
    F func()
}

结构体类型 S 中的每个字段(field)都有唯一的名字与对应的类型,即便是使用空标识符占位的字段,它的类型也是明确的。

带有嵌入字段的结构体定义,看下面代码中的 S1

type T1 int
type t2 struct{
    n int
    m int
}

type I interface {
    M1()
}

type S1 struct {
    T1
    *t2
    I            
    a int
    b string
}
  • 标识符 T1 表示字段名为 T1,它的类型为自定义类型 T1;
  • 标识符 t2 表示字段名为 t2,它的类型为自定义结构体类型 t2 的指针类型;
  • 标识符 I 表示字段名为 I,它的类型为接口类型 I。
  • 如果嵌入类型的名字是首字母大写的,那么也就说明这个嵌入字段是可导出的。
  • 如果结构体使用从其他包导入的类型作为嵌入字段,比如 pkg.T,那么这个嵌入字段的字段名就是 T,代表的类型为 pkg.T。

这种以某个类型名、类型的指针类型名或接口类型名,直接作为结构体字段的方式就叫做结构体的类型嵌入,这些字段也被叫做嵌入字段

嵌入字段的使用的确可以帮我们在 Go 中实现方法的“继承”。

类型嵌入这种看似“继承”的机制,实际上是一种组合的思想。更具体点,它是一种组合中的代理(delegate)模式,如下图所示:

在这里插入图片描述

结构体类型的方法集合,包含嵌入的接口类型的方法集合。 也就是说,当结构体类型中嵌入了接口类型时,接口类型中的方法都会并入到结构体类型中。

在结构体类型中嵌入结构体类型,为 Gopher 们提供了一种“实现继承”的手段,外部的结构体类型 T 可以“继承”嵌入的结构体类型的所有方法的实现。

当通过结构体类型 S 的变量 s 调用 Read 方法时,Go 发现结构体类型 S 自身并没有定义 Read 方法,于是 Go 会查看 S 的嵌入字段对应的类型是否定义了 Read 方法。这个时候,Reader 字段就被找了出来,之后 s.Read 的调用就被转换为 s.Reader.Read 调用。

这样一来,嵌入字段 Reader 的 Read 方法就被提升为 S 的方法,放入了类型 S 的方法集合。

当结构体嵌入的多个接口类型的方法集合存在交集时,Go 编译器会报错。

嵌入了其他类型的结构体类型本身是一个代理,在调用其实例所代理的方法时,Go 会首先查看结构体自身是否实现了该方法。

  • 如果实现了,Go 就会优先使用结构体自己实现的方法。
  • 如果没有实现,那么 Go 就会查找结构体中的嵌入字段的方法集合中,是否包含了这个方法。
    • 如果多个嵌入字段的方法集合中都包含这个方法,那么我们就说方法集合存在交集。
    • 这个时候,Go 编译器就会因无法确定究竟使用哪个方法而报错。(有点类似 C++ 多重继承出现的问题)

一个示例:

  type E1 interface {
      M1()
      M2()
      M3()
  }
  
  type E2 interface {
     M1()
     M2()
     M4()
 }
 
 type T struct {
     E1
     E2
 }
 
 func main() {
     t := T{}
     t.M1()
     t.M2()
 }

输出如下:

main.go:22:3: ambiguous selector t.M1
main.go:23:3: ambiguous selector t.M2

这个问题有两种解决办法

  • 一是,我们可以消除 E1 和 E2 方法集合存在交集的情况。
  • 二是为 T 增加 M1 和 M2 方法的实现,这样的话,编译器便会直接选择 T 自己实现的 M1 和 M2,不会陷入两难境地。

如下:

... ...
type T struct {
    E1
    E2
}

func (T) M1() { println("T's M1") }
func (T) M2() { println("T's M2") }

func main() {
    t := T{}
    t.M1() // T's M1
    t.M2() // T's M2
}

3,type 定义新类型时的方法集合

分两种情况:

  • type NewT OldT:定义新的类型
    • 如果 OldT 是接口类型,NewT 的方法集合与 OldT 的方法集合是一致的
    • 如果 OldT 是非接口类型,那么 NewT 不会继承 OldT 的任意一个方法,NewT 的方法集合是空集合
  • type NewT = OldT:类型别名
    • 无论 OldT 是接口类型还是非接口类型,NewT 都与 OldT 拥有完全相同的方法集合
    • 实际上 NewT 与 OldT 是完全等价的

4,一个思考题

下面带有类型嵌入的结构体 S1 与不带类型嵌入的结构体 S2 是否是等价的,如不等价,区别在哪里?

type T1 int
type t2 struct{
    n int
    m int
}

type I interface {
    M1()
}

type S1 struct {
    T1
    *t2
    I
    a int
    b string
}

type S2 struct { 
    T1 T1
    t2 *t2
    I  I
    a  int
    b  string
}
  • S1 是类型嵌入的,拥有“继承”能力
  • S2 并不是类型嵌入的,只是普通的结构体组合,所以没有对应接口的方法集合(并不存在“继承”的能力)

12,Go 接口类型

接口本质上是一种抽象,它的功能是解耦。尽管接口不是 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 {
    M1()
}

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 {
    n int
    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 {
    n int
    s string
}

func (T) M1() {}
func (T) M2() {}

type NonEmptyInterface interface {
    M1()
    M2()
}

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)
    }
}