对于任意类型的支持或许可以这样做

165 阅读4分钟

今天想跟大家分享两个问题,这两个问题关联性不大,仅仅是同一时期遇到的两个比较有针对性的问题,所以放在一起进行总结

问题一:在rpc中proto文件中如何定义一个Any类型的数据结构,以便容纳任意数据类型的数据进去,让其他客户端服务可以拿到并且成功解析

问题二:在复杂的yaml文件中如何将其转化为json格式的数据格式

先看第一个问题

  1. 在rpc中proto文件中如何定义一个Any类型的数据结构,以便容纳任意数据类型的数据进去,让其他客户端服务可以拿到并且成功解析

大家都知道,我们再实现业务逻辑时,比如你需要实现一个用户登录的接口,通过rpc服务进行数据核验之后,返回给客户端token字符串,那么具体返回什么数据形式,一般是定义好的,格式固定,在gateway层也有想对应的数据结构来承接通过rpc返回的数据。这种情况一般是比较好处理的。

但是在一种场景下,如果和第三方服务商进行交互,此时对于服务商返回的数据格式就比较不好掌控了,如果在后续的迭代升级中,服务商接口进行了升级改造,变更了数据格式,那么需要重新进行适配,调整数据格式,这种方式就比较不方便,此时,就可以在rpc层面定义一个结构体,用来承接第三方服务商返回的数据格式,然后直接透传给gateway层,让这种数据形式的更改和适配不在rpc层进行适配了,直接丢给客户端,让客户端根据自己的需求来获取相应的数据。如何做呢,接下来,我们看一个实例,来说明一下这个过程。为了方便起见,以下内容以go-zero框架进行代码演示,首先来看rpc层面的代码

我们首先创建一个demo rpc

syntax = "proto3";

package demo;
option go_package="./demo";
import "google/protobuf/any.proto";
message Request {
  string ping = 1;
}

message Response {
  google.protobuf.Any pong = 1;
}

service Demo {
  rpc Ping(Request) returns(Response);
}

在proto文件中,我们定义的返回值结构体使用google.protobuf.Any的类型来承接任意类型的数据内容。

在gateway层,在api文件中,我们可以使用interface来承接rpc返回的数据内容

type Request {
    Name string `path:"name,options=you|me"`
}

type Response {
    Message interface{} `json:"message"`
}

service demo1-api {
    @handler Demo1Handler
    get /from/:name(Request) returns (Response)
}

看起来是不是觉得很简单,重点在于,这两种数据类型不能直接进行赋值,需要通过中间的数据转换,在rpc层面,需要进行以下处理

     ...
    "google.golang.org/protobuf/types/known/anypb"
	"google.golang.org/protobuf/types/known/structpb"

    ... 
    structData, err := structpb.NewStruct(result)
    if err != nil {
        return nil, fmt.Errorf("failed to create struct: %v", err)
    }
    anyData, err := anypb.New(structData)

将任意类型的数据进行转换后,赋值给返回值结构体进行流转。

在gateway层面,需要将rpc返回的数据结构体进行解构处理

    "google.golang.org/protobuf/types/known/structpb"
    ...
    if result.Data != nil {
        var structData structpb.Struct
        if err := result.Data.UnmarshalTo(&structData); err != nil {
            logx.Error("failed to unmarshal Any to Struct: ", err)
            return nil, fmt.Errorf("failed to unmarshal Any to Struct: %v", err)
        }
        data = structData.AsMap()
    }

这样就能达到直接透传不确定数据类型的数据给前端进行处理了,且数据在中间不做任何的数据结构的定义,对于后续的升级更加友好。

  1. 在复杂的yaml文件中如何将其转化为json格式的数据格式

这个问题的场景并不多见,我们可以先来看一个实例,进行说明

// Convert YAML to JSON while preserving comments
func yamlToJsonWithComments(yamlData []byte) ([]byte, error) {
    // Step 1: Parse the YAML with comments
    var node yaml.Node
    err := yaml.Unmarshal(yamlData, &node)
    if err != nil {
        return nil, err
    }

    // Step 2: Convert YAML node structure to map
    var jsonMap interface{}
    err = node.Decode(&jsonMap)
    if err != nil {
        return nil, err
    }

    // Step 3: Marshal the JSON
    jsonData, err := json.MarshalIndent(jsonMap, "", "  ")
    if err != nil {
        return nil, err
    }

    return jsonData, nil
}

func main() {
	yamlStr := `# Example YAML file for testing purposes, used in reading/writing files.
name: Jimmy
age: 29
relationships:
    # 增加注释
    parents:
        # 增加注释2
        - Sally # 增加注释1
        - Robert # 增加注释3
# 增加注释4`
	output, err := yamlToJsonWithComments([]byte(yamlStr))
	if err != nil {
		log.Fatalf("error parsing YAML: %v", err)
	}
	fmt.Println(string(output))
}

就是这种形式的字符串进行转换后,这样看是不是觉得好像也没什么,但是,实例中使用的是 "gopkg.in/yaml.v3"库,如果因为在老代码中,使用的是 "gopkg.in/yaml.v2",如果在进行转换的时候就会出现

2024/11/12 20:07:29 Warning: Unsupported key type map[interface {}]interface {}

这种类型的诡异错误,此时就需要进行特殊的转换,将yaml.Unmarshal后的数据在进行处理一下

func convert(i interface{}) interface{} {
    switch x := i.(type) {
    case map[interface{}]interface{}:
        m2 := map[string]interface{}{}
        for k, v := range x {
            m2[k.(string)] = convert(v)
        }
        return m2
    case []interface{}:
        for i, v := range x {
            x[i] = convert(v)
        }
    }
    return i
}

然后再进行json.Marshal才能正常的进行编码。这可以说是yamlv2版本的一个问题,但是,v3版本可以说进行了很大的升级,对于注释也进行了全面的支持,比如上面实例中的注释,分为HeadComment,LineComment,FootComment三种注释,在进行结构解析时,可以根据需要进行获取。所以在有条件的情况下,尽量选择新版本的yaml库。