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手游肖俊毅

相关推荐
CodeSheep19 分钟前
中国四大软件外包公司
前端·后端·程序员
千寻技术帮20 分钟前
10370_基于Springboot的校园志愿者管理系统
java·spring boot·后端·毕业设计
风象南21 分钟前
Spring Boot 中统一同步与异步执行模型
后端
聆风吟º22 分钟前
【Spring Boot 报错已解决】彻底解决 “Main method not found in class com.xxx.Application” 报错
java·spring boot·后端
乐茵lin30 分钟前
golang中 Context的四大用法
开发语言·后端·学习·golang·编程·大学生·context
步步为营DotNet1 小时前
深度探索ASP.NET Core中间件的错误处理机制:保障应用程序稳健运行
后端·中间件·asp.net
bybitq1 小时前
Go中的闭包函数Closure
开发语言·后端·golang
吴佳浩9 小时前
Python入门指南(六) - 搭建你的第一个YOLO检测API
人工智能·后端·python
踏浪无痕9 小时前
JobFlow已开源:面向业务中台的轻量级分布式调度引擎 — 支持动态分片与延时队列
后端·架构·开源
Pitayafruit10 小时前
Spring AI 进阶之路05:集成 MCP 协议实现工具调用
spring boot·后端·llm