关于 Golang 接口的一些总结

在使用 Golang 构建后端服务时,接口是常用的强大工具。下面是我个人在使用 Golang 接口进行编程的一些总结。

基本定义和用法

在 Go 语言中,接口类型是函数签名的集合,也就是方法的集合。定义一个接口就表示定义了一堆方法:

// define an interface and its abstract methods
type I interface {
  PrintVal()
  setVal(float64)
}

接口所定义的只是函数的签名(抽象方法),函数的具体实现肯定要由其他具体的类型来实现。和 Java 中的接口一样,实现接口的类型必须要实现接口中的所有方法:

// type Number implements I's methods
type Number struct {
  val float64
}
func (num *Number) PrintVal() {
  fmt.Printf("%f\n", num.val)
}
func (num *Number) setVal(newNum float64) {
  num.val = newNum
}

// build-in type float64 implements I's methods
type F float64
func (num F) PrintVal() {
    fmt.Printf("%f\n", num)
}
func (num F) setVal(newNum float64) {
    num = F(newNum)
}

当一个类型实现了接口中的方法时,就可以把这个类型的实例赋值给该接口类型的变量:

func main() {
  // i is type of interface I
  var i I

  i = &Number{72.6}
  i.PrintVal()
  i.setVal(89.6)
  i.PrintVal()

  num := F(12.8)
  i = num
  i.PrintVal()
  i.setVal(16.9)
  i.PrintVal()
}

output:
72.600000
89.600000
12.800000
12.800000

对以上代码的相关解释如下:

  • NumberF 类型都实现了接口 I 的两个方法,因此这两个类型的实例都可以赋值给接口 I 的变量 i ,并通过 i 来调用两个类型的方法,由此可见,Go 语言利用接口实现了像泛型这样的概念。

  • 由于 F 类型实现 setVal 方法时传递的不是指针类型 *F ,因此调用 setVal 方法时并不能改变变量 num 的值;而 Number 类型实现 setVal 方法时传递的是指针类型 *Number ,因此可以改变 num.val 的值。

  • 要想使用内建类型(如图中的 float64)去实现某个接口的方法,必须像上面的代码所示把内建类型定义为另外一种类型 type F float64 ,然后使用类型 F 去实现接口的方法。如果直接用内建类型去实现接口的方法将会报错:

    func (num *float64) PrintVal() {
    fmt.Printf("%f\n", num)
    }
    func (num *float64) setVal(newNum float64) {
    *num = newNum
    }
    
    output:
    cannot define new methods on non-local type float64
    cannot define new methods on non-local type float64

如果一个类型实现了接口定义的抽象方法之外的方法,则在用接口的变量指向该类型的实例是不能调用这个额外的方法,比如 Number 类型还定义了一个在接口中没有定义的方法addOne ,此时在 main 函数中调用 i.addOne() 时将会报错:

func (num *Number) addOne() {
  num.val += 1
}

func main() {
  ...
  i.addOne()
  ...
}

output:
i.addOne undefined (type I has no field or method addOne)

假设把 F 类型实现的 setVal 函数改为传递指针类型,而 PrintVal 函数仍然是传递值类型:

func (num F) PrintVal() {
    fmt.Printf("%f\n", num)
}
func (num *F) setVal(newNum float64) {
    *num = F(newNum)
}

此时在 main 函数中执行如下代码将报错:

num := F(12.8)
// num is of type F
i = num
i.PrintVal()
i.setVal(16.9)
i.PrintVal()

output:
cannot use num (type F) as type I in assignment:
F does not implement I (setVal method has pointer receiver)

表示 F 类型并没有实现接口 IsetVal 函数,因为 setVal 函数接收的是指针类型 *F 。然而有趣的是,当把 num 的地址传递给 i 时,程序却能够正常运行:

num := F(12.8)
// num is of type *F
i = &num
i.PrintVal()
i.setVal(16.9)
i.PrintVal()

output:
12.800000
16.900000

*F 类型应该没有实现接口 IPrintVal 函数,因为 PrintVal 函数接收的是值类型 F 。但程序的正确运行说明 func (num F) PrintVal() 的实现包含了 func (num *F) PrintVal() 的实现。而从上面报错的程序来看,func (num *F) setVal(newNum float64) 却不包含 func (num F) setVal(newNum float64) 的实现。由此我们可以得出这样的结论:当一个类型的值类型实现了接口的某个方法时,这个类型的指针类型也实现了这个方法;但当一个类型的指针类型实现了接口的某个方法时,并不意味着这个类型的值类型也实现了这个方法!

但是这种问题只是在实现接口的方法时出现,如果不是实现接口的方法,则在调用方法时指针类型和值类型是可以相互进行隐式转换的:

func (num Number) addOne() {
    num.val += 1
}
num := &Number{72.6}
num.addOne()               // 等价于 (*num).addOne(),只是执行后 num.val 的值不会改变,仍为 72.6

func (num *Number) addOne() {
    num.val += 1
}
num := Number{72.6}
num.addOne()               // 等价于 (&num).addOne(),只是执行后 num.val 的值改变了,变为 73.6

接口的值

在 Go 语言中,一个接口的值可以视为一个元组 (value, type) ,其中 type 表示是哪个类型的实例赋值给了这个接口,而 value 则是这个类型的值。紧接着第一部分中接口 I 和类型 NumberF 的定义,运行如下代码:

var i I
fmt.Printf("%v, %T\n", i, i)

i = &Number{72.6}
fmt.Printf("%v, %T\n", i, i)

num := F(12.8)
i = num
fmt.Printf("%v, %T\n", i, i)

output:
<nil>, <nil>
&{72.6}, *main.Number
12.8, main.F

从输出结果中可以看到,i 的类型不是 main.I !当 i 没有被赋值时,i 的类型为 nil ,而当 i 被赋值后,i 的类型就变成了赋值给 i 的那个实例的类型,而 i 值则是对应实例的值。这个特性使得 Go 语言的接口使用起来非常灵活。

接口用于函数参数

接口也可以用作函数参数,比如定义一个函数 func display(i I) ,则这个函数可以既可以接收 *Number 类型的参数也可以接收 F 类型的参数:

j := &Number{15.6}
display(j)

k := F(17.1)
display(k)

output:
15.600000
17.100000

判断接口类型

当函数接收一个接口时,由于该接口可以由不同的类型来实现,有时候需要判断这个接口属于哪种类型再进行下一步的操作,判断接口类型可以用以下两种方法:

  • 使用类型断言 t, ok = i.(T) 。其中,i 是接口,T 是某个类型。如果接口 i 的值 <value, type> 中的 type == T ,则返回 ok == true ,并且 t 将会变成类型 T 的一个实例,而不再是一个接口类型!这个实例的值与接口 ivalue 相同;如果接口 i 的值 <value, type> 中的 type != T ,则返回 ok == false ,但是 t 仍然会变成类型 T 的一个实例 !只是 t 的值为 nil

  • 使用类型判断 switch t := i.(type)

    switch t := i.(type) {
    case T:
        // t is type of T here
    case S:
        // t is type of S here
    default:
        // no match; t's type and value is the same as i
    }

    分析上述代码,类型判断语句中的 case 语句后面不再是值,而是不同的类型 TS 。当 t 的类型是 T 时,在 case T 语句后面 t 就变成了一个类型为 T实例 ,而不再是一个接口类型!这个实例的值与接口 ivalue 相同,当 t 的类型为 S 时,也同理,t 变成了类型 S 的一个实例。当没有匹配时,进入 default 语句,此时 t 仍然是和 i 相同的一个接口类型,其 typevalue 都与 itypevalue 相同。

下面再用代码说明进行类型断言或判断后相应类型和值的改变:

func display1(i I) {
    switch t := i.(type) {
    case *Number:
        fmt.Printf("%f\n", t.val)  // 可以直接使用 t 来获取 *Number 类型的字段 val 以及调用接口 I
        t.addOne()                 // 没有定义的函数 addOne,这充分说明了 t 已经不再是一个接口,
        fmt.Printf("%f\n", t.val)  // 而是 *Number 类型的一个实例!
    default:
        fmt.Printf("%v, %T\n", t, t)
    }
}

func display2(i I) {
    t, ok := i.(*Number)
    if ok {
        fmt.Printf("%f\n", t.val)  // 同理,t 已经变成 *Number 类型的一个实例。
        t.addOne()
        fmt.Printf("%f\n", t.val)
    } else {
        fmt.Printf("%v, %T\n", t, t)
    }
}

func main() {
    var i I
    i = &Number{72.6}
    display1(i)
    display2(i)

    var j I
    display1(j)
    display2(j)
}

output:
72.600000
73.600000
73.600000
74.600000
<nil>, <nil>          // switch type 没有匹配时,t 的类型和值仍然与 j 的类型和值相同,为空接口
<nil>, *main.Number   // 但是在使用类型断言时,如果没有匹配,t 会变成 *Number 的一个实例,值为空。

空接口

在第一部分中已经可以看到了,当 Number 类型定义了一个在接口 I 中没有定义的函数 addOne 时,接口变量 i 不能调用这个 addOne 方法。对于空接口 interface{} 来说,它没有定义任何的抽象方法。这就意味着,任何一个类型,无论有什么方法,都可以实现空接口;换言之,任何类型的实例都可以赋值给空接口类型的变量。但是,在一个类型的实例赋值给一个空接口的变量 i 后,i 将不能调用这个类型的任何方法!必须要进行类型断言或者类型判断后,才能把空接口 i 变成相应类型的实例,从而调用这个类型的方法或访问其字段!

var k interface{}
k = "hello"
fmt.Printf("%v, %T\n", k, k)

output:
hello, string

参考资料

  1. 《学习 Go 语言》Miek Gieben, 邢星译
  2. A Tour of Go
阅读更多

没有更多推荐了,返回首页