
在调用 Yahoo 股票 API 时,同一字段(如 quote)可能返回单个对象或对象数组,Go 的 json.Unmarshal 默认无法自动适配。本文介绍通过自定义 UnmarshalJSON 方法,无需重复定义结构体,即可统一处理两种格式。
在调用 yahoo 股票 api 时,同一字段(如 `quote`)可能返回单个对象或对象数组,go 的 `json.unmarshal` 默认无法自动适配。本文介绍通过自定义 `unmarshaljson` 方法,无需重复定义结构体,即可统一处理两种格式。
当外部 API(如 Yahoo Finance)对同一接口路径返回不同 JSON 结构——单个资源时为对象 { "quote": { ... } },多个资源时为数组 { "quote": [ {...}, {...} ] }——标准 Go 结构体绑定会失败:若字段声明为 StockQuote 类型,则数组反序列化报错;若声明为 []StockQuote,则单对象反序列化失败。
最简洁、可维护的解决方案是为承载字段(如 Quote)实现自定义 UnmarshalJSON 方法,在反序列化过程中动态判断 JSON token 类型({ 或 [),并分别解析为单对象或切片。
以下是一个完整、生产可用的实现示例:
type Quote struct {
Quote *StockQuote `json:"-"` // 仅用于单对象场景(非 JSON 字段)
Quotes []StockQuote `json:"-"` // 仅用于多对象场景(非 JSON 字段)
}
// UnmarshalJSON 实现智能解析:自动识别 quote 是 object 还是 array
func (q *Quote) UnmarshalJSON(data []byte) error {
// 先尝试解析为数组
var arr []StockQuote
if err := json.Unmarshal(data, &arr); err == nil {
q.Quotes = arr
if len(arr) > 0 {
q.Quote = &arr[0] // 兼容单对象访问习惯
}
return nil
}
// 若数组解析失败,尝试解析为单对象
var obj StockQuote
if err := json.Unmarshal(data, &obj); err == nil {
q.Quote = &obj
q.Quotes = []StockQuote{obj}
return nil
}
return fmt.Errorf("failed to unmarshal quote: neither object nor array")
}
type StockQuote struct {
Change string `json:"Change"`
PercentChange string `json:"PercentChange"`
DaysLow string `json:"DaysLow"`
DaysHigh string `json:"DaysHigh"`
Open string `json:"Open"`
PreviousClose string `json:"PreviousClose"`
Symbol string `json:"Symbol"`
Name string `json:"Name"`
Volume string `json:"Volume"`
}
// 嵌套结构保持简洁
type Query struct {
Count int `json:"count"`
Created string `json:"created"`
Lang string `json:"lang"`
Results struct {
Quote Quote `json:"quote"` // ✅ 此处直接使用自定义类型
} `json:"results"`
}
type QueryResult struct {
Query Query `json:"query"`
}✅ 使用方式(完全透明):
var result QueryResult
if err := json.Unmarshal([]byte(singleQuoteJSON), &result); err != nil {
log.Fatal(err)
}
fmt.Printf("Single: %+v\n", result.Query.Results.Quote) // 指向首个报价
fmt.Printf("All: %+v\n", result.Query.Results.Quotes) // 完整切片
if err := json.Unmarshal([]byte(multipleQuotesJSON), &result); err != nil {
log.Fatal(err)
}
// 同样可安全访问 .Quote 和 .Quotes —— 逻辑自动适配⚠️ 注意事项:
- 字段名需严格匹配 API 返回的 key(如 "Change" 而非 "change"),本例中原始 JSON 使用 PascalCase,故结构体 tag 应对应;
- UnmarshalJSON 中先试数组再试对象,符合常见场景(多数 API 优先返回数组,单条为特例);
- 避免在 Quote 中同时暴露 Quote *StockQuote 和 Quotes []StockQuote 作为公共字段,除非业务明确需要双接口;更推荐统一通过 Quotes 访问,并提供 First() 辅助方法;
- 若需高性能(高频调用),可预先检查首字节(data[0])替代 json.Token(),进一步减少开销。
该方案无需拆分结构体、不依赖第三方库、零反射开销,是 Go 生态中处理“弹性 JSON 结构”的标准实践。


















