理解 Go 语言的空接口
那么,什么是空接口?
以下是来自 Go Tour 的简短引用:
空接口可以保存任何类型的值。空接口由处理未知类型值的代码使用。
虽然 Go 是一种静态类型语言,Go 也有一些动态类型语言(如 PHP、Ruby 和 Python)的优点。例如,在 API 的上下文中,空接口提供了仅在数据可用时才返回数据的灵活性。你不必仅仅为了这样做而设置一个有效的空值。
虽然我对他们很熟悉,但我以前从未真正和他们一起工作过。然而,最近,当我使用 Twilio 的 Go Helper 库使用 Twilio 的 Lookup API 时,我不得不这样做。
如果你不熟悉该 API:
Lookup v2 API 允许你查询电话号码的信息,以便与用户进行可信的交互。使用此端点,你可以使用免费的基本查找请求和附加数据包格式化和验证电话号码,以获取更深入的运营商和呼叫者信息。
以下是简短示例:
package main
import (
"fmt"
"github.com/twilio/twilio-go"
lookups "github.com/twilio/twilio-go/rest/lookups/v1"
)
func main() {
client := twilio.NewRestClient()
params := &lookups.FetchPhoneNumberParams{}
params.SetType([]string{"caller-name"})
resp, err := client.LookupsV1.FetchPhoneNumber("+15108675310", params)
}
调用 FetchPhoneNumber()
会检查 +15108675310
,如果可用,则会在返回的 LookupsV2PhoneNumber
对象中返回呼叫着名称详细信息。
如果你查看 LookupsV2PhoneNumber
的定义,你会看到 CallerName
属性存储了电话号码的呼叫者姓名详细信息,并被定义为指向空接口(*interface{}
)的指针。
以这种方式定义它,使其能够存储呼叫者姓名详细信息(如果可用的话)。它并没有强制要求信息必须始终可用。
不幸的是,这让我想知道如何访问这些信息(如果有的话)。
对于经验丰富的 Go 开发者来说,这甚至可能都不用想。但是,如果你像我一样还在学习 Go,那么(至少在一开始)你可能不知道该怎么办。
如果用 PHP 我会怎么做?
例如,在使用 laminas-hydrator 时,我会使用响应中的数据对对象进行水合,如下例所示(借用自文档):
<?php
$hydrator = new Laminas\Hydrator\ArraySerializableHydrator();
$data = [
'first_name' => 'James',
'last_name' => 'Kahn',
'email_address' => 'james.kahn@example.org',
'phone_number' => '+61 419 1234 5678',
];
$object = $hydrator->hydrate($data, new ArrayObject());
此处,它从 $data
的内容中生成了新的 ArrayObject ($object
)
之后,我可以通过调用 ArrayObject 的 offsetExists()
方法来检查是否设置了元素。如果它确实存在,我会通过调用 offsetGet()
来检索它的值,如下例所示。
if ($object->offsetExists('first_name')) {
printf("%s\n",$object->offsetGet('first_name'));
}
也许这就是我的问题。我仍然习惯于用 PHP 思考,更普遍的是动态类型语言。
在谷歌上搜索并了解了更多关于空接口的信息后,就清楚了需要什么。即使我意识到这有多么简单,我也有点手忙脚乱。🤦🏼
以下是我所做的。
if resp.CallerName != nil {
caller := *resp.CallerName
callerDetails, _ := caller.(map[string]interface{})
if callerName, ok := callerDetails["caller_name"]; ok && callerName != "" {
message = fmt.Sprintf("%s It is registered to: %s", message, callerName)
}
}
如果 CallerName
不是 nil
,我将其解引用到一个名为 caller
的新变量中。然后,我将该值转换为一个名为 callerDetails
的新对象。
不管怎样,如果设置了 CallerName
,它将是一个带有字符串键的映射。不需要设置任何键,因为数据可能也不可用,因此它们被转换为空接口。
此时,我检查了是否设置了键并具有值。如果是,则打印该值。
如何确定变量的类型?
虽然这不是一个完整的列表,但如果你想知道如何找出变量的类型,我知道有两种方法:
- 类型断言
- reflect 包的
TypeOf()
方法
类型断言
当涉及到类型断言时,有广义和狭义两种方法。广义的方法是使用带有 value.(type)
的 switch
语句,如下例所示。
switch v := value.(type) {
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
fmt.Println("It's an integer")
case string:
fmt.Println("It's a string")
case nil:
fmt.Println("Oh no! It's nil!")
default:
fmt.Println("I cannae do it, captain.")
}
通过传递 type
,它将返回 value
的类型。但是,请记住,你只能将 type
与 switch 语句一起使用。
而狭义方法使用“逗号 ok”习语对特定类型进行测试,如下例所示;它测试 value
是否是字符串。
if v, ok := value.(string); ok {
fmt.Println("It's a string")
}
使用 reflect 包的 TypeOf 方法
另一种方法是,使用 reflect 包的 TypeOf
方法,如下例所示。
varType := reflect.TypeOf(sheetName)
if varType.Kind() == reflect.String {
fmt.Println("sheetName is a string.")
}
该方法返回一个 reflect.Type
的对象。然后你可以调用对象的 Kind()
方法,该防护返回一个 reflect.Kind
对象。而该对象表示了变量的类型。从那里开始,你只需将对象与你感兴趣的适用类型 Type
进行比较。
使用空接口时请小心
我的朋友提醒我:
应尽量避免使用空接口。如果你使用,你是在说变量可以是任何东西,甚至是你不期望的类型。使用它意味着你失去了使用强类型语言的所有优势,因为编译器无法告诉你你犯了错误。在运行时之前,你不会发现错误。
这让我更深入地思考了是否应该使用它。我完全同意他的观点。空接口确实提供了很大的灵活性,但部分代价是放弃了类型安全性。然而,尽管你需要做更多的工作,但可以使用类型断言和/或反射包来部分弥补丢失的内容。
所以,至少在我使用 Go 的这个阶段,我仍然觉得空接口值得作为一种选择。但是,在使用它时要考虑周到,并理解上面提到的含义。
总结
如果你和我一样,从另一种语言(尤其是动态类型的语言)来学习 Go,Go 的空接口可能看起来有点奇怪。事实上,他们甚至可能看起来令人反感。
只需将其视为允许值为空或待设置即可。然后,检查是否设置了该值,如果是,则适当地转换它,然后像其他情况一样处理该对象。