关于 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
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Golang是一种编程语言,具备强大的接口特性。在Golang中,接口是一种规定了对象必须遵循的行为规范。通过接口,我们可以定义一组方法的集合,任何实现了这些方法的类型都被视为实现了该接口接口的实现是指一个类型声明自己遵循了某个接口。实现一个接口的类型,需要实现接口中定义的所有方法。实现接口的类型不需要显式声明实现了该接口,只要实现了接口中定义的方法即可。这种方式称为隐式接口实现。 接口对象是指一个接口类型的变量或参数,可以指向满足该接口的任何具体类型的对象。通过接口对象,我们可以调用接口中定义的方法,而不需要关心具体对象是哪种类型。 使用接口的好处是可以将接口作为一个统一的契约,使得不同类型的对象可以通过实现相同的接口来实现类型的替换和灵活的设计。接口可以实现代码的重用,并且松耦合,提高代码的可维护性和可测试性。 在实际应用中,我们可以先定义接口,然后根据接口的行为规范来编写具体类型的实现。接口对象可以根据需要替换为不同的具体类型对象,实现了面向接口编程的思想。 总结来说,Golang中的接口实现是指类型声明自己遵循了某个接口并实现了接口中定义的方法。接口对象是指可以指向满足该接口的任何具体类型的对象。通过接口,我们可以实现代码的灵活和重用,使得不同类型的对象可以通过实现相同的接口来进行类型的替换。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值