Golang中的interface{}:信息丢失案例与推荐使用策略

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)
}

你会发现,d2d3BigInt字段完全错误了。如果我们需要使用这个字段来执行某些操作,那就会导致出现bug了。

这是因为在反序列化为interface{}类型时,所有的数值类型都会被转换为float64类型,而这种类型本身就不是精确的。

Go语言的官方json库文档也对这种情况进行了说明:

pkg.go.dev/encoding/js...

为了避免这种情况,我们可以将数值类型转换为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手游肖俊毅

相关推荐
gb421528731 分钟前
springboot中Jackson库和jsonpath库的区别和联系。
java·spring boot·后端
程序猿进阶32 分钟前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
颜淡慕潇1 小时前
【K8S问题系列 |19 】如何解决 Pod 无法挂载 PVC问题
后端·云原生·容器·kubernetes
向前看-8 小时前
验证码机制
前端·后端
超爱吃士力架10 小时前
邀请逻辑
java·linux·后端
AskHarries12 小时前
Spring Cloud OpenFeign快速入门demo
spring boot·后端
isolusion13 小时前
Springboot的创建方式
java·spring boot·后端
zjw_rp13 小时前
Spring-AOP
java·后端·spring·spring-aop
TodoCoder14 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试