写点什么

GoLang 简单易用的 json value 读取工具!还并发安全

作者:Krysta
  • 2022 年 6 月 15 日
  • 本文字数:3708 字

    阅读完需:约 12 分钟

实践中的问题

在实际的代码生产过程中,很多时候需要调用其他服务的接口。然而有些接口返回的数据并不是那么友好、让我们看简单的例子:

"{\"simpleArray\":[1,2,3],\"labels\":\"{\\\"level_1\\\":{\\\"tag_id\\\":\\\"example-1\\\",\\\"tag_name\\\":\\\"school\\\",\\\"prob\\\":1,\\\"level\\\":1},\\\"level_2\\\":{\\\"tag_id\\\":\\\"example-2\\\",\\\"tag_name\\\":\\\"class\\\",\\\"prob\\\":1,\\\"level\\\":2}}\"}"
复制代码

返回的是一个字符串、只嵌套了两层。正常想要获取 labels 里面的 tag_name:1.构造 struct、2.使用 map[string]interface{} 层层解析。struct 的优点很明显、就是层次清晰,使用简单。但是当返回的内容非常多、而我们又恰恰只需要其中的一部分 这投入产出比就很低了


而如果是使用 map 层层解析的话、它长这样

const (	simple = "{\"simpleArray\":[1,2,3],\"labels\":\"{\\\"level_1\\\":{\\\"tag_id\\\":\\\"example-1\\\",\\\"tag_name\\\":\\\"school\\\",\\\"prob\\\":1,\\\"level\\\":1},\\\"level_2\\\":{\\\"tag_id\\\":\\\"example-2\\\",\\\"tag_name\\\":\\\"class\\\",\\\"prob\\\":1,\\\"level\\\":2}}\"}")
func TestMap(t *testing.T) { m := make(map[string]interface{}) json.Unmarshal([]byte(simple), &m) m2 := make(map[string]interface{}) json.Unmarshal([]byte(m["labels"].(string)), &m2) level1 := m2["level_1"].(map[string]interface{}) tag1 := level1["tag_name"] level2 := m2["level_2"].(map[string]interface{}) tag2 := level2["tag_name"]}
复制代码

这还仅仅是嵌套了两层、多嵌套几层这代码就写麻了。

使用 JPath 轻松解决这类问题

针对这种:返回的信息非常多、嵌套了很多层的 json 字符,而我们又只需要其中一小部分信息的场景。有一个简单的工具 jpath: https://github.com/krystalics/jpath 可以解决、全部代码仅仅 300 行左右、非常多简单;功能却很强大


package main
import ( "fmt" "github.com/krystalics/jpath")
const ( simple = "{\"simpleArray\":[1,2,3],\"labels\":\"{\\\"level_1\\\":{\\\"tag_id\\\":\\\"example-1\\\",\\\"tag_name\\\":\\\"school\\\",\\\"prob\\\":1,\\\"level\\\":1},\\\"level_2\\\":{\\\"tag_id\\\":\\\"example-2\\\",\\\"tag_name\\\":\\\"class\\\",\\\"prob\\\":1,\\\"level\\\":2}}\"}")
func main() { jPath, _ := jpath.New(simple) tag1 := jPath.FindString("labels.level_1.tag_name") tag2 := jPath.FindString("labels.level_2.tag_name")
fmt.Println(tag1, tag2)}
复制代码

直接按照嵌套结构进行 find 就可以了。不需要其他额外的操作!如果需要并发的进行读取、还提供专门的并发版本

safe, _ := jpath.NewConcurrencySafe(simple)for i := 0; i < 100; i++ {  go func() {    tag1 = safe.FindString("labels.level_1.tag_name")    tag2 = safe.FindString("labels.level_2.tag_name")  }()}
复制代码

对于数组的处理也非常的方便

val := jPath.FindInt("simpleArray[1]")
复制代码

JPath 原理探究

其实它的设计思路就是如果方便快捷的帮助用户快速简单的获取多层嵌套 json 字符串里的内容。一些基本的信息、它的底层核心其实仍然是 map[string]interface{},只不过它可以在 find 的运行过程中自动帮用户创建有需要的 map。

核心的 Find

const (	ARRAY_TYPE_PREFIX = "["	ARRAY_TYPE_SUFFIX = "]"	DEFAULT_SEPARATOR = ".")
var ( ARRAY_REGEX, _ = regexp.Compile(`\[-?\d+\]`))
type JPath struct { data map[string]interface{} Separator string //路径分隔符默认是.}
复制代码


核心的方法是 Find

func (jp *JPath) Find(path string) (interface{}, error) {	keys := strings.Split(path, jp.Separator)	index := len(keys) - 1 //路径的尽头
m := jp.data for i, v := range keys { if m == nil { return nil, errors.New(fmt.Sprintf("path %+v not found ", path)) } //数组判断 if !strings.HasSuffix(v, ARRAY_TYPE_SUFFIX) { data := m[v] //到达了要寻找的路径末尾 if i == index { if data == nil { return nil, errors.New(fmt.Sprintf("path %+v not found ", path)) } return data, nil } //path中这个节点并不是数组也不是路径的结尾、只需要处理字符串和map的情况 switch data.(type) { case map[string]interface{}: m = data.(map[string]interface{}) case string: m_sub, _ := strToMap(m[v].(string), false) m[v] = m_sub m = m_sub case []interface{}: return nil, errors.New(fmt.Sprintf("key %+v error,array should be like this [index]", path))
default: return nil, errors.New(fmt.Sprintf("key %+v error,it is not string or map", v)) } } else { //匹配出[index] flag := ARRAY_REGEX.FindString(v) value, err := strconv.Atoi(flag[1 : len(flag)-1]) if err != nil { return nil, errors.New(fmt.Sprintf("illegal index args %+v", flag)) } key := strings.Split(v, flag)[0] //获取数组路径的前缀、例如simple[1]中的simple data := m[key] //这段路径被用户作为数组传入、只需要处理数组的情况 switch data.(type) { case []interface{}: arr := data.([]interface{}) if value > len(arr)-1 { return nil, errors.New(fmt.Sprintf("key %+v error,it is larger than array last index %+v", v, len(arr)-1)) } tmp := arr[value] if i == index { if tmp == nil { return nil, errors.New(fmt.Sprintf("path %+v not found ", path)) } return tmp, nil } //对数组中取出的值、要进行再次判断、因为它并不是路径的最后、需要再生成一个map以便于获取下一层value switch tmp.(type) { case map[string]interface{}: m = tmp.(map[string]interface{}) case string: m_sub, _ := strToMap(tmp.(string), false) m[v] = m_sub m = m_sub default: return nil, errors.New(fmt.Sprintf("key %+v error,it is not string or map", v)) } //在并发场景下、会需要将map完全递归生成。会有这种类型、统一处理了 case []map[string]interface{}:
arr := data.([]map[string]interface{}) if value > len(arr)-1 { return nil, errors.New(fmt.Sprintf("key %+v error,it is larger than array last index %+v", v, len(arr)-1)) } tmp := arr[value] if i == index { if tmp == nil { return nil, errors.New(fmt.Sprintf("path %+v not found ", path)) } return tmp, nil }
m = tmp
default: return nil, errors.New(fmt.Sprintf("key %+v error,it is not a array", v))
} } }
return nil, errors.New(fmt.Sprintf("path %+v not found ", path))}
复制代码

可以看到在非并发场景下、整个处理是沿着用户写的 path、再一层层自动的构造。所以这种方式在并发的时候就会有它的问题、于是 jpath 给出了另一种方式 NewConcurrencySafe 专门用于并发的场景。

并发安全的思路

传统的并发安全、我们自然想到的是加锁。但是为了更大程度的提升性能、JPath 它的处理方式是在实例初始化的时候就将所有的 map 都自动构造。这样在 find 的过程中就不会有写 map 的操作、自然就是并发安全的了。

func NewConcurrencySafe(source string) (*JPath, error) {	jPath, err := New(source)	if err != nil {		return nil, err	}	deepRecursion(jPath.data)	return jPath, nil}
//递归构造func deepRecursion(m map[string]interface{}) { for k, v := range m { switch v.(type) { case map[string]interface{}: deepRecursion(v.(map[string]interface{})) case string: m_sub, err := strToMap(v.(string), true) if err == nil { m[k] = m_sub deepRecursion(m_sub) } else { m[k] = v } case []interface{}: arr := make([]map[string]interface{}, 0) ori := make([]interface{}, 0) for _, val := range v.([]interface{}) { switch val.(type) { case map[string]interface{}: arr = append(arr, val.(map[string]interface{})) case string: m_sub, err := strToMap(val.(string), true) if err != nil { ori = append(ori, val.(string)) } else { arr = append(arr, m_sub) } default: ori = append(ori, val) } } if len(arr) > 0 { m[k] = arr for _, v := range arr { deepRecursion(v) } } else { m[k] = ori } default:
} }}
复制代码


好了、如果工具有帮助到你、或者这个思路对你有启发、就给项目来个 star 吧!https://github.com/krystalics/jpath、主打简单易用!

发布于: 刚刚阅读数: 3
用户头像

Krysta

关注

还未添加个人签名 2018.12.10 加入

还未添加个人简介

评论

发布
暂无评论
GoLang简单易用的json value读取工具!还并发安全_Go_Krysta_InfoQ写作社区