Go 的高級編碼和解碼技術

靜觀風云 7年前發布 | 7K 次閱讀 Go語言 Google Go/Golang開發

高級編碼和解碼技術

Go 的標準庫包含了一些很不錯的編碼和解碼包,里面涵蓋了大量的編碼方案。一切數據,不管是CSV,XML,JSON,還是 gob —— 一個 Go 特定的編碼格式,都涵蓋在內,并且,這些包都非常容易上手使用。 事實上,它們中的大多數都不需要再添加任何代碼,你只需插入數據,它就會輸出編碼后的數據。

不過,并不是所有的應用程序都樂于處理這種到 JSON 展現的一對一映射。Struct 標記可以涵蓋一些場景中的大多數情況,但如果你使用了很多 API,它的功能還是有限。

例如,你可能會遇到一個 API,它會輸出不同的對象到同一個鍵,使其成為泛型的首選后補對象,但是 Go 并沒有這些東西。或者你也可能會使用一個 API,它可以接收并且返回 Unix 時間 而不是 RFC 3339 格式的時間,雖然我們可以在代碼中將它表達成一個 int,但是如果可以直接以  time 包的  Time 類型來操作,豈非更好?

在這篇文章中,我們將回顧一些技術,它們可以幫助我們將繁瑣的代碼簡化成相對容易處理的代碼。我們會使用 encoding/json 包來做這件事情,而值得注意的是 Go 為大多數編碼類型提供了一個 Marshaler 和 Unmarshaler 接口,讓你可以在多編碼場景中對數據被編碼和解碼的方式進行自定義。

長期有效的新類型方法

我們要檢查的第一種技術是創建一個新類型,并在編碼和解碼之前將數據和這個類型進行轉換。從技術層面說,這不是一個具體的編碼方案,但它非常可靠并易于遵循。它屬于一項基本技術,后面幾節我們也會用到,所以值得你現在花時間看一看。

想像一下,我們的應用是從下面簡單的 Dog 類型開始。

type Dog struct {
  ID      int
  Name    string
  Breed   string
  BornAt  time.Time
}

默認情況下, time.Time 類型按 RFC 3339 格式提供。也就是說,它會是一個字符串,類似于 2016-12-07T17:47:35.099008045-05:00。

此類格式沒有什么特別的問題,但我們可能希望編碼與解碼能處于不同的領域。例如,我們可能會使用 API 發送一個 Unix 時間,并期待同樣格式的響應。

不管怎樣,我們需要有一種辦法來改變它轉換為 JSON 以及從 JSON 解析的方式。解決方法之一是創建一個 JSONDog 的新類型,并使用它來編碼和解碼 JSON 。

type JSONDog struct {
  ID     int    `json:"id"`
  Name   string `json:"name"`
  Breed  string `json:"breed"`
  BornAt int64  `json:"born_at"`
}

現在,如果要把 Dog 類型轉為 JSON,我們只需要將它轉換為 JSONDog 類型,然后使用 encoding/json 包來編排即可。

先從一個接受 Dog 類型參數和返回一個 JSONDog 類型結果的構造函數開始,代碼如下:

func NewJSONDog(dog Dog) JSONDog {
  return JSONDog{
    dog.ID,
    dog.Name,
    dog.Breed,
    dog.BornAt.Unix(),
  }
}

把它和 encoding/json 包整合后:

func main() {
  dog := Dog{1, "bowser", "husky", time.Now()}
  b, err := json.Marshal(NewJSONDog(dog))
  if err != nil {
    panic(err)
  }
  fmt.Println(string(b))
}

從 JSON 解碼為 Dog 類型的過程也類似。先解碼為 JSONDog 類型,然后使用 JSONDog 類型中的 Dog() 方法將它轉換回 Dog 類型。

func (jd JSONDog) Dog() Dog {
  return Dog{
    jd.ID,
    jd.Name,
    jd.Breed,
    time.Unix(jd.BornAt, 0),
  }
}

func main() { b := []byte({ "id":1, "name":"bowser", "breed":"husky", "born_at":1480979203}) var jsonDog JSONDog json.Unmarshal(b, &jsonDog) fmt.Println(jsonDog.Dog()) }</code></pre>

你可以在 Go 演練場看到完整的代碼示例并運行它: https://play.golang.org/p/0hEhCL0ltW

優點:首先,該方法很通用,適合我們構建轉換層的情況。 雖然 JSON 部分看起來不像 Go 代碼,但我們能對其進行轉換。其次,其代碼極容易理解,新手也能很好掌握。

但這種方式也是有缺點的,主要體現在兩方面:

1、開發人員容易忘記將 Dog 類型轉換成 JSONDog 類型

2、它包含很多額外的代碼

不過不要灰心,讓我們來看看如何在保持代碼清晰度的前提下解決這兩個問題。

實現 Marshaler 和 Unmarshaler 接口

用最后一種方法會很容易忘記將 Dog 轉換為 JSONDog,因此在本節中,我們將討論如何在 encoding / json 包中實現 Marshaler 和 Unmarshaler 接口,以使轉換自動化。

這兩個接口的工作方式很簡單;當 encoding/json 包遇到一個實現了 Marshaler 接口的類型時,它使用了 MarshalJSON() 的方法代替默認的 marshaling 代碼,將對象轉換成 JSON。同樣地,當解碼 JSON 對象時,它將測試該對象是否實現了 Unmarshaler 接口,如果是這樣,它會使用 UnmarshalJSON() 方法代替默認的 unmarshaling 行為。我們只需確保 Dog 類型能進行編碼與反編碼,因為 JSONDog 能幫我們實現這兩個方法并做轉換。

我們先從編碼開始,在 Dog 類型上實現 MarshalJSON() ([]byte, error) 方法。

雖然第一印象是要做好多事情,但實際上我們可以利用已經存在的代碼,這樣需要我們寫的代碼就不多了。我們真正需要在這個方法里做的事情只是對當前 Dog 對象的 JSONDog 描述調用 json.Marshal() 方法并返回結果。

func (d Dog) MarshalJSON() ([]byte, error) {
  return json.Marshal(NewJSONDog(d))
}

現在即使開發忘記將 Dog 類型轉換為 JSONDog  類型也沒關系了,這件事情會在 Dog 轉換為 JSON 的時候默認進行。

Unmarshaler 的最終實現非常相似。我們準備實現 UnmarshalJSON([]byte) error 方法,并再一次利用 JSONDog 類型。

func (d *Dog) UnmarshalJSON(data []byte) error {
  var jd JSONDog
  if err := json.Unmarshal(data, &jd); err != nil {
    return err
  }
  *d = jd.Dog()
  return nil
}

最后我們修改一下 main() 函數,在編碼和解碼時使用 Dog 類型而不是 JSONDog 類型。

func main() {
  dog := Dog{1, "bowser", "husky", time.Now()}
  b, err := json.Marshal(dog)
  if err != nil {
    panic(err)
  }
  fmt.Println(string(b))

b = []byte({ "id":1, "name":"bowser", "breed":"husky", "born_at":1480979203}) dog = Dog{} json.Unmarshal(b, &dog) fmt.Println(dog) }</code></pre>

我們大約只用10行代碼就對 Dog 類型重寫了默認的 JSON 編碼方法,相當簡潔,不是嗎?

接下來,我們將著手解決使用嵌入數據和別名類型的初始方法中的其它問題。

使用嵌入的數據和別名類型簡化代碼

注意:這里提到的“別名”與 Go 1.9 的別名提議不同。這個“別名”只是簡單的指向某個新類型,它具有與另一種類型相同的數組,但有不同的方法集。

正如我們之前所見,在把所有數據從一種類型復制到另一種類型的時候,定義字段的過程相當乏味。進一步放大來看,如果我們要處理擁有10個或20個字段的對象,保持 JSONDog 和 Dog 類型同步的過程就會讓人覺得厭煩。

幸好有另一種方法來解決這個問題,它可以減少需要我們定義的字段,只處理那些需要定義編輯和解碼的字段。我們會把 Dog 對象嵌入到 JSONDog 中去,然后自定義一些需要自定義的字段。

一開始需要更新 Dog 類型,為其加入 JSON 標簽,這些標簽加在需要自定義的字段后面。然后 我們將告訴 / json 包忽略字段,通過使用結構標簽  json:"-" 來提醒 JSON 編碼器應該忽略這個字段,即使它被導出。

type Dog struct {
  ID     int       `json:"id"`
  Name   string    `json:"name"`
  Breed  string    `json:"breed"`
  BornAt time.Time `json:"-"`
}

接下來,我們把 Dog 類型嵌入到 JSONDog  類型中,并更新 NewJSONDog() 函數和 JSONDog 類型的 Dog() 方法。我們臨時把 Dog() 方法改名為 ToDog(),避免與內部的 Dog 對象沖突。

警告:代碼現在不能工作,我展示了中間過程來說明其原因。

func NewJSONDog(dog Dog) JSONDog {
  return JSONDog{
    dog,
    dog.BornAt.Unix(),
  }
}

type JSONDog struct { Dog BornAt int64 json:"born_at" }

func (jd JSONDog) ToDog() Dog { return Dog{ jd.Dog.ID, jd.Dog.Name, jd.Dog.Breed, time.Unix(jd.BornAt, 0), } }</code></pre>

它可以編譯,但如果嘗試運行的話會產生一個致命錯誤:堆棧溢出。這發生在調用 Dog 類型的 MarshaJSON() 方法的時候。當函數調用的時候,它會構造一個 JSONDog,但是這個對象內部有一個 Dog 對象,構造 Dog 對象的時候又會構造新的 JSONDog,這就產生了一個無限循環,直到程序崩潰。

為了避免這種情況發生,我們需要創建一個 Dog 類型的別名,它不包含 MarshalJSON() 和 UnmarsshalJSON() 方法。

type DogAlias Dog

擁有了別名類型之后,就可以更新 JSONDog 類型,用它來代替 Dog 類型。我們也需要更新 NewJSONDog(),將 Dog 改為 DogAlias,然后可以清理一下 JSONDOg 類型的 Dog() 方法,將內部的 Dog 作為返回值。

func NewJSONDog(dog Dog) JSONDog {  return JSONDog{
    DogAlias(dog),
    dog.BornAt.Unix(),
  }
}type JSONDog struct {
  DogAlias
  BornAt int64 `json:"born_at"`}func (jd JSONDog) Dog() Dog {
  dog := Dog(jd.DogAlias)
  dog.BornAt = time.Unix(jd.BornAt, 0)  return dog
}

如你所見,初始設置需要大約花了30行代碼,但現在我們已經設置好了,Dog 類型中有多少字段并不重要。JSON 代碼只會在需要自定義 JSON 的字段增加時才會增長。

特定字段的自定義類型

上一節提到的方法重點關注了在編碼和解碼之前將整個對象轉換為另一種類型。但即使使用了嵌入的別名,我們仍然會需要對每個具有 time.Time 字段的不同類型的對象重復這段代碼。

本節我們會著眼一種方法,使我們能夠定義我們需要的單次編碼或解碼的類型。然后我們會在整個程序中復用這個類型。回到最初的示例,從需要為 BornAt 字段定義 JSON 的 Dog 類型開始。

type Dog struct {
  ID     int       `json:"id"`
  Name   string    `json:"name"`
  Breed  string    `json:"breed"`
  BornAt time.Time `json:"born_at"`}

我們已經知道這不能工作,所以與其使用 time.Time 類型,不如創建自己的 Time 類型,并在其中嵌入 time.Time。現在使用我們新建的 Time 類型更新 Dog 類型。

type Dog struct {
  ID     int    `json:"id"`
  Name   string `json:"name"`
  Breed  string `json:"breed"`
  BornAt Time   `json:"born_at"`}type Time struct {
  time.Time
}

接著,我們開始寫為 Time 類型定義的 MarshalJSON() 和 UnmarshalJSON() 方法。新方法分別輸出 Unix 時間,或從 Unix 時間解析。

func (t Time) MarshalJSON() ([]byte, error) {
  return json.Marshal(t.Time.Unix())
}

func (t *Time) UnmarshalJSON(data []byte) error { var i int64 if err := json.Unmarshal(data, &i); err != nil { return err } t.Time = time.Unix(i, 0) return nil }</code></pre>

就是這樣!我們現在可以在所有結構使用新 Time 類型,它會編碼成 Unix 時間或者從 Unix 時間解碼。最重要的是,因為嵌入了 time.Time 對象,我們甚至可以在 Time 類型中隨意使用 likeDay() 方法,這意味著我們寫的代碼不需要重寫。

這種方法也有缺點。由于采用了一種新類型,我們會破壞那些期望使用 time.Time 而不是我們新定義的 Time 類型的代碼。你可以更新所有代碼以使用新類型,或者也可以訪問嵌入的 time.Time 對象,這可能要求一些重構。

對于這個問題,還有一種方案是將這個方法與我們第一次討論的方法結合起來,同時取兩者的優點 —— Dog 類型擁有一個 time.Time 對象,但 JSONDog 不需要在兩種類型轉換中操心過多細節。所有轉換邏輯都已經包含在的 Time 類型中了。

對泛型進行編碼和解碼

我們要看的最后一種技術與前兩種略有不同,因為它解決的問題與前兩者完全不同——保存在嵌套 JSON 中的動態類型。

例如,假如你想從服務器獲得下面的 JSON 響應:

{
  "data": {
    "object": "bank_account",
    "id": "ba_123",
    "routing_number": "110000000"
  }
}

從同一個終端你可以收到這樣的信息:

{
  "data": {
    "object": "card",
    "id": "card_123",
    "last4": "4242"
  }
}

乍一看這兩條數據與很相似,但它們是完全不同的對象。你可以用銀行賬戶做什么和可以用卡做什么是完全不同的,在這里根本看不出來,但它們都可能用于差異較大的不同領域。

解決這個問題的方案之一是使用泛型,并在解析 JSON 的時候設置類型。你必須使用反射庫,它在某種語言,如 Java 中,會有一些類,如下所示:

class Data<T> {
  public T t;
}
class Card {...}
class BankAccount {...}

然而,Go 沒有泛型,那么應該如何解析這個 JSON?

一種辦法是使用鍵為字符串的映射表,但是值應該用什么類型?就算我們假設它是一個嵌套的映射表,如果卡對象包含整數的時候會發生什么事件,如果有嵌套的 JSON 對象又會發生什么事件?

我們的選擇確實很受限,但基本上我們會采用空接口(interface {}) 來解決。

func main() {
  jsonStr := {
  "data": {
    "object": "card",
    "id": "card_123",
    "last4": "4242"
  }
}
  var m map[string]map[string]interface{}
  if err := json.Unmarshal([]byte(jsonStr), &m); err != nil {
    panic(err)
  }
  fmt.Println(m)

b, err := json.Marshal(m) if err != nil { panic(err) } fmt.Println(string(b)) }</code></pre>

使用空接口類型會帶來相應的設置問題,最值得注意的是,空接口不會提供數據信息。如果我們想要知道數據存儲對應的鍵,我們需要做一種斷言,但這很不方便。慶幸地是,還有其他的方法能解決這個問題!

這種方法需要再次利用 Marshaler 和 Unmarshaler 接口,但這次需要添加一些條件邏輯代碼,還要使用指向 Card 類型和指向 BankAccount 類型的指針。開始解碼 JSON 時,我們將首先解碼這兩個字段的對象,以確定哪些關鍵字段需要我們填滿,之后再填補上去。

接下來,我們開始聲明類型。BankAccount 和 Card 類型是很簡單的,我們要將 JSON 直接映射成 Go 的結構體。

type BankAccount struct {
  ID            string json:"id"
  Object        string json:"object"
  RoutingNumber string json:"routing_number"
}

type Card struct { ID string json:"id" Object string json:"object" Last4 string json:"last4" }</code></pre>

然后我們就有了自己的數據類型。你可以對它自定義命名,使用 Source 或者 CardOrBankAccount類似的名字會比較好區分。在此我還是使用 Data。

type Data struct {
  *Card
  *BankAccount
}

我在這里使用指針是因為我們不會對這兩個數據進行初始化,而是選擇其中一個。而你要先確定代碼中確實用到了這種類型,然后寫一些類似于 if data.Card != nil{...} 的指令來判斷當前數據是否是 Card 數據。當然,你也可以將對象的屬性存儲在數據類型上,但需要注意一些代碼的調整 。

現在我們擁有一個 Data 類型的結構,我們需要繼續完善 main() 方法,使得將對象映射到 JSON 中的過程更明晰:

func main() {
  jsonStr := {
  "data": {
    "object": "card",
    "id": "card_123",
    "last4": "4242"
  }
}
  var m map[string]Data
  if err := json.Unmarshal([]byte(jsonStr), &m); err != nil {
    panic(err)
  }
  fmt.Println(m)
  data := m["data"]
  if data.Card != nil {
    fmt.Println(data.Card)
  }
  if data.BankAccount != nil {
    fmt.Println(data.BankAccount)
  }

b, err := json.Marshal(m) if err != nil { panic(err) } fmt.Println(string(b)) }</code></pre>

Data 數據并不代表完全的 JSON 結構,而是代表所有存儲在 JSON 對象中的鍵。在我們的代碼中,Data 類型擁有 Card 和 BankAccount 兩個指針成員,但是在 JSON 中它們不再是嵌套的對象。這就意味著我們需要寫一個 MarshalJSON() 方法去反射它:

func (d Data) MarshalJSON() ([]byte, error) {
  if d.Card != nil {
    return json.Marshal(d.Card)
  } else if d.BankAccount != nil {
    return json.Marshal(d.BankAccount)
  } else {
    return json.Marshal(nil)
  }
}

這段代碼首先檢查我們是否擁有一個 Card 或者 BankAccount 對象。如果有,它將會呈現在 JSON 中相應的對象上。如果兩個都沒有,它將會以 nil 的形式呈現在 JSON 中,nil 在 JSON 中為 null。

 

來自:https://www.oschina.net/translate/advanced-encoding-decoding

 

 本文由用戶 靜觀風云 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!