Go接口

Wesley13
• 阅读 642

Go接口的定义

Go 语言不是一种 “传统” 的面向对象编程语言:它里面没有类和继承的概念。但是 Go 语言里有非常灵活的 接口(Interfaces are named collections of method signatures 概念,通过它可以实现很多面向对象的特性。接口提供了一种方式来 说明 对象的行为。_接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。_

通过如下格式定义接口:

type Namer interface {
    Method1(param_list) return_type
    Method2(param_list) return_type
    ...
}

上面的 Namer 是一个 接口类型

(按照约定,只包含一个方法的)接口的名字由方法名加 [e]r 后缀组成,例如 PrinterReaderWriterLoggerConverter 等等。

还有一些不常用的方式(当后缀 er 不合适时),比如 Recoverable,此时接口名以 able 结尾,或者以 I 开头(像 .NET 或 Java 中那样)。

Go 语言中的接口都很简短,通常它们会包含 0 个、最多 3 个方法。

接口的实现

Go 语言中的接口很特别,而且提供了难以置信的一系列灵活性和抽象性。

下面定义一个接口:

type Notifier interface {
    Notify() error
}

我们定义了一个叫做 Notifier 的接口并包含一个 Notify 方法。当一个接口只包含一个方法时,按照 Go 语言的约定命名该接口时添加 -er 后缀。这个约定很有用,特别是接口和方法具有相同名字和意义的时候。

Go 语言不需要我们显式的实现类型的接口。如果一个接口里的所有方法都被我们的类型实现了,那么我们就说该类型实现了该接口。

package main

import (
    "log"
)

type Notifier interface {
    Notify() error
}

type User struct {
    Name  string
    Email string
}

// User 结构体类型实现了接口 Notifier
func (u *User) Notify() error {
    log.Printf("User: Sending User Email To %s<%s>\n", u.Name, u.Email)
    return nil
}

func SendNotification(notify Notifier) error {
    return notify.Notify()
}

func main() {
    user := &User{
        Name:  "AriesDevil",
        Email: "ariesdevil@xxoo.com",
    }

    user.Notify() 

    SendNotification(user)
}

空接口(empty interface) - interface{} 

go 允许不带任何方法的 interface ,这种类型的 interface 叫 empty interface

空的 interface 没有方法,所以可以认为所有的类型都实现了 interface{}

如果定义一个函数参数是 interface{} 类型,这个函数应该可以接受任何类型作为它的参数。

func doSomething(v interface{}){    
}

既然空的 interface 可以接受任何类型的参数,那么一个 interface{}类型的 slice 是不是就可以接受任何类型的 slice ?

func printAll(vals []interface{}) { //1
    for _, val := range vals {
        fmt.Println(val)
    }
}
func main(){
    names := []string{"stanley", "david", "oscar"}
    printAll(names)
}

上面的代码是按照我们的假设修改的,执行之后竟然会报 cannot use names (type []string) as type []interface {} in argument to printAll 错误,why?

这个错误说明 go 没有帮助我们自动把 slice 转换成 interface{} 类型的 slice,所以出错了。go 不会对 类型是interface{} 的 slice 进行转换 。

原因是它们俩在内存中的布局是不一样的(参考 官方说明)。

必须使用 for-range 语句来一个一个显式地复制:

var dataSlice []myType = FuncReturnSlice()
var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
for ix, d := range dataSlice {
    interfaceSlice[i] = d
}

接口的值

golang中,接口值是由两部分组成的,一部分是接口的类型,另一部分是该类型对应的值,我们称其为动态类型和动态值。看如下代码打印接口的动态类型和动态值。

package main

import (
    "fmt"
    "reflect"
)

func main() {

    var val interface{} = int64(58)
    fmt.Println(reflect.TypeOf(val))  // int64
    fmt.Println(reflect.ValueOf(val)) // 58

    val = 50
    fmt.Println(reflect.TypeOf(val))  // int
    fmt.Println(reflect.ValueOf(val)) // 50

    val = string("hello world")
    fmt.Println(reflect.TypeOf(val))  // string
    fmt.Println(reflect.ValueOf(val)) // hello world

    val = Point{1, 2}
    fmt.Println(reflect.TypeOf(val))  // main.Point
    fmt.Println(reflect.ValueOf(val)) // {1 2}

    val = &Point{2, 3}                // 此时 val 的类型是 *Point
    fmt.Println(reflect.TypeOf(val))  // *main.Point
    fmt.Println(reflect.ValueOf(val)) // &{2 3}

    val = nil
    fmt.Println(val)                  // <nil>
    fmt.Println(reflect.TypeOf(val))  // <nil>
    fmt.Println(reflect.ValueOf(val)) // <invalid reflect.Value>

}

type Point struct{ X, Y float64 }

接口的nil 值

定义一个变量w ,声明了其类型为io.Writer

func main() {
    var w io.Writer
    fmt.Printf("w.type = %T ,nil = %t \n", w, w == nil) //w.type = <nil> ,nil = true
}

w是nil 是因为

按照Go语言规范,任何类型在未初始化时都对应一个零值:布尔类型是false,整型是0,字符串是"",而指针、函数、interface、slice、channel和map的零值都是nil。

interface的实际实现结构可以理解为下图:

Go接口

其中type就是它的类型(动态类型),value部分是它的值(动态值)。

一个interface类型的变量 w 为nil,就代表着其动态类型和动态值都为 nil 。**对于一个接口的零值就是它的类型和值的部分都是nil。**

但是,考虑这种情况:

Go接口

图中代表着类型不为空,但是interface的动态值是 nil ,那么这种情况下,如果去判断 w 是否为 nil 时,会得到一个false

fmt.Println(w == nil)  //输出 false

要特别注意这钟情况,因为我们可能会犯这样的错误:

if w != nil {
    w.Write([]byte("done!\n")) // 当w的动态值为nil时,会发生panic
}

比如这面这段代码,

package main

import (
    "bytes"
    "fmt"
    "io"
)

/**
io.Writer 是一个接口
接口有值 动态类型 和 动态值
 */
func test(w io.Writer) {
    fmt.Printf("2.type = %T ,nil = %t\n", w, w == nil)
    if w != nil {
        w.Write([]byte("ok"))
    }
}

func main() {
    /**
    buf 是一个结构体类型,可以赋值给它实现的接口
     */
    var buf *bytes.Buffer
    fmt.Printf("1.type = %T ,nil = %t \n", buf, buf == nil)
    test(buf)
}

报错输出如下,

1.type = *bytes.Buffer ,nil = true 
panic: runtime error: invalid memory address or nil pointer dereference
2.type = *bytes.Buffer ,nil = false
[signal SIGSEGV: segmentation violation code=0x1 addr=0x60 pc=0x105f4e6]

goroutine 1 [running]:
bytes.(*Buffer).Write(0x0, 0xc000074008, 0x2, 0x8, 0x2, 0x8, 0x0)
    /usr/local/go/src/bytes/buffer.go:171 +0x26
main.test(0x10d9b40, 0x0)
    /Users/xinxingegeya/workspace-go/src/gitee.com/xxggy/go-learning/test/temp/temp.go:16 +0x10d
main.main()
    /Users/xinxingegeya/workspace-go/src/gitee.com/xxggy/go-learning/test/temp/temp.go:23 +0xab

Process finished with exit code 2

我们来稍微分析一下,在main()中,我们首先声明了一个buf变量,类型是*bytes.Buffer指针类型,值是指针的零值nil。在调用函数test()的时候,参数w会被赋值为动态类型为*bytes.Buffer,_动态值为nil,也就是w是一个包含了空指针值的非空接口_。那么在w != nil判断时,这个等式便是成立的,不过这里也从侧面反映出一个现象,就是这种传参都是值拷贝。

接口嵌套接口

一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。

比如接口 File 包含了 ReadWrite 和 Lock 的所有方法,它还额外有一个 Close() 方法。

type ReadWrite interface {
    Read(b Buffer) bool
    Write(b Buffer) bool
}

type Lock interface {
    Lock()
    Unlock()
}

type File interface {
    ReadWrite
    Lock
    Close()
}

类型断言:如何检测和转换接口变量的类型

var v interface{}

一个接口类型的变量 v 中可以包含任何类型的值,必须有一种方式来检测它的 动态 类型,即运行时在变量中存储的值的实际类型。在执行过程中动态类型可能会有所不同,但是它总是可以分配给接口变量本身的类型。通常我们可以使用 类型断言 来测试在某个时刻 v 是否包含类型 T 的值:

value, ok := element.(T)

element必须是接口类型的变量,T是普通类型。如果断言失败,ok为false,否则ok为true并且value为变量的值。来看个例子:

package main

import (
    "fmt"
)

type Html []interface{}

func main() {
    html := make(Html, 5)
    html[0] = "div"
    html[1] = "span"
    html[2] = []byte("script")
    html[3] = "style"
    html[4] = "head"
    for index, element := range html {
        if value, ok := element.(string); ok {
            fmt.Printf("html[%d] is a string and its value is %s\n", index, value)
        } else if value, ok := element.([]byte); ok {
            fmt.Printf("html[%d] is a []byte and its value is %s\n", index, string(value))
        }
    }
}

其实断言还支持另一种简化使用的方式:value := element.(T)。但这种方式不建议使用,因为一旦element.(T)断言失败,则会产生运行时错误。如:

package main

import (
    "fmt"
)

func main() {
    var val interface{} = "good"
    fmt.Println(val.(string))
    // fmt.Println(val.(int))
}

以上的代码中被注释的那一行会运行时错误。这是因为val实际存储的是string类型,因此断言失败。

还有一种转换方式是switch测试。既然称之为switch测试,也就是说这种转换方式只能出现在switch语句中。可以很轻松的将刚才用断言的例子换成由switch测试来实现:

package main

import (
    "fmt"
)

type Html []interface{}

func main() {
    html := make(Html, 5)
    html[0] = "div"
    html[1] = "span"
    html[2] = []byte("script")
    html[3] = "style"
    html[4] = "head"
    for index, element := range html {
        switch value := element.(type) {
        case string:
            fmt.Printf("html[%d] is a string and its value is %s\n", index, value)
        case []byte:
            fmt.Printf("html[%d] is a []byte and its value is %s\n", index, string(value))
        case int:
            fmt.Printf("invalid type\n")
        default:
            fmt.Printf("unknown type\n")
        }
    }
}

CASE 一

em必须为interface类型才可以进行类型断言

比如下面这段代码,

s := "hello world"
if v, ok := s.(string); ok {
    fmt.Println(v)
}

运行报错, invalid type assertion: s.(string) (non-interface type string on left)

在这里只要是在声明时或函数传进来的参数不是interface类型那么做类型断言都是回报 non-interface的错误的 所以我们只能通过将s作为一个interface{}的方法来进行类型断言,如下代码所示:

x := "hello world"
if v, ok := interface{}(x).(string); ok { // interface{}(x):把 x 的类型转换成 interface{}
    fmt.Println(v)
}

将s显示的转换为interface{}接口类型则可以进行类型断言了

CASE 二

当函数作为参数并且被调用函数将参数类型指定为interface{}的时候是没有办法直接调用该方法的,如下面代码,

package main

import (
    "fmt"
)

func ServeHTTP(s string) {
    fmt.Println(s)
}

type Handler func(string)

func panduan(in interface{}) {
    Handler(in)("wujunbin")
}

func main() {
    panduan(Handler(ServeHTTP))
}

运行报错,

./assert.go:14: cannot convert in (type interface {}) to type Handler: need type assertion

改正如下,

package main

import (
    "fmt"
)

func ServeHTTP(s string) {
    fmt.Println(s)
}

type Handler func(string) //使用 type 定义一个函数类型

func panduan(in interface{}) {
    v, ok := in.(Handler)
    if ok {
        v("hello world")
    } else {
        panic("assert fail")
    }
}

func main() {
    panduan(Handler(ServeHTTP))
}

只有让传进来的in参数先与Handler进行类型判断如果返回值是OK则代表类型相同才能进行对应的方法调用。

CASE 三

进行类型断言之后如果断言成功,就只能使用该类型的方法。 比如对一个结构体S进行与A接口断言,S实际上实现了A B两个接口,A interface 具有 a()方法,B interface 具有 b()方法,如果结构体S作为参数被传入一个函数中并且在该函数中是interface{}类型,那么进行与A的类型断言之后就只能调用a()而不能调用b(),因为编译器只知道你目前是A类型却不知道你目前也是B类型。看下面这个例子,

package main

import (
    "fmt"
)

type InterfaceA interface {
    AA()
}

type InterfaceB interface {
    BB()
}

// Won 实现了接口 AA 和 BB
type Won struct{}

func (w Won) AA() {
    fmt.Println("AA")
}

func (w Won) BB() {
    fmt.Println("BB")
}

func info(in interface{}) {

    //必须经过 类型断言,才能调用相应的方法
    v, ok := in.(InterfaceA) //必须进行相应的断言才能进行函数的调用,不然编译器不知道是什么具体类型
    if ok {
        v.AA()
    } else {
        fmt.Println("assert fail")
    }
}

func main() {
    var x InterfaceA = Won{}
    info(x)

    var y Won = Won{}
    info(y)
}

空接口和函数重载

函数重载是不被允许的。在 Go 语言中函数重载可以用可变参数 ...T 作为函数最后一个参数来实现。如果我们把 T 换为空接口,那么可以知道任何类型的变量都是满足 T (空接口)类型的,这样就允许我们传递任何数量任何类型的参数给函数,即重载的实际含义。

函数 fmt.Printf 就是这样做的:

fmt.Printf(format string, a ...interface{}) (n int, errno error)

 接口的继承

golang 使用组合实现继承

========END========

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
科工人 科工人
3年前
一文吃透 Go 语言解密之接口 interface
转载:大家好,我是煎鱼。自古流传着一个传言...在Go语言面试的时候必有人会问接口(interface)的实现原理。这又是为什么?为何对接口如此执着?实际上,Go语言的接口设计在整体扮演着非常重要的角色,没有他,很多程序估计都跑的不愉快了。在Go语言的语义上,只要某个类型实现了所定义的一组方法集,则就认为其就是同一种类型,是一个东西。大家常常称其
三十分钟入门基础Go(Java小子版)
本篇文章适用于学习过其他面向对象语言(Java、Php),但没有学过Go语言的初学者。文章主要从Go与Java功能上的对比来阐述Go语言的基础语法、面向对象编程、并发与错误四个方面。
Stella981 Stella981
3年前
Javascript 是如何体现继承的 ?
js继承的概念js里常用的如下两种继承方式:原型链继承(对象间的继承)类式继承(构造函数间的继承) 由于js不像java那样是真正面向对象的语言,js是基于对象的,它没有类的概念。所以,要想实现继承,可以用js的原型prototype机制或者用apply和call方法去实现在面向对象的语言中,我们使用类来创建一个自定义对象
Stella981 Stella981
3年前
Spring AOP 两种代理 Cglib、JDK
概念AOP:AOP是OOP(面向对象编程)的一种延续,OOP中继承的思想主要是提高代码的重用率,但是继承不能同个类在多个方法的相同位置出现的相同代码的问题JDK动态代理:AOP的一种实现,仅支持实现了接口的类。性能较好Cglib:AOP的一种实现,支持实现了接口的类和没有实现接口的类。对比JDK动态代理性能较差SpringAOP:结
Wesley13 Wesley13
3年前
Go语言中通过结构体匿名字段实现方法的继承和重载
Go语言中的结构体可以定义匿名字段。Go语言中没有对象,但是结构体却有大量对象的功能。并且用匿名字段的确可以实现对象的继承和重载。packagemain import "fmt"  typeA struct{     x int }  typeA1 struct{
Stella981 Stella981
3年前
Go 用 interface 模拟多态
多态是C这种语言中的概念,是指对不同的子类对象运行自己定义的方法。在Go语言中没有类的概念,但仍然可以使用structinterface来模拟实现类的功能。下面这个例子演示如何使用Go来模拟C中的多态行为。packagemainimport"fmt"//首先定义了一