interface{} 介绍
在Go语言的世界里,interface{}类型扮演着一个空接口的角色,它能够代表任何类型的值,这与其他编程语言中的void类型或泛型有着相似之处。作为所有类型的超集,任何类型的值都可以赋给interface{}类型的变量,这使得interface{}在Go语言中具有极高的灵活性和通用性。
然而,赋值给interface{}类型的过程并非无损的,尤其是在经历了序列化和反序列化(如入队列、HTTP接口调用等)之后,原有的信息可能会丢失。这不仅可能给调用方带来困扰,还可能导致一些意料之外的bug。
信息丢失案例
案例一:结构/类型信息的丢失
golang
type Result struct {
Code int
Reason string
}
func TestInterfaceStruct(t *testing.T) {
r := Result{
Code: 1,
Reason: "fail",
}
var i interface{}
//序列化后再反序列化,i的类型变成了map[string]interface{}
j, _ := json.Marshal(r)
_ = json.Unmarshal(j, &i)
fmt.Printf("%T\n", i)
fmt.Printf("%+v\n", i)
}
在这个例子中,我们可以看到结构和类型信息的丢失。虽然乍一看这种丢失似乎可以接受,但实际上,当调用方需要根据Code
的值来判断操作是否成功时,这种信息丢失就会变得非常棘手。
golang
func TestInterfaceStruct1(t *testing.T) {
r := Result{
Code: 1,
Reason: "fail",
}
var i interface{}
//序列化后再反序列化,i的类型变成了map[string]interface{}
j, _ := json.Marshal(r)
_ = json.Unmarshal(j, &i)
//调用方面对i时,需准确进行类型断言
if r1, ok := i.(map[string]interface{}); ok {
//断言类型之后,还需断言字段是否存在
if code, ok := r1["Code"]; ok {
// 之后再判断字段的值
if code == 1 {
println("code=1")
} else {
println("code!=1")
}
}
} else {
println("断言失败")
}
}
经过一系列的 if 判断和嵌套,你可能会惊讶地发现输出的是code!=1
:
这是因为在序列化和反序列化过程中丢失了结构信息。在Go语言中,字面值1的默认类型是int
,而r1["Code"]
的类型是interface{}
。由于编译器不会进行隐式类型转换,所以这两个值在比较时被认为是不相等的。
为了准确地进行判断,我们需要进行如下操作:
golang
func TestInterfaceStruct2(t *testing.T) {
r := Result{
Code: 1,
Reason: "fail",
}
var i interface{}
//序列化后再反序列化,i的类型变成了map[string]interface{}
j, _ := json.Marshal(r)
_ = json.Unmarshal(j, &i)
//调用方面对i时,需准确进行类型断言
if r1, ok := i.(map[string]interface{}); ok {
//断言类型之后,还需断言字段是否存在
if code, ok := r1["Code"]; ok {
// 还需要再准确断言字段类型才能进行比较!!!
if c, ok := code.(float64); ok && c == 1 {
println("code=1")
} else {
println("code!=1")
}
}
} else {
println("断言失败")
}
}
最终,我们完成了判断。然而,这个过程非常繁琐,容易出错,甚至可能导致panic。
案例二:大数字精度的丢失
golang
type OrderData struct {
Name string
BigInt int64
}
// 这是一个需要 OrderData 类型的参数的函数
func printDataBigInt(d OrderData) {
fmt.Printf("%d\n", d.BigInt)
}
// 假设这是一个通用延迟队列的消息结构
type DelayMsg struct {
Action string // 执行的动作
Delay int // 延迟多少秒
Data interface{} // 具体的数据
}
func TestInterfaceNumber(t *testing.T) {
d := OrderData{
Name: "test",
BigInt: 1234567890123456789,
}
//延迟消息
m1 := DelayMsg{
Action: "notify", // 10分钟后通知
Delay: 600,
Data: d,
}
//序列化,模拟入队列
j, _ := json.Marshal(m1)
//反序列化,模拟出队列
var m2 DelayMsg
_ = json.Unmarshal(j, &m2)
fmt.Printf("%+v\n", m2)
//取出数据,需要各种断言
var d2 OrderData
d2.Name = m2.Data.(map[string]interface{})["Name"].(string)
d2.BigInt = int64(m2.Data.(map[string]interface{})["BigInt"].(float64))
printDataBigInt(d)
printDataBigInt(d2)
//即便取个巧
var d3 OrderData
j3, _ := json.Marshal(m2.Data)
_ = json.Unmarshal(j3, &d3)
printDataBigInt(d3)
}
你会发现,d2
和d3
的BigInt
字段完全错误了。如果我们需要使用这个字段来执行某些操作,那就会导致出现bug了。
这是因为在反序列化为interface{}
类型时,所有的数值类型都会被转换为float64
类型,而这种类型本身就不是精确的。
Go语言的官方json库文档也对这种情况进行了说明:
为了避免这种情况,我们可以将数值类型转换为json.Number:
接下来,我将分享一种有效的策略,这种策略可以帮助我们避免上述的信息丢失问题。
推荐使用策略
对于反序列化的场景,我将分享一种使用方法,可以有效地避免信息丢失的问题。
核心思路其实就是在运行时给interface{}
指定类型:
golang
// 这是经典的http接口返回结构
type Ret struct {
State int `json:"state"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
type SpecificData struct {
Result int `json:"result"`
Name string `json:"name"`
Power int64 `json:"power"`
}
func TestSpecificData(t *testing.T) {
//http请求返回的body
body := `{"state":1,"msg":"ok","data":{"result":1,"name":"test","power":1234567890123456789}}`
resData := SpecificData{}
ret := Ret{}
ret.Data = &resData //利用具体类型变量的指针,去指定interface{}的类型
_ = json.Unmarshal([]byte(body), &ret)
fmt.Printf("ret.Data type: %T\n", ret.Data)
fmt.Printf("ret: %+v\n", ret)
fmt.Printf("resData: %+v\n", resData)
fmt.Printf("resData.Power: %T\n", resData.Power)
fmt.Printf("resData.Power: %d\n", resData.Power)
if resData.Result == 1 {
println("resData.Result==1")
} else {
println("resData.Result!=1")
}
}
如你所见,结果完美,没有丢失任何信息。
以下是一个体现了上文提到的推荐使用策略的通用HTTP客户端设计示例:
golang
import (
"context"
"encoding/json"
"errors"
)
// HttpClient 可自行根据框架或Go原生方法实现http请求
type HttpClient interface {
Do(ctx context.Context, method, route string, param interface{}) (body string, err error)
}
// TestApiClient 假设有一个api服务,有两个接口,一个是/api/test1,一个是/api/test2
type TestApiClient struct {
Cli HttpClient
}
func NewTestApiClient(cli HttpClient) *TestApiClient {
return &TestApiClient{Cli: cli}
}
type (
// Response 该api服务返回的通用数据结构
Response struct {
State int `json:"state"` // 1表示成功,其他表示失败
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
Test1Param struct {
A int `json:"a"`
}
Test1Data struct {
BigInt int64 `json:"big_int"`
}
Test2Data struct {
Result int `json:"result"`
Money float64 `json:"money"`
}
)
func (t *TestApiClient) DoRequest(ctx context.Context, method, route string, param, ret interface{}) (err error) {
body, err := t.Cli.Do(ctx, method, route, param)
if err != nil {
return
}
res := &Response{}
res.Data = ret
err = json.Unmarshal([]byte(body), res)
if err != nil {
return
}
// 统一处理State判断
if res.State != 1 {
err = errors.New(res.Msg)
}
return
}
func (t *TestApiClient) ApiTest1(ctx context.Context, param Test1Param) (res Test1Data, err error) {
err = t.DoRequest(ctx, "POST", "/api/test1", param, &res)
return
}
func (t *TestApiClient) ApiTest2(ctx context.Context) (res Test2Data, err error) {
err = t.DoRequest(ctx, "POST", "/api/test2", nil, &res)
return
}
简单的测试用例:
golang
import (
"context"
"errors"
"testing"
)
type TestHttpClient struct {
}
// 模拟http请求
func (t TestHttpClient) Do(ctx context.Context, method, route string, param interface{}) (body string, err error) {
switch route {
case "/api/test1":
body = `{"state":1,"msg":"ok","data":{"big_int":1234567890123456789}}`
case "/api/test2":
body = `{"state":1,"msg":"ok","data":{"result":1,"money":123456.456}}`
default:
err = errors.New("not found")
}
return
}
func TestHttpRequest(t *testing.T) {
cli := NewTestApiClient(TestHttpClient{})
// 测试api1
res1, err := cli.ApiTest1(context.Background(), Test1Param{A: 1})
if err != nil {
t.Fatal(err)
}
t.Logf("res1: %+v\n", res1)
if res1.BigInt == 1234567890123456789 {
t.Log("res1.BigInt==1234567890123456789")
} else {
t.Log("res1.BigInt!=1234567890123456789")
}
// 测试api2
res2, err := cli.ApiTest2(context.Background())
if err != nil {
t.Fatal(err)
}
t.Logf("res2: %+v\n", res2)
if res2.Result == 1 {
t.Log("res2.Result==1")
} else {
t.Log("res2.Result!=1")
}
}
此文章来源于------37手游肖俊毅