golang 使用接口和类型断言在Go中使用任意键名解析JSON对象

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang 使用接口和类型断言在Go中使用任意键名解析JSON对象相关的知识,希望对你有一定的参考价值。

// Parsing arbitrary JSON using interfaces in Go
// Demonstrates how to parse JSON with abritrary key names
// See https://blog.golang.org/json-and-go for more info on generic JSON parsing

package main

import (
	"encoding/json"
	"fmt"
)

var jsonBytes = []byte(`
{
	"key1":{
		"Item1": "Value1",
		"Item2": 1},
	"key2":{
		"Item1": "Value2",
		"Item2": 2},
	"key3":{
		"Item1": "Value3",
		"Item2": 3}
}`)

// Item struct; we want to create these from the JSON above
type Item struct {
	Item1 string
	Item2 int
}

// Implement the String interface for pretty printing Items
func (i Item) String() string {
	return fmt.Sprintf("Item: %s, %d", i.Item1, i.Item2)
}

func main() {

	// Unmarshal using a generic interface
	var f interface{}
	err := json.Unmarshal(jsonBytes, &f)
	if err != nil {
		fmt.Println("Error parsing JSON: ", err)
	}

	// JSON object parses into a map with string keys
	itemsMap := f.(map[string]interface{})

	// Loop through the Items; we're not interested in the key, just the values
	for _, v := range itemsMap {
		// Use type assertions to ensure that the value's a JSON object
		switch jsonObj := v.(type) {
		// The value is an Item, represented as a generic interface
		case interface{}:
			var item Item
			// Access the values in the JSON object and place them in an Item
			for itemKey, itemValue := range jsonObj.(map[string]interface{}) {
				switch itemKey {
				case "Item1":
					// Make sure that Item1 is a string
					switch itemValue := itemValue.(type) {
					case string:
						item.Item1 = itemValue
					default:
						fmt.Println("Incorrect type for", itemKey)
					}
				case "Item2":
					// Make sure that Item2 is a number; all numbers are transformed to float64
					switch itemValue := itemValue.(type) {
					case float64:
						item.Item2 = int(itemValue)
					default:
						fmt.Println("Incorrect type for", itemKey)
					}
				default:
					fmt.Println("Unknown key for Item found in JSON")
				}
			}
			fmt.Println(item)
		// Not a JSON object; handle the error
		default:
			fmt.Println("Expecting a JSON object; got something else")
		}
	}

}

golang类型断言的使用(Type Assertion)

第一部分

首先,转自https://studygolang.com/articles/3314对断言的基本介绍

golang的语言中提供了断言的功能。golang中的所有程序都实现了interface的接口,这意味着,所有的类型如string,int,int64甚至是自定义的struct类型都就此拥有了interface的接口,这种做法和java中的Object类型比较类似。那么在一个数据通过func funcName(interface)的方式传进来的时候,也就意味着这个参数被自动的转为interface的类型。

如以下的代码:

func funcName(a interface) string 
     return string(a)

编译器将会返回:

cannot convert a (type interface) to type string: need type assertion

此时,意味着整个转化的过程需要类型断言。类型断言有以下几种形式:

1)直接断言使用

var a interface

fmt.Println("Where are you,Jonny?", a.(string))

但是如果断言失败一般会导致panic的发生。所以为了防止panic的发生,我们需要在断言前进行一定的判断

value, ok := a.(string)

如果断言失败,那么ok的值将会是false,但是如果断言成功ok的值将会是true,同时value将会得到所期待的正确的值。示例:

value, ok := a.(string)
if !ok 
    fmt.Println("It‘s not ok for type string")
    return

fmt.Println("The value is ", value)


另外也可以配合switch语句进行判断:

var t interface
t = functionOfSomeType()
switch t := t.(type) 
default:
    fmt.Printf("unexpected type %T", t)       // %T prints whatever type t has
break case bool: fmt.Printf("boolean %t\n", t) // t has type bool
break case int: fmt.Printf("integer %d\n", t) // t has type int
break case *bool: fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
break case *int: fmt.Printf("pointer to integer %d\n", *t) // t has type *int
break


第二部分

net/jsonrpc增加get_client_ip功能

问题描述:falcon-agent无法监测到宿主机ip的变动,导致初始化安装时写在配置里的IP地址与当前真实IP地址不符,产生错误的上报数据(将数据以旧IP上报,新IP查不到任何监控项)。

解决思路:由于agent已大规模部署在服务器上,更新比较麻烦,考虑修改transfer端,从rpc连接中直接获取到agent的IP。

 

来自open-falcon/falcon-plus/transfer组件中的rpc相关代码:

初始建立RPC server:open-falcon\falcon-plus\modules\transfer\receiver\rpc\rpc.go

package rpc

import (
    "github.com/open-falcon/falcon-plus/modules/transfer/g"
    "log"
    "net"
    "net/rpc"
    "net/rpc/jsonrpc"
)

func StartRpc() 
    if !g.Config().Rpc.Enabled 
        return
    

    addr := g.Config().Rpc.Listen
    tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
    if err != nil 
        log.Fatalf("net.ResolveTCPAddr fail: %s", err)
    

    listener, err := net.ListenTCP("tcp", tcpAddr)
    if err != nil 
        log.Fatalf("listen %s fail: %s", addr, err)
     else 
        log.Println("rpc listening", addr)
    

    server := rpc.NewServer()
    server.Register(new(Transfer))

    for 
        conn, err := listener.Accept()
        if err != nil 
            log.Println("listener.Accept occur error:", err)
            continue
        
        go server.ServeCodec(jsonrpc.NewServerCodec(conn))
    

这里使用的是jsonrpc的编码解码器,其中会对conn中的数据使用json.Unmarshal解码。

 

重要的步骤位于jsonrpc/server.go的下列函数中:

 1 type ArgsContext interface 
 2     Value(key string) interface
 3     SetValue(key string, value interface)
 4 
 5 
 6 func (c *serverCodec) ReadRequestBody(x interface) error 
 7     if x == nil 
 8         return nil
 9     
10     if c.req.Params == nil 
11         return errMissingParams
12     
13     if args, ok := x.(ArgsContext); ok 
14         args.SetValue("conn", c.c)
15     
16     // JSON params is array value.
17     // RPC params is struct.
18     // Unmarshal into array containing struct for now.
19     // Should think about making RPC more general.
20     var params [1]interface
21     params[0] = x
22     return json.Unmarshal(*c.req.Params, &params)
23 

1-4行和13-15行是新增的一个断言判断,目的是为了给解析出来的args参数增加一些上下文信息,比如最重要的:将conn对象存入其中。

如此,便可以从rpc的callback函数中访问到conn对象,从而拿到client IP【要求args的类型在定义时实现ArgsContext的接口】。

 

该思路源自https://github.com/club-codoon/rpcx/blob/630e53bff09759ba2a21644f318907504cfdd98a/_examples/context/server.go,应用方式如下:

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "net"
 6 
 7     "github.com/smallnest/rpcx"
 8 )
 9 
10 type Args struct 
11     A   int `msg:"a"`
12     B   int `msg:"b"`
13     ctx map[string]interface
14 
15 
16 type Reply struct 
17     C int `msg:"c"`
18 
19 
20 func (a *Args) Value(key string) interface 
21     if a.ctx != nil 
22         return a.ctx[key]
23     
24     return nil
25 
26 
27 func (a *Args) SetValue(key string, value interface) 
28     if a.ctx == nil 
29         a.ctx = make(map[string]interface)
30     
31     a.ctx[key] = value
32 
33 
34 type Arith int
35 
36 func (t *Arith) Mul(args *Args, reply *Reply) error 
37     reply.C = args.A * args.B
38     conn := args.Value("conn").(net.Conn)
39     fmt.Printf("Client IP: %s \n", conn.RemoteAddr().String())
40     return nil
41 
42 
43 func main() 
44     server := rpcx.NewServer()
45     server.RegisterName("Arith", new(Arith))
46     server.Serve("tcp", "127.0.0.1:8972")
47 

 

但是该方法有一个局限,如下的callback场景中,args参数为[]*sometype,是一个slice。

如果是一个struct,则可以方便的按照上述方法添加一个私有的ctx即可存放相关数据,但如果是一个slice,是没办法放一个ctx解决的,那样的话会把slice改为一个struct,从而在json.Unmarshal时失败。

RPC server的callback函数定义:open-falcon\falcon-plus\modules\transfer\receiver\rpc\rpc_transfer.go

import (
    "fmt"
    cmodel "github.com/open-falcon/falcon-plus/common/model"
    cutils "github.com/open-falcon/falcon-plus/common/utils"
    "github.com/open-falcon/falcon-plus/modules/transfer/g"
    "github.com/open-falcon/falcon-plus/modules/transfer/proc"
    "github.com/open-falcon/falcon-plus/modules/transfer/sender"
    "strconv"
    "time"
    "path/filepath"
    "crypto/md5"
    "io"
    "encoding/hex"
)

type Transfer int

type TransferResp struct 
    Msg        string
    Total      int
    ErrInvalid int
    Latency    int64


func (t *TransferResp) String() string 
    s := fmt.Sprintf("TransferResp total=%d, err_invalid=%d, latency=%dms",
        t.Total, t.ErrInvalid, t.Latency)
    if t.Msg != "" 
        s = fmt.Sprintf("%s, msg=%s", s, t.Msg)
    
    return s


func (t *Transfer) Update(args []*cmodel.MetricValue, reply *cmodel.TransferResponse) error 
    return RecvMetricValues(args, reply, "rpc")

 

一个workaround思路是,将jsonrpc单拿出来作为一个私有依赖包,更改其中的逻辑,直接将args断言为slice指针类型,并遍历其数据,将client IP放入Endpoint字段中。

【由于transfer的rpc机制只有这里用到了jsonrpc包,所以该workaround可以不影响其他rpc逻辑】:

 1 func (c *serverCodec) ReadRequestBody(x interface) error 
 2     if x == nil 
 3         return nil
 4     
 5     if c.req.Params == nil 
 6         return errMissingParams
 7     
 8     // JSON params is array value.
 9     // RPC params is struct.
10     // Unmarshal into array containing struct for now.
11     // Should think about making RPC more general.
12     var params [1]interface
13     params[0] = x
14     if err := json.Unmarshal(*c.req.Params, &params); err != nil 
15         return err
16     
17     // fmt.Printf("[jsonrpc]x type is %T \n", x)
18     if args, ok := x.(*[]*cmodel.MetricValue); ok 
19         remote_addr := strings.Split(c.c.(net.Conn).RemoteAddr().String(), ":")[0]
20         if remote_addr != "" 
21             for _, v := range *args 
22                 v.Endpoint = remote_addr
23             
24         
25     
26     return nil
27 

 

以上是关于golang 使用接口和类型断言在Go中使用任意键名解析JSON对象的主要内容,如果未能解决你的问题,请参考以下文章

带有指针的 Golang 类型断言

Golang M 2023 6 topic

golang类型断言的使用(Type Assertion)

GoLang接口---中

go语言学习笔记 — 进阶 — 接口:在接口和类型之间转换

Golang type assertion 类型断言