golang json/yaml tips

实际项目中,常常会用到 yaml,json 与内部结构体实例之间的转换,这里整理一些相关的使用tips以及常见的代码示例,注意json的两种解析方式,一种是json串直接解析成对应的结构,另一种返回的是io.Reader,不再需要通过本地结构作中介,直接进行解析。此外还介绍了实际项目中json与结构体之间进行解析和转换的注意事项。

直接使用json encoding进行处理

在程序中经常涉及到内部组件之间的数据交互,比如client给server端发送请求,或者CRUD api object 等等。或者有些时候,需要在启动的时候加载一个配置文件,再以上场景中,在程序中通常都要声明有对应的struct结构。

通常的情况有两种,一种是从struct实例转化为json数据(或者是yml 也有对应的库),另一种是根据传入的json数据decode成为struct数据,主要涉及到函数方式是json的marshal与unmarshal,实际就是[]byte的encoding以及decoding的操作,下面是一些常见的例子,主要是参考gobyexample中的常见的情况:

此外,在marshal的时候,通过在字段的value值后面加上注解,可以很方便的指定在marshal之后的key值,具体的注解的细节可以参考谢大的这一篇

//注意json marshal 导出golang原本的数据结构的时候 
//需要把结构体的字段大写 这样才能把数据导出来
package main
import (
"encoding/json"
"fmt"
"os"
)
//We’ll use these two structs to demonstrate encoding and decoding of custom types below.
type Response1 struct {
Page int
Fruits []string
}
type Response2 struct {
Page int `json:"page"`
Fruits []string `json:"fruits"`
}
func main() {
//基本类型转化为json数据
bolB, _ := json.Marshal(true)
fmt.Println(string(bolB))
intB, _ := json.Marshal(1)
fmt.Println(string(intB))
fltB, _ := json.Marshal(2.34)

//此时实际存在[]中的是asc码
//通过string强制转化之后变为可读的形式
fmt.Println(fltB)
fmt.Println(string(fltB))
/*
output
[50 46 51 52]
2.34
*/
strB, _ := json.Marshal("gopher")
fmt.Println(string(strB))
//slices以及map encode 成为json结构
slcD := []string{"apple", "peach", "pear"}
slcB, _ := json.Marshal(slcD)
fmt.Println(string(slcB))
//marshal成json的结构
mapD := map[string]int{"apple": 5, "lettuce": 7}
mapB, _ := json.Marshal(mapD)
fmt.Println(string(mapB))
//json package could atomatically encode the custome data type
//The JSON package can automatically encode your custom data types. It will only include exported fields in the encoded output and will by default use those names as the JSON keys.
res1D := &Response1{
Page: 1,
Fruits: []string{"apple", "peach", "pear"}}
res1B, _ := json.Marshal(res1D)
fmt.Println(string(res1B))
//decoding json into the go values
byt := []byte(`{"num":6.13,"strs":["a","b"]}`)
//对于无法识别(未定义)的类型或者是多层嵌套的类型 可以转化为interface{}
var dat map[string]interface{}
//进行decoding操作
if err := json.Unmarshal(byt, &dat); err != nil {
panic(err)
}
fmt.Println(dat)

//之后通过断言的性质将interface{}强制转换成为其底层类型
num := dat["num"].(float64)
fmt.Println(num)
strs := dat["strs"].([]interface{})
str1 := strs[0].(string)
fmt.Println(str1)
// 将json数据unmarshal成为某个结构体实例
str := `{"page": 1, "fruits": ["apple", "peach"],"abc":"der"}`
res := &Response2{}
json.Unmarshal([]byte(str), &res)
fmt.Println(res)
fmt.Println(res.Fruits[0])

// 流式化的方式将struct转化为json
// 不再需要本地结构作为中介
enc := json.NewEncoder(os.Stdout)
d := map[string]int{"apple": 5, "lettuce": 7}
enc.Encode(d)
//fmt.Println(enc)
}

不需要本地结构的实际例子

在上一部分的例子中可以看到,通过json.newEncoder,可以得到json.Decoder结构,之后可以通过流式处理的方式,将json信息转化为本地的结构。特别是对于事件类的信息进行处理的时候,上述方法比较好用,比如要不断监听docker daemon的事件信息,就可以利用如下的方式,下面代码就是一个获取event信息的实际例子:

package main
import (
"io"
"log"
"encoding/json"
dockerclient "github.com/docker/engine-api/client"
"github.com/docker/engine-api/types"
eventtypes "github.com/docker/engine-api/types/events"
"golang.org/x/net/context"
)
func main() {
ctx := context.Background()
endpoint := "unix:///var/run/docker.sock"
defaultHeaders := map[string]string{"User-Agent": "engine-api-cli-1.0"}
cli, err := dockerclient.NewClient(endpoint, "v1.22", nil, defaultHeaders)
if err != nil {
log.Println("err: ", err)
}
// 调用docker event api 返回的是一个io.ReadCloser
body, err := cli.Events(ctx, types.EventsOptions{})
if err != nil {
log.Println("err: ", err)
}
//生成json decoder
dec := json.NewDecoder(body)
log.Println("start to monitor the docker events")
//不断循环 将json 数据流解析成目标结构体
for {
var event eventtypes.Message
err := dec.Decode(&event)
//如果遇到 EOF 或者其他错误 输出对应结果
if err != nil && err != io.EOF {
log.Println("failed to decode")
break
}
log.Printf("event info: %+v", event)
}
}

使用simplejson

与上面的场景不同,有的时候,从api返回json的信息,并不需要构造出全部的结构,可能仅仅是需要其中的某几个字段,就是所谓的组件之间进行通信的场景。这个时候,可以使用simplejson的方式进行信息提取,最核心的地方就是,str与map之间进行的转化,只要转化成map之后就很好处理了。根据下面的例子可以看到,在value值为一个嵌套的struct类型的时候,返回的map中的value值是一个interface{},实际上里面也已经包含了所需要的内容,之后可以根据实际需求,从interface{}中提取具体所需要的信息。interface的相关内容可以参考之前golang-reflection-model一篇。

这种场景下,json的解析使用起来与python的场景比较类似,特点就是数据存取比较灵活。

package main
import (
"fmt"
"reflect"
"github.com/bitly/go-simplejson"
)
func main() {
str := `{"name":"wangzhe","addr":{"city":"hangzhou","street":{"line1":"addra","line2":"addr2"}}}`

js, _ := simplejson.NewJson([]byte(str))

//if the innner value is a struct
//the type of actual value is interface{}
innerMap, _ := js.Get("addr").Map()
fmt.Println(innerMap["street"])
fmt.Println("the type value: ", reflect.TypeOf(innerMap["street"]))

//if the inner value is like the "key":"string"...
//the type of actual value is string
innerMap, _ = js.Get("addr").Get("street").Map()
fmt.Println(innerMap["line2"])
fmt.Println("the type value: ", reflect.TypeOf(innerMap["line2"]))
}
/*
output:
map[line2:addr2 line1:addra]
the type value: map[string]interface {}
addr2
the type value: string
*/

关于yaml的库 以及 yml与json的转化

下面这个是yamltojson的例子,这里使用了一个k8s里面的包,底层还是用的这个包 https://github.com/ghodss/yaml ,在实际使用的时候可以参考其具体的实现。

package main
import (
"fmt"
"k8s.io/kubernetes/pkg/util/yaml"
)
var data = `
apiVersion: v1
kind: ReplicationController
metadata:
name: kube-dns-v8
namespace: kube-system
labels:
k8s-app: kube-dns
version: v8
kubernetes.io/cluster-service: "true"
spec:
replicas: 1
selector:
k8s-app: kube-dns
version: v8
template:
metadata:
labels:
k8s-app: kube-dns
version: v8
kubernetes.io/cluster-service: "true"
spec:
containers:
- name: etcd
image: gcr.io/google_containers/etcd:2.0.9
resources:
limits:
cpu: 100m
memory: 50Mi
command:
- /usr/local/bin/etcd
- -data-dir
- /var/etcd/data
- -listen-client-urls
- http://127.0.0.1:2379,http://127.0.0.1:4001
- -advertise-client-urls
- http://127.0.0.1:2379,http://127.0.0.1:4001
- -initial-cluster-token
- skydns-etcd
volumeMounts:
- name: etcd-storage
mountPath: /var/etcd/data
- name: kube2sky
image: gcr.io/google_containers/kube2sky:1.11
resources:
limits:
cpu: 100m
memory: 50Mi
args:
# command = "/kube2sky"
- -kube_master_url=http://121.40.171.96:8080
- -domain=cluster.local
- name: skydns
image: gcr.io/google_containers/skydns:2015-03-11-001
resources:
limits:
cpu: 100m
memory: 50Mi
args:
# command = "/skydns"
- -machines=http://localhost:4001
- -addr=0.0.0.0:53
- -domain=cluster.local.
ports:
- containerPort: 53
name: dns
protocol: UDP
- containerPort: 53
name: dns-tcp
protocol: TCP
livenessProbe:
httpGet:
path: /healthz
port: 8080
scheme: HTTP
initialDelaySeconds: 30
timeoutSeconds: 5
- name: healthz
image: gcr.io/google_containers/exechealthz:1.0
resources:
limits:
cpu: 10m
memory: 20Mi
args:
- -cmd=nslookup kubernetes.default.svc.cluster.local localhost >/dev/null
- -port=8080
ports:
- containerPort: 8080
protocol: TCP
volumes:
- name: etcd-storage
emptyDir: {}
dnsPolicy: Default # Don't use cluster DNS.
`
func main() {
out, err := yaml.ToJSON([]byte(data))
if err != nil {
panic(err)
}
//resp, err := codec.Decode()
if err != nil {
panic(err)
}
fmt.Printf("%v", string(out))
}

还有一个包是gopkg.in/yaml.v2,可以将yaml的信息转化为map或者是指定的结构,具体的操作与yaml的转化比较类似,下面是一个yaml转指定struct的例子:

package main
import (
"fmt"
"log"
"gopkg.in/yaml.v2"
)
var data =
`
a: Easy!
b:
c: 2
d: [3, 4]
`
type T struct {
A string
B struct {
C int
D []int ",flow"
}
}
func main() {
t := T{}
err := yaml.Unmarshal([]byte(data), &t)
if err != nil {
log.Fatalf("error: %v", err)
}
fmt.Printf("--- t:\n%v\n\n", t)
}

实践中的小tips

unmaeshal之前设置初始值
在将一个json格式的字符串unmarshal成为一个结构体变量之前,应该给这个结构体变量设置初始的默认值,因为不能保证上游的数据是不是都包含了结构体的某个字段,这样在设置了初始值之后,如果上游的json串没有某个key:value,向下走的数据会使用默认值,如果使用了某个key:value,向下走的结构体实例会自动赋值上新的字段值,这样操作的目的是保证数据可控。

interface{}类型在unmarshal之后丢失信息
参考下面的例子

package main
import (
"encoding/json"
"fmt"
"reflect"
)
type Interfacea interface {
methoda()
}
type Ta struct {
Paraa string `json: paraa`
Parab string `json: parab`
}
type Tb struct {
Tbinter interface{} `json: tbprinter`
Tbpara string `json: tbparaa`
}
func (t Ta) methoda() {
fmt.Println("print the parameters in method a", t.Paraa, t.Parab)
}
func main() {
ta := Ta{
Paraa: "paraa",
Parab: "parab",
}
data, err := json.Marshal(ta)
if err != nil {
fmt.Println(err)
}
fmt.Println("marshal info:", string(data))
var infac Interfacea
infac = ta
value, ok := infac.(Ta)
fmt.Printf("transfer interface to struct: %+v, %+v\n", ok, value)
tb := Tb{
Tbinter: infac,
Tbpara: "tbparaa",
}
datab, err := json.Marshal(tb)
if err != nil {
fmt.Println(err)
}
fmt.Println("marshal info:", string(datab))
var tbnew Tb
err = json.Unmarshal(datab, &tbnew)
if err != nil {
fmt.Println(err)
}
fmt.Printf("after marchal tbnew: %+v\n", tbnew)
fmt.Printf("the inner type before unmarshal: %+v\n", reflect.TypeOf(tb.Tbinter).Kind())
fmt.Printf("the inner type after unmarshal: %+v\n", reflect.TypeOf(tbnew.Tbinter).Kind())
}
/*
output
marshal info: {"Paraa":"paraa","Parab":"parab"}
transfer interface to struct: true, {Paraa:paraa Parab:parab}
marshal info: {"Tbinter":{"Paraa":"paraa","Parab":"parab"},"Tbpara":"tbparaa"}
after marchal tbnew: {Tbinter:map[Paraa:paraa Parab:parab] Tbpara:tbparaa}
the inner type before unmarshal: struct
the inner type after unmarshal: map
*/

在unmarshal之前,Tb结构的Tbinter中的类型是一个interface,其中的data是struct类型,但是在进行了转化之后,类型变成了一个map,这样原先的interface中的data值就被转化成了一个map[string]interface{}类型的值,此时就不能再将interface通过断言的方式解析成原本存在于interface中的Tb类型的实例而只能根据map中key值的内容一点一点向内解析,或者通过simplejson的库也可以很容易的操作。

json转化进阶

主要内容参考这个

参考资料

simplejson使用

http://stackoverflow.com/questions/21432848/go-json-with-simplejson

invalid character的错误

http://stackoverflow.com/questions/25187718/invalid-character-x00-after-top-level-value

推荐文章