高級JSON定制化
通過使用結構體標簽、添加空白和封裝響應數據氛改,我們已經能夠為JSON響應添加大量定制信息。但是撩独,當這些內容還不夠時敞曹,您需要更自由地定制JSON時账月,會發(fā)生什么呢?
要回答這個問題,我們首先需要談談Go如何處理JSON序列化的一些理論澳迫。要理解的關鍵是:
Go是在什么時候將特殊類型序列化為JSON局齿,它首先查看對應的類型是否實現了MarshalJSON()方法。如果實現了橄登,GO將調用這個方法來決定JSON編碼格式抓歼。
這么講有點模糊,我們更精確點拢锹。嚴格地說谣妻,當Go將特定類型編碼為JSON時,它會查看該類型是否滿足json.Marshaler接口卒稳,該接口如下所示:
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
如果類型確實滿足接口蹋半,那么Go將調用它的MarshalJSON()方法,并使用它返回的[]byte切片作為JSON編碼的值充坑。
如果該類型沒有MarshalJSON()方法减江,那么Go將返回嘗試根據自己的內部規(guī)則將其編碼為JSON。
因此捻爷,如果我們想定制某些類型的編碼方式辈灼,只需要在其上實現MarshalJSON()方法,該方法以[]byte類型返回自定義的JSON內容也榄。
提示:如果您查看time.Time類型源代碼巡莹,就可以看到這一點。time.Time實際上是一個結構體手蝎,但是它有一個MarshalJSON()方法榕莺,輸出RFC3339格式JSON對象。當time.Time值被序列化為JSON對象時棵介,就會調用MarshalJSON()方法钉鸯。
定制電影Runtime字段JSON序列化
為了說明這一點,讓我們看一下應用程序中的一個具體示例邮辽。
當我們的Movie結構被編碼為JSON時唠雕,Runtime字段(它是一個int32類型)編碼為JSON數字。現在我們來更改它吨述,將其編碼為"<runtime> mins“的字符串岩睁。像這樣:
{
"id": 123,
"title": "Casablanca",
"runtime": "102 mins",
"genres":
[
"drama",
"romance",
"war"
],
"version": 1
}
有幾種方法可以實現這一點,但一種簡單的方法是為Runtime字段創(chuàng)建一個自定義類型揣云,并在這個類型上實現MarshalJSON()方法捕儒。
為了防止internal/data/movie.go文件不會太亂,我們創(chuàng)建一個新的文件來處理runtime類型序列化邏輯:
$ touch internal/data/runtime.go
然后繼續(xù)添加以下代碼:
package data
import (
"fmt"
"strconv"
)
//申明Runtime類型,其底層是int32類型(和movie中的字段一樣)
type Runtime int32
//實現MarshalJSON()方法刘莹,這樣就實現了json.Marshaler接口阎毅。
func (r Runtime) MarshalJSON() ([]byte, error) {
//生成一個字符串包含電影時長
jsonValue := fmt.Sprintf("%d mins", r)
//使用strconv.Quote()函數封裝雙引號。為了在JSON中以字符串對象輸出点弯,需要用雙引號扇调。
quotedJSONValue := strconv.Quote(jsonValue)
//將字符串轉為[]byte返回
return []byte(quotedJSONValue)
}
這里我想強調兩點:
- 如果您的MarshalJSON()方法像我們的方法一樣返回一個JSON字符串值,那么您必須在返回字符串之前用雙引號包裝它抢肛。否則它將不會被解釋為JSON字符串狼钮,你將收到類似于這樣的運行時錯誤:
json: error calling MarshalJSON for type data.Runtime: invalid character 'm' after top-level value
- 我們故意為MarshalJSON()方法使用值接收器,而不是指針接收器func (r *Runtime) MarshalJSON()捡絮。這給了我們更多的靈活性熬芜,因為這意味著定制JSON編碼將對Runtime值對象和指針對象都有效。正如Effective Go提到的:
如果你不確定指針和值接收器之間的區(qū)別锦援,那么這篇博客提供了一個很好的總結猛蔽。
好的,現在有了自定義Runtime類型灵寺,打開internal/data/movies.go文件并更新Movie結構:
File: internal/data/movies.go
package data
import (
"time"
)
type Movie struct {
ID int64 `json:"id"`
CreateAt time.Time `json:"-"`
Title string `json:"title"`
Year int32 `json:"year,omitempty"`
//使用Runtime類型取代int32曼库,注意omitempty還是能生效的
Runtime Runtime `json:"runtime,omitempty,string"`
Genres []string `json:"genres,omitempty"`
Version int32 `json:"version"`
}
重啟服務然后對GET /v1/movies/:id接口發(fā)起請求。你應該看到一個包含自定義runtime值的響應略板,格式為"xx mins"毁枯,類似如下:
$ curl localhost:4000/v1/movies/123
{
"movie":
{
"id": 123,
"title": "Casablanca",
"runtime": "102 mins",
"genres":
[
"drama",
"romance",
"war"
],
"version": 1
}
}
總之,這是定制JSON序列化的一種很好的方法叮称。我們的代碼簡潔明了种玛,并且我們有一個自定義的Runtime類型,可以隨時隨地使用它瓤檐。
但也有不利的一面赂韵。在將代碼與其他包集成時,使用自定義類型有時會很尷尬挠蛉,您可能需要執(zhí)行類型轉換祭示,將自定義類型轉換為其他包理解和可接受的值。