12.Go 接口

接口在类型上定义了一组方法。
接口用于抽象行为。
在Go中,接口只是一组方法。 我们使用接口来指定某个对象的行为。

例如:标准库中定义的io.Reader接口:

type Reader interface {
    Read(d []byte) (int, error)
}

对二进制数据流进行操作的大多数函数(例如json解码器)都将io.Reader用作数据源。 这样,我们可以为物理文件,内存中的字节,网络连接实现Reader接口,并在所有这些源上使用json.Decode。

下面是定义和实现一个简单的接口:

// Stringer是只有一个方法的接口
type Stringer interface {
    String() string
}

// User struct实现了Stringer接口
type User struct {
    Name string
}

func (u *User) String() string {
    return u.Name
}

// 任何类型都可以实现接口
// 这里我们创建int的别称来实现Stringer接口

type MyInt int

func (mi MyInt) String() string {
    return strconv.Itoa(int(mi))
}

// printTypeAndString 接收一个接口作为参数, 's'可以是实现了Stringer接口的任意值

func printTypeAndString(s Stringer) {
    fmt.Printf("%T: %s\n", s, s)
}

func main() {
    u := &User{Name: "John"}
    printTypeAndString(u)

    n := MyInt(5)
    printTypeAndString(n)
}

*main.User: John
main.MyInt: 5

与大多数其他语言不同,Go的接口是隐式满足的。我们不必明确声明struct User实现了Stringer接口。

接口只能包含方法,不能包含数据。 如果要重用方法和数据,则可以使用结构嵌入。

只能在同一包中定义的类型上定义方法。 上例中我们必须定义类型别名MyInt,因为我们无法将方法添加到Go内置的int类型。

简单接口

type Painter interface {
    Paint()
}

类型实现接口时不必声明它正在实现那个接口。 定义相同签名的方法就可以了。

type Rembrandt struct{}

func (r Rembrandt) Paint() {
    // use a lot of canvas here
}

现在,我们可以将这个struct用作接口。

var p Painter
p = Rembrandt{}

接口可以被任意数量的类型实现。 一个类型也可以实现任意数量的接口。

type Singer interface {
        Sing()
}

type Writer interface {
        Write()
}

type Human struct{}

func (h *Human) Sing() {
    fmt.Println("singing")
}

func (h *Human) Write() {
    fmt.Println("writing")
}

type OnlySinger struct{}

func (o *OnlySinger) Sing() {
    fmt.Println("singing")
}

这里Human同时实现了Singer和Writer接口,而OnlySinger只实现了Sinnger接口.

空接口

空接口类型是不包含任何方法的接口。 我们将其声明为interface {}。 它不包含任何方法,因此每种类型都可以满足要求。 因此,空接口可以包含任何类型值。

var a interface{}
var i int = 5
s := "Hello world"

type StructType struct {
    i, j int
    k string
}

// 下列声明都合法
a = i
a = s
a = &StructType{1, 2, "hello"}

接口最常见的作用是确保变量支持一个或多个行为。 相比之下,空接口的主要作用是定义一个变量,该变量可以保留任何值,而不管其具体类型是什么。
为了使这些值恢复为原始类型,我们只需要做:

i = a.(int)
s = a.(string)
m := a.(*StructType)

或者

i, ok := a.(int)
s, ok := a.(string)
m, ok := a.(*StructType)

返回值ok表示接口a是否可转换为给定类型。 如果无法执行,ok将为false。

接口值

如果声明了接口的变量,则它可以存储任何实现了接口声明方法的任何类型!

如果我们声明h为接口Singer,则它可以存储Human或OnlySinger类型的值。 这是因为它们都实现了Singer接口指定的方法。

var h Singer
h = &human{}

h.Sing()

从接口确定底层类型

在Go中,有时候需要知道参数的底层类型。 这可以通过switch来完成。 假设我们有两个结构:

type Rembrandt struct{}

func (r Rembrandt) Paint() {}

type Picasso struct{}

func (r Picasso) Paint() {}

都实现了Painter接口:

type Painter interface {
    Paint()
}

然后我们可以通过switch来决定参数底层类型:

func WhichPainter(painter Painter) {
    switch painter.(type) {
    case Rembrandt:
        fmt.Println("The underlying type is Rembrandt")
    case Picasso:
        fmt.Println("The underlying type is Picasso")
    default:
        fmt.Println("Unknown type")
    }
}

确保类型实现了接口

在Go中,接口是隐式满足的, 即不必声明类型是一定要实现某个接口的。
这很方便,但也可能导致无法完全实现接口错误,并且编译器无法检测到这种情况。
有一种方法可以进行编译期的类型检查:

type MyReadCloser struct {
}

func (rc *MyReadCloser) Read(d []byte) (int, error) {
    return 0, nil
}

var _ io.ReadCloser = &MyReadCloser{}

./main.go:15:5: cannot use &MyReadCloser literal (type *MyReadCloser) as type io.ReadCloser in assignment:
*MyReadCloser does not implement io.ReadCloser (missing Close method)
exit status 2

我们的意图是让MyReadCloser实现io.ReadCloser接口, 然而我们忘记了实现Close方法,这行代码在编译时就报错了:

var _ io.ReadCloser = &MyReadCloser{}

我们尝试把MyReadCloser赋值给io.ReadCloser
因为
MyReadCloser没有实现Close方法,编译器在编译期间检测到这是非法的赋值
我们把值赋给空变量_是因为这个值并不会被使用

空接口

技术上讲, 空接口就是没有方法的接口.从而可以说任何类型都实现了空接口

实际上, Go中的空接口就像Java和C++中的object类型,它结合了类型及其值。
空接口等效于静态语言中的动态类型
空接口也是Go中实现union类型的一种方法.
由于每种类型都符合interface {},因此可以将任何值分配给interface {}类型的变量。

那时你将无法在编译时知道它的真实类型。

空接口的零值为nil.

使用示例:

func printVariableType(v interface{}) {
    switch v.(type) {
    case string:
        fmt.Printf("v is of type 'string'\n")
    case int:
        fmt.Printf("v is of type 'int'\n")
    default:
        // generic fallback
        fmt.Printf("v is of type '%T'\n", v)
    }
}

func main() {
    printVariableType("string") // string
    printVariableType(5)        // int
    printVariableType(int32(5)) // int32
}

v is of type 'string'
v is of type 'int'
v is of type 'int32'

编译时,如果变量的类型为interface(包括空接口),则你不知道它的真正类型是什么。

可以在运行时使用类型断言来访问基础类型:

func printTypeAndValue(iv interface{}) {
    if v, ok := iv.(string); ok {
        fmt.Printf("iv is of type string and has value '%s'\n", v)
        return
    }
    if v, ok := iv.(int); ok {
        fmt.Printf("iv is of type int and has value '%d'\n", v)
        return
    }
    if v, ok := iv.(*int); ok {
        fmt.Printf("iv is of type *int and has value '%s'\n", v)
        return
    }
}

func panicOnInvalidConversion() {
    var iv interface{} = "string"

    v := iv.(int)
    fmt.Printf("v is int of value: %d\n", v)
}

func main() {
    // pass a string
    printTypeAndValue("string")
    i := 5
    // pass an int
    printTypeAndValue(i)
    // pass a pointer to int i.e. *int
    printTypeAndValue(&i)

    panicOnInvalidConversion()
}

iv is of type string and has value 'string'
iv is of type int and has value '5'
iv is of type int and has value '%!s(int=0xc000016048)'
nic: interface conversion: interface {} is string, not int

routine 1 [running]:
in.panicOnInvalidConversion()
/tmp/src178978545/type assertion.go:28 +0x45
in.main()
/tmp/src178978545/type assertion.go:41 +0x9a
it status 2

类型断言

类型断言允许你检查空接口的值是否为给定的类型。

为了完整起见,可以使用类型switch的简短版本:v:= iv.(int)(v, ok:= iv.(int))。

它和完整版本的区别在于,如果iv不是断言的类型,则简短版本会panic:

func panicOnInvalidConversion(iv interface{}) {
    v := iv.(int)
    fmt.Printf("v is int of value: %d\n", v)
}

func main() {
    panicOnInvalidConversion("string")
}

panic: interface conversion: interface {} is string, not int

goroutine 1 [running]:
main.panicOnInvalidConversion(0x4a0120, 0x4db030)
/tmp/src459967900/type assertion.go:11 +0xd6
main.main()
/tmp/src459967900/type assertion.go:16 +0x39
exit status 2

As a rule of thumb, you shouldn’t try to discover underlying value of interface type as it pierces through an abstraction.
根据经验,你不应该尝试发现贯穿抽象的接口类型的底层值。

类型switch

switch语句可以基于接口包装的类型进行分派。

如果具有接口值,则可以根据底层类型进行切换:

func smartConvertToInt(iv interface{}) (int, error) {
    // inside case statements, v is of type matching case type
    switch v := iv.(type) {
    case int:
        return v, nil
    case string:
        return strconv.Atoi(v)
    case float64:
        return int(v), nil
    default:
        return 0, fmt.Errorf("unsupported type: %T", iv)
    }
}

func printSmartConvertToInt(iv interface{}) {
    i, err := smartConvertToInt(iv)
    if err != nil {
        fmt.Printf("Failed to convert %#v to int\n", iv)
        return
    }
    fmt.Printf("%#v of type %T converted to %d\n", iv, iv, i)
}

func main() {
    printSmartConvertToInt("5")
    printSmartConvertToInt(4)
    printSmartConvertToInt(int32(8))
    printSmartConvertToInt("not valid int")
}

"5" of type string converted to 5
4 of type int converted to 4
Failed to convert 8 to int
Failed to convert "not valid int" to int

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,039评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,426评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,417评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,868评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,892评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,692评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,416评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,326评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,782评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,957评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,102评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,790评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,442评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,996评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,113评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,332评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,044评论 2 355

推荐阅读更多精彩内容