Go语言接口

2021/06/05

Tags: Go

摘要:Go支持接口,接口是方法特征的命名集合。


go语言接口

go语言中有接口的概念,接口是方法特征的命名集合。它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。

注意,实现了这些方法就算实现了这个接口。

定义接口

// 定义geometry接口
type geometry interface {
    area() float64
    peri() float64
}

接口的定义也比较简单。定义和实现规则如下:

/* 定义接口 */
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] {
   /* 方法实现*/
}

实现接口

Go语言中接口的实现都是隐式的,默认实现了接口的所有方法就隐式地实现了接口。

假如现已定义上文中的geometry接口,现在要实现该接口。

// 定义rect结构体
type rect struct {
    width, height float64
}

// rect 实现geometry接口,实现接口的方式与定义方法类似
func (r rect) area() float64 {
    return r.width * r.height
}

// rect 实现geometry接口,只有实现了接口的所有方法才算实现接口
func (r rect) peri() float64 {
    return r.height*2 + r.width*2
}

注意:只有实现了接口的所有方法才算实现接口。

调用方式:

var geo1 geometry = rect{height: 1, width: 3}
fmt.Println("geo1.area()", geo1.area())
fmt.Println("geo1.peri()", geo1.peri())

接口也可以作为函数参数,传参传入具体的实现。

// 定义调用接口方法的函数
func measure(g geometry) {
    fmt.Println(g.peri())
    fmt.Println(g.area())
}

// --- main()
rect1 := rect{height: 3, width: 2}
measure(rect1)

接口类型

接口也是Go语言中的一种类型,这种类型可以出现在变量的定义、函数的入参、函数的返回值中。

// define Print
func Print(i interface{}) {
    fmt.Println(i)
}

v1 := 22
v2 := "ss"
v3 := make([]string, 1)
v3 = append(v3, "333")

Print(v1)
Print(v2)
Print(v3)

// 结果
// 22
// ss
// [ 333]

Print()函数中,参数为interface{}类型,但并不是任意类型,在调用函数时,将参数转换成了interface{}类型,这里是类型转换。

接口和指针

看以下两个例子:

  1. 通过结构体实现接口
// 定义rect结构体
type rect struct {
    width, height float64
}

// rect 实现geometry接口,实现接口的方式与定义方法类似
func (r rect) area() float64 {
    return r.width * r.height
}

// rect 实现geometry接口,只有实现了接口的所有方法才算实现接口
func (r rect) peri() float64 {
    return r.height*2 + r.width*2
}
  1. 通过结构体指针实现接口
// 定义rect结构体
type rect struct {
    width, height float64
}

// rect 实现geometry接口,实现接口的方式与定义方法类似
func (r *rect) area() float64 {
    return r.width * r.height
}

// rect 实现geometry接口,只有实现了接口的所有方法才算实现接口
func (r *rect) peri() float64 {
    return r.height*2 + r.width*2
}

在Go语言中,结构体实现接口有两种方式,通过结构体实现、通过结构体指针实现。但两种方式不能同时存在,这两种实现方式也会导致接口的调用有些差别。

var geo1 geometry = rect{height: 1, width: 3} // 使用结构体初始化变量
var geo1 geometry = &rect{height: 1, width: 3} // 使用结构体指针初始化变量
结构体实现接口 结构体指针实现接口
结构体指针初始化变量 通过 通过
结构体初始化变量 通过 不通过

当使用结构体实现接口时,无论初始化变量是结构体还是结构体指针,都可以编译通过;当使用结构体指针实现接口时,初始化变量为结构体时无法编译通过。

为什么使用结构体指针实现接口时,初始化变量为结构体时无法编译通过?

原因是在函数调用阶段,Go语言在传递参数时都是值传递。无论初始变量是rect{}还是&rect{},在调用方法时都会发生值拷贝。

当我们使用指针实现接口时,只有指针类型的变量才会实现该接口;当我们使用结构体实现接口时,指针类型和结构体类型都会实现该接口。当然这并不意味着我们应该一律使用结构体实现接口,这个问题在实际工程中也没那么重要。

再谈接口类型

了解了接口参数的隐式转换后,再看上文提到的,接口不是任何一种类型,看以下示例:

type TestStruct struct{}

func NilOrNot(v interface{}) bool {
    return v == nil
}

func main() {
    var s *TestStruct
    fmt.Println(s == nil)      //  true
    fmt.Println(NilOrNot(s))   //  false
}

调用NilOrNot函数时,发生了隐式类型转换,除了向方法传入参数之外,变量的赋值也会触发隐式类型转换,在转换过程中,*TestStruct类型会转换为interface{}类型,转换后的变量不仅包含转换前的变量,还包含变量的类型信息,所以转换后的变量与 nil 不相等。