今天在解析 Facebook Graph API 返回结果时,Golang 的 encoding/json 包中的 Unmarshaler 接口 发挥了非常不错的效果。

远端 API 返回的 JSON 结构大概是这样:

{
    "name": "Hello World",
    "first_name": "World",
    "last_name": "Hello",
    "picture": {
        "data": {
            "url": "https://lkebin.com/helloworld.png"
        }
    }
}

针对这个数据,在 Golang 代码中定义的结构这样:

type Profile struct {
    Name      string  `json:"name"`
    FirstName string  `json:"first_name"`
    LastName  string  `json:"last_name"`
    Picture   string  `json:"picture"`
}

在程序代码中我希望 Profile.Picture 只是一个 URL 字符串,方便使用。而远端返回的数据却是一个多层级的结构,就不能直接把远端返回的数据按照预期的效果映射到Profile 结构体中。

以往使用其它语言处理这种情况时,可能会使用一些中间处理代码,先把返回的数据解析出来,再提取数据创建目标结构。或者直接把原始数据处理成目标结构后再解析。虽然都能实现,但不是那么优雅。

这个时候,encoding/json 包中提供的 Unmarshaler 接口就派上用场了。

目标是在解析远端 API 返回的这一段数据时:

{
    "picture": {
        "data": {
            "url": "https://lkebin.com/helloworld.png"
        }
    }
}

能做到像解析下面这段一样:

{
    "picture": "https://lkebin.com/helloworld.png"
}

Unmarshaler 接口提供了各类型自定义 JSON 解析的能力,那 Profile 结构中的 Picture 字段类型如果也可以自定义 JSON 解析的方式,就能实现目标效果了。先把 Profile.Picture 换成自定义类型:

type Picture string

type Profile struct {
    ...
    Picture   Picture `json:"picture"`
}

字段数据类型换成了自定义的 Picture 类型,下面就来为 Picture 类型实现自定义的 JSON 解析方式。Unmarshaler 接口定义如下,只有一个方法。

type Unmarshaler interface {
    UnmarshalJSON([]byte) error
}

实现

func (p *Picture) UnmarshalJSON(b []byte) error {
    // 定义一个能够映射原始数据中 picture 的结构
    var pictureData struct {
		Data struct {
			Url string `json:"url"`
		} `json:"data"`
	}

    // 把原始数据中的 picture 映射到定义的结构中
	if err := json.Unmarshal(b, &pictureData); err != nil {
		return err
	}

    // 提取目标数据,替换原始数据
	*p = Picture(pictureData.Data.Url)

    return nil
}

这样一来,当我在接收到远端 API 返回时,就可以直接把返回的数据解析到目标结构中。虽然自定义的 UnmarshalJSON 实现方法同样使用了中间代码来处理,但它却让处理过程变成非常简单。

func main() {
	var j = `{
		"name": "Hello World",
		"first_name": "World",
		"last_name": "Hello",
		"picture": {
			"data": {
				"url": "https://lkebin.com/helloworld.png"
			}
		}
	}`
	var profile Profile
	json.Unmarshal([]byte(j), &profile)
}

Gist 有完整的代码