12.4. 示例: 編碼S表達式

Display是一個用於顯示結構化數據的調試工具,但是它併不能將任意的Go語言對象編碼爲通用消息然後用於進程間通信。

正如我們在4.5節中中看到的,Go語言的標準庫支持了包括JSON、XML和ASN.1等多種編碼格式。還有另一種依然被廣泛使用的格式是S表達式格式,采用類似Lisp語言的語法。但是和其他編碼格式不同的是,Go語言自帶的標準庫併不支持S表達式,主要是因爲它沒有一個公認的標準規范。

在本節中,我們將定義一個包用於將Go語言的對象編碼爲S表達式格式,它支持以下結構:

42          integer
"hello"     string (with Go-style quotation)
foo         symbol (an unquoted name)
(1 2 3)     list   (zero or more items enclosed in parentheses)

布爾型習慣上使用t符號表示true,空列表或nil符號表示false,但是爲了簡單起見,我們暫時忽略布爾類型。同時忽略的還有chan管道和函數,因爲通過反射併無法知道它們的確切狀態。我們忽略的還浮點數、複數和interface。支持它們是練習12.3的任務。

我們將Go語言的類型編碼爲S表達式的方法如下。整數和字符串以自然的方式編碼。Nil值編碼爲nil符號。數組和slice被編碼爲一個列表。

結構體被編碼爲成員對象的列表,每個成員對象對應一個個僅有兩個元素的子列表,其中子列表的第一個元素是成員的名字,子列表的第二個元素是成員的值。Map被編碼爲鍵值對的列表。傳統上,S表達式使用點狀符號列表(key . value)結構來表示key/value對,而不是用一個含雙元素的列表,不過爲了簡單我們忽略了點狀符號列表。

編碼是由一個encode遞歸函數完成,如下所示。它的結構本質上和前面的Display函數類似:

gopl.io/ch12/sexpr

func encode(buf *bytes.Buffer, v reflect.Value) error {
    switch v.Kind() {
    case reflect.Invalid:
        buf.WriteString("nil")

    case reflect.Int, reflect.Int8, reflect.Int16,
        reflect.Int32, reflect.Int64:
        fmt.Fprintf(buf, "%d", v.Int())

    case reflect.Uint, reflect.Uint8, reflect.Uint16,
        reflect.Uint32, reflect.Uint64, reflect.Uintptr:
        fmt.Fprintf(buf, "%d", v.Uint())

    case reflect.String:
        fmt.Fprintf(buf, "%q", v.String())

    case reflect.Ptr:
        return encode(buf, v.Elem())

    case reflect.Array, reflect.Slice: // (value ...)
        buf.WriteByte('(')
        for i := 0; i < v.Len(); i++ {
            if i > 0 {
                buf.WriteByte(' ')
            }
            if err := encode(buf, v.Index(i)); err != nil {
                return err
            }
        }
        buf.WriteByte(')')

    case reflect.Struct: // ((name value) ...)
        buf.WriteByte('(')
        for i := 0; i < v.NumField(); i++ {
            if i > 0 {
                buf.WriteByte(' ')
            }
            fmt.Fprintf(buf, "(%s ", v.Type().Field(i).Name)
            if err := encode(buf, v.Field(i)); err != nil {
                return err
            }
            buf.WriteByte(')')
        }
        buf.WriteByte(')')

    case reflect.Map: // ((key value) ...)
        buf.WriteByte('(')
        for i, key := range v.MapKeys() {
            if i > 0 {
                buf.WriteByte(' ')
            }
            buf.WriteByte('(')
            if err := encode(buf, key); err != nil {
                return err
            }
            buf.WriteByte(' ')
            if err := encode(buf, v.MapIndex(key)); err != nil {
                return err
            }
            buf.WriteByte(')')
        }
        buf.WriteByte(')')

    default: // float, complex, bool, chan, func, interface
        return fmt.Errorf("unsupported type: %s", v.Type())
    }
    return nil
}

Marshal函數是對encode的保證,以保持和encoding/...下其它包有着相似的API:

// Marshal encodes a Go value in S-expression form.
func Marshal(v interface{}) ([]byte, error) {
    var buf bytes.Buffer
    if err := encode(&buf, reflect.ValueOf(v)); err != nil {
        return nil, err
    }
    return buf.Bytes(), nil
}

下面是Marshal對12.3節的strangelove變量編碼後的結果:

((Title "Dr. Strangelove") (Subtitle "How I Learned to Stop Worrying and Lo
ve the Bomb") (Year 1964) (Actor (("Grp. Capt. Lionel Mandrake" "Peter Sell
ers") ("Pres. Merkin Muffley" "Peter Sellers") ("Gen. Buck Turgidson" "Geor
ge C. Scott") ("Brig. Gen. Jack D. Ripper" "Sterling Hayden") ("Maj. T.J. \
"King\" Kong" "Slim Pickens") ("Dr. Strangelove" "Peter Sellers"))) (Oscars
("Best Actor (Nomin.)" "Best Adapted Screenplay (Nomin.)" "Best Director (N
omin.)" "Best Picture (Nomin.)")) (Sequel nil))

整個輸出編碼爲一行中以減少輸出的大小,但是也很難閲讀。這里有一個對S表達式格式化的約定。編寫一個S表達式的格式化函數將作爲一個具有挑戰性的練習任務;不過 http://gopl.io 也提供了一個簡單的版本。

((Title "Dr. Strangelove")
 (Subtitle "How I Learned to Stop Worrying and Love the Bomb")
 (Year 1964)
 (Actor (("Grp. Capt. Lionel Mandrake" "Peter Sellers")
         ("Pres. Merkin Muffley" "Peter Sellers")
         ("Gen. Buck Turgidson" "George C. Scott")
         ("Brig. Gen. Jack D. Ripper" "Sterling Hayden")
         ("Maj. T.J. \"King\" Kong" "Slim Pickens")
         ("Dr. Strangelove" "Peter Sellers")))
 (Oscars ("Best Actor (Nomin.)"
          "Best Adapted Screenplay (Nomin.)"
          "Best Director (Nomin.)"
          "Best Picture (Nomin.)"))
 (Sequel nil))

和fmt.Print、json.Marshal、Display函數類似,sexpr.Marshal函數處理帶環的數據結構也會陷入死循環。

在12.6節中,我們將給出S表達式解碼器的實現步驟,但是在那之前,我們還需要先了解如果通過反射技術來更新程序的變量。

練習 12.3: 實現encode函數缺少的分支。將布爾類型編碼爲t和nil,浮點數編碼爲Go語言的格式,複數1+2i編碼爲#C(1.0 2.0)格式。接口編碼爲類型名和值對,例如("[]int" (1 2 3)),但是這個形式可能會造成歧義:reflect.Type.String方法對於不同的類型可能返迴相同的結果。

練習 12.4: 脩改encode函數,以上面的格式化形式輸出S表達式。

練習 12.5: 脩改encode函數,用JSON格式代替S表達式格式。然後使用標準庫提供的json.Unmarshal解碼器來驗證函數是正確的。

練習 12.6: 脩改encode,作爲一個優化,忽略對是零值對象的編碼。

練習 12.7: 創建一個基於流式的API,用於S表達式的解碼,和json.Decoder(§4.5)函數功能類似。