Golang 中的空接口

Empty Interfaces in Golang

编辑:这不是在 Go 中使用接口的正确方法。这个问题的目的是让我了解空接口在 Go 中是如何工作的。

如果 Go 中的所有类型都实现 interface{}(空接口),为什么我不能访问 CatDog 结构中的 name 字段?如何通过函数 sayHi() 访问每个结构体的名称字段?

package main

import (
    "fmt"
)

func sayHi(i interface{}) {

    fmt.Println(i, "says hello")

    // Not understanding this error message
    fmt.Println(i.name) //  i.name undefined (type interface {} is interface with no methods)
} 

type Dog struct{
    name string
}
type Cat struct{
    name string
}

func main() {
    d := Dog{"Sparky"}
    c := Cat{"Garfield"}

    sayHi(d) // {Sparky} says hello
    sayHi(c) // {Garfield} says hello
}

interface{} 是方法集,不是字段集。如果一个类型的方法包含该接口的方法,则它实现了一个接口。由于空接口没有任何方法,所有类型都实现它。

如果你需要访问一个字段,你必须得到原始类型:

name, ok:=i.(Dog).name

如果 iDog,这将恢复名称。

或者,为 DogCat 实现一个 getName() 函数,然后它们都将实现以下接口:

type NamedType interface {
   getName() string
}

然后您可以将函数重写为:

func sayHi(i NamedType) {
   fmt.Println(i.getName()) 
}

你不能那样做,因为接口值不那样做。

接口值做什么——不管接口类型本身;接口类型是否为空并不重要——它们包含两个东西:

  • 某个值的具体类型(或无类型);和
  • 该具体类型的值(或无值)。

因此,如果某些变量 v 或表达式 e 具有类型 I,其中 I 是接口类型,那么您可以使用某些语法检查或这两个 "fields"。它们不是 struct 字段,因此您不能只使用 v.type,但您 可以 这样做:

switch v.(type) {
case int: // the value in v has type int
case *float64: // the value in v has type float64
// etc
}

switch中的.(type)表示让我看一下类型字段

获取实际的 value 更难,因为 Go 或多或少要求您先检查类型。在你的例子中,你知道 i 持有 DogCat,所以你可以写:

var name string
switch i.(type) {
case Dog: name = i.(Dog).name
case Cat: name = i.(Cat).name
default: panic("whatever 'i' is, it is not a Dog or Cat")
}
fmt.Println(name)

这很笨拙,有很多方法可以让它不那么笨拙,但这始终是第一步:弄清楚类型是什么

好吧,有时在第一步之前有一个步骤:找出变量中是否有任何东西。你这样做:

if i == nil {
    ...
}

但是请注意,如果 i 中有一些 typed 值,并且该类型可以包含 nil 指针,则 value[= i 的 88=] 部分可以为零,但 i == nil 将为假。那是因为 i 里面有一个 type.

var i interface{}
var p *int
if i == nil {
    fmt.Println("i is initially nil")
}
if p == nil {
    fmt.Println("p is nil")
}
i = p
if i != nil {
    fmt.Printf("i is now not nil, even though i.(*int) is %v\n", i.(*int))
}

(在 Go playground 上试试这个)。

这通常不是正确的使用方式 interface

大多数时候——也有例外——我们甚至不尝试查看某些接口的类型。相反,我们定义了一个接口,它提供 方法 ——我们可以调用的函数——来完成我们需要完成的事情。请参阅 ,其中接口类型具有 getName 方法。然后,我们不会试图弄清楚某人给了我们哪些有限的类型,我们只是说:

name := i.getName()

在基础具体值上调用 getName 方法 。如果 i 持有一个 Dog,它调用 func (Dog) getName() string,您需要对其进行定义。如果 i 持有 Cat,它调用 func (Cat) getName() string。如果您决定向您的集合中添加一个名为 Bird 的类型,您可以定义 func (Bird) getName() string,依此类推。

(通常,这些方法也会 导出 GetName,而不是 getName。)

如您所说,interface{} 是一个 接口。你怎么能假设 "empty" 中有一个 name 字段 (fmt.Println(i.name))?你不能。事实上,go 不支持接口中的字段,只支持方法。

您可以做的(当然还有很多解决方案)是定义一个接口(我们称之为 Pet),它有一个返回宠物名称的方法:

type Pet interface {
    getName() string
}

然后你可以在sayHi函数中接收这个接口(它的对象)并用它来打印宠物的名字:

func sayHi(i Pet) {
    fmt.Println(i.getName())
}

现在,为了能够将 DogCat 传递给 sayHi(),这两个结构都必须实现接口。因此,为它们定义 getName() 方法:

func (d Dog) getName() string {
    return d.name
}

func (c Cat) getName() string {
    return c.name
}

就是这样。

也可以使用如下开关类型实现-

package main

import (
    "fmt"
)

func sayHi(i interface{}) {

    fmt.Println(i, "says hello")

    switch x := i.(type) {
    case Dog:
        fmt.Println(x.name)
    case Cat:
        fmt.Println(x.name)
    }

}

type Dog struct {
    name string
}
type Cat struct {
    name string
}

func main() {
    d := Dog{"Sparky"}
    c := Cat{"Garfield"}

    sayHi(d) // {Sparky} says hello
    sayHi(c) // {Garfield} says hello
}

游乐场link - https://go.dev/play/p/p4s3_jmmbGJ