掘金 后端 ( ) • 2022-11-29 14:27

Golang interface 接口详细原理和使用技巧

一、Go interface 介绍

interface 在 Go 中的重要性说明

interface 接口在 Go 语言里面的地位非常重要,是一个非常重要的数据结构,只要是实际业务编程,并且想要写出优雅的代码,那么必然要用上 interface,因此 interface 在 Go 语言里面处于非常核心的地位。

我们都知道,Go 语言和典型的面向对象的语言不太一样,Go 在语法上是不支持面向对象的类、继承等相关概念的。但是,并不代表 Go 里面不能实现面向对象的一些行为比如继承、多态,在 Go 里面,通过 interface 完全可以实现诸如 C++ 里面的继承 和 多态的语法效果。

interface 的特性

Go 中的 interface 接口有如下特性:

  • 关于接口的定义和签名

    • 接口是一个或多个方法签名的集合,接口只有方法声明,没有实现,没有数据字段,只要某个类型拥有该接口的所有方法签名,那么就相当于实现了该接口,无需显示声明了哪个接口,这称为 Structural Typing。
    • interface 接口可以匿名嵌入其他接口中,或嵌入到 struct 结构中
    • 接口可以支持匿名字段方法
  • 关于接口赋值

    • 只有当接口存储的类型和对象都为 nil 时,接口才等于 nil
    • 一个空的接口可以作为任何类型数据的容器
    • 如果两个接口都拥有相同的方法,那么它们就是等同的,任何实现了他们这个接口的对象之间,都可以相互赋值
    • 如果某个 struct 对象实现了某个接口的所有方法,那么可以直接将这个 struct 的实例对象直接赋值给这个接口类型的变量。
  • 关于接口嵌套,Go 里面支持接口嵌套,但是不支持递归嵌套

  • 通过接口可以实现面向对象编程中的多态的效果

interface 接口和 reflect 反射

在 Go 的实现里面,每个 interface 接口变量都有一个对应 pair,这个 pair 中记录了接口的实际变量的类型和值(value, type),其中,value 是实际变量值,type 是实际变量的类型。任何一个 interface{} 类型的变量都包含了2个指针,一个指针指向值的类型,对应 pair 中的 type,这个 type 类型包括静态的类型 (static type,比如 int、string...)和具体的类型(concrete type,interface 所指向的具体类型),另外一个指针指向实际的值,对应 pair 中的 value。

interface 及其 pair 的存在,是 Go 语言中实现 reflect 反射的前提,理解了 pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair 对的一种机制。

二、Go 里面为啥偏向使用 Interface

Go 里面为啥偏向使用 Interface 呢? 主要原因有如下几点:

可以实现泛型编程(虽然 Go 在 1.18 之后已经支持泛型了)

在 C++ 等高级语言中使用泛型编程非常的简单,但是 Go 在 1.18 版本之前,是不支持泛型的,而通过 Go 的接口,可以实现类似的泛型编程,如下是一个参考示例

    package sort

    // A type, typically a collection, that satisfies sort.Interface can be
    // sorted by the routines in this package.  The methods require that the
    // elements of the collection be enumerated by an integer index.
    type Interface interface {
        // Len is the number of elements in the collection.
        Len() int
        // Less reports whether the element with
        // index i should sort before the element with index j.
        Less(i, j int) bool
        // Swap swaps the elements with indexes i and j.
        Swap(i, j int)
    }
    
    ...
    
    // Sort sorts data.
    // It makes one call to data.Len to determine n, and O(n*log(n)) calls to
    // data.Less and data.Swap. The sort is not guaranteed to be stable.
    func Sort(data Interface) {
        // Switch to heapsort if depth of 2*ceil(lg(n+1)) is reached.
        n := data.Len()
        maxDepth := 0
        for i := n; i > 0; i >>= 1 {
            maxDepth++
        }
        maxDepth *= 2
        quickSort(data, 0, n, maxDepth)
    }
    

Sort 函数的形参是一个 interface,包含了三个方法:Len(),Less(i,j int),Swap(i, j int)。使用的时候不管数组的元素类型是什么类型(int, float, string…),只要我们实现了这三个方法就可以使用 Sort 函数,这样就实现了“泛型编程”。

这种方式,我在闪聊项目里面也有实际应用过,具体案例就是对消息排序。 下面给一个具体示例,代码能够说明一切,一看就懂:

    type Person struct {
    Name string
    Age  int
    }
    
    func (p Person) String() string {
        return fmt.Sprintf("%s: %d", p.Name, p.Age)
    }
    
    // ByAge implements sort.Interface for []Person based on
    // the Age field.
    type ByAge []Person //自定义
    
    func (a ByAge) Len() int           { return len(a) }
    func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
    func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
    
    func main() {
        people := []Person{
            {"Bob", 31},
            {"John", 42},
            {"Michael", 17},
            {"Jenny", 26},
        }
    
        fmt.Println(people)
        sort.Sort(ByAge(people))
        fmt.Println(people)
    }

可以隐藏具体的实现

隐藏具体的实现,是说我们提供给外部的一个方法(函数),但是我们是通过 interface 接口的方式提供的,对调用方来说,只能通过 interface 里面的方法来做一些操作,但是内部的具体实现是完全不知道的。

例如我们常用的 context 包,就是这样设计的,如果熟悉 Context 具体实现的就会很容易理解。详细代码如下:

    func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
        c := newCancelCtx(parent)
        propagateCancel(parent, &c)
        return &c, func() { c.cancel(true, Canceled) }
    }
    

可以看到 WithCancel 函数返回的还是一个 Context interface,但是这个 interface 的具体实现是 cancelCtx struct。

        // newCancelCtx returns an initialized cancelCtx.
        func newCancelCtx(parent Context) cancelCtx {
            return cancelCtx{
                Context: parent,
                done:    make(chan struct{}),
            }
        }
        
        // A cancelCtx can be canceled. When canceled, it also cancels any children
        // that implement canceler.
        type cancelCtx struct {
            Context     //注意一下这个地方
        
            done chan struct{} // closed by the first cancel call.
            mu       sync.Mutex
            children map[canceler]struct{} // set to nil by the first cancel call
            err      error                 // set to non-nil by the first cancel call
        }
        
        func (c *cancelCtx) Done() <-chan struct{} {
            return c.done
        }
        
        func (c *cancelCtx) Err() error {
            c.mu.Lock()
            defer c.mu.Unlock()
            return c.err
        }
        
        func (c *cancelCtx) String() string {
            return fmt.Sprintf("%v.WithCancel", c.Context)
        }

尽管内部实现上下面三个函数返回的具体 struct (都实现了 Context interface)不同,但是对于使用者来说是完全无感知的。

    func WithCancel(parent Context) (ctx Context, cancel CancelFunc)    //返回 cancelCtx
    func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) //返回 timerCtx
    func WithValue(parent Context, key, val interface{}) Context    //返回 valueCtx

可以实现面向对象编程中的多态用法

interface 只是定义一个或一组方法函数,但是这些方法只有函数签名,没有具体的实现,这个 C++ 中的虚函数非常类似。在 Go 里面,如果某个数据类型实现 interface 中定义的那些函数,则称这些数据类型实现(implement)了这个接口 interface,这是我们常用的 OO 方式,如下是一个简单的示例

    // 定义一个 SimpleLog 接口
    type SimpleLog interface {
        Print()
    }
    
    func TestFunc(x SimpleLog) {}
   
    // 定义一个 PrintImpl 结构,用来实现 SimpleLog 接口
    type PrintImpl struct {}
    // PrintImpl 对象实现了SimpleLog 接口的所有方法(本例中是 Print 方法),就说明实现了  SimpleLog 接口
    func (p *PrintImpl) Print() {
    
    }
    
    func main() {
        var p PrintImpl
        TestFunc(p)
    }

空接口可以接受任何类型的参数

空接口比较特殊,它不包含任何方法:interface{} ,在 Go 语言中,所有其它数据类型都实现了空接口,如下:

var v1 interface{} = 1
var v2 interface{} = "abc"
var v3 interface{} = struct{ X int }{1}

因此,当我们给 func 定义了一个 interface{} 类型的参数(也就是一个空接口)之后,那么这个参数可以接受任何类型,官方包中最典型的例子就是标准库 fmt 包中的 Print 和 Fprint 系列的函数。

一个简单的定义示例方法如下:

Persist(context context.Context, msg interface{}) bool

msg 可以为任何类型,如 pb.MsgInfo or pb.GroupMsgInfo,定义方法的时候可以统一命名模块,实现的时候,根据不同场景实现不同方法。

三、Go interface 的常见应用和实战技巧

interface 接口赋值

可以将一个实现接口的对象实例赋值给接口,也可以将另外一个接口赋值给接口。

通过对象实例赋值

将一个对象实例赋值给一个接口之前,要保证该对象实现了接口的所有方法。在 Go 语言中,一个类只需要实现了接口要求的所有函数,我们就说这个类实现了该接口,这个是非侵入式接口的设计模式,非侵入式接口一个很重要的优势就是可以免去面向对象里面那套比较复杂的类的继承体系。

在 Go 里面,面向对象的那套类的继承体系就不需要关心了,定义接口的时候,我们只需关心这个接口应该提供哪些方法,当然,按照 Go 的原则,接口的功能要尽可能的保证职责单一。而对应接口的实现,也就是接口的调用方,我们只需要知道这个接口定义了哪些方法,然后我们实现这些方法就可以了,这个也无需提前规划,调用方也无需关系是否有其他模块定义过类似的接口或者实现,只关注自身就行。

考虑如下示例:

type Integer int
func (a Integer) Less(b Integer) bool {
    return a < b
}
func (a *Integer) Add(b Integer) {
    *a += b
}
type LessAdder interface { 
    Less(b Integer) bool 
    Add(b Integer)
}
var a Integer = 1
var b1 LessAdder = &a  //OK
var b2 LessAdder = a   //not OK

b2 的赋值会报编译错误,为什么呢?因为这个:The method set of any other named type T consists of all methods with receiver type T. The method set of the corresponding pointer type T is the set of all methods with receiver T or T (that is, it also contains the method set of T). 也就是说 *Integer 这个指针类型实现了接口 LessAdder 的所有方法,而 Integer 只实现了 Less 方法,所以不能赋值。

通过接口赋值

        var r io.Reader = new(os.File)
        var rw io.ReadWriter = r   //not ok
        var rw2 io.ReadWriter = new(os.File)
        var r2 io.Reader = rw2    //ok

因为 r 没有 Write 方法,所以不能赋值给rw。

interface 接口嵌套

io package 中的一个接口:

// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
    Reader
    Writer
}

ReadWriter 接口嵌套了 io.Reader 和 io.Writer 两个接口,实际上,它等同于下面的写法:

type ReadWriter interface {
    Read(p []byte) (n int, err error) 
    Write(p []byte) (n int, err error)
}

注意,Go 语言中的接口不能递归嵌套,如下:

// illegal: Bad cannot embed itself
type Bad interface {
    Bad
}
// illegal: Bad1 cannot embed itself using Bad2
type Bad1 interface {
    Bad2
}
type Bad2 interface {
    Bad1
}

interface 强制类型转换

ret, ok := interface.(type) 断言

在 Go 语言中,可以通过 interface.(type) 的方式来对一个 interface 进行强制类型转换,但是如果这个 interface 被转换为一个不包含指定类型的类型,那么就会出现 panic 。因此,实战应用中,我们通常都是通过 ret, ok := interface.(type) 这种断言的方式来优雅的进行转换,这个方法中第一个返回值是对应类型的值,第二个返回值是类型是否正确,只有 ok = true 的情况下,才说明转换成功,最重要的是,通过这样的转换方式可以避免直接转换如果类型不对的情况下产生 panic。

如下是一个以 string 为类型的示例:

str, ok := value.(string)
if ok {
    fmt.Printf("string value is: %q\n", str)
} else {
    fmt.Printf("value is not a string\n")
}

如果类型断言失败,则str将依然存在,并且类型为字符串,不过其为零值,即一个空字符串。

switch x.(type) 断言

查询接口类型的方式为:

switch x.(type) {
    // cases :
}

示例如下:

var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
    return str //type of str is string
case int: 
    return int //type of str is int
}
语句switch中的value必须是接口类型,变量str的类型为转换后的类型。

interface 与 nil 的比较

interface 与 nil 的比较是挺有意思的,例子是最好的说明,如下例子:

package main

import (
"fmt"
"reflect"
)

type State struct{}

func testnil1(a, b interface{}) bool {
return a == b
}

func testnil2(a *State, b interface{}) bool {
return a == b
}

func testnil3(a interface{}) bool {
return a == nil
}

func testnil4(a *State) bool {
return a == nil
}

func testnil5(a interface{}) bool {
v := reflect.ValueOf(a)
return !v.IsValid() || v.IsNil()
}

func main() {
var a *State
fmt.Println(testnil1(a, nil))
fmt.Println(testnil2(a, nil))
fmt.Println(testnil3(a))
fmt.Println(testnil4(a))
fmt.Println(testnil5(a))
}

运行后返回的结果如下

false
false
false
true
true

为什么是这个结果?

*因为一个 interface{} 类型的变量包含了2个指针,一个指针指向值的类型,另外一个指针指向实际的值。对一个 interface{} 类型的 nil 变量来说,它的两个指针都是0;但是 var a State 传进去后,指向的类型的指针不为0了,因为有类型了, 所以比较为 false。 interface 类型比较, 要是两个指针都相等,才能相等。

最后