06 結構

Go 的設計者對物件導向程式設計(object-oriented programming)的看法跟一般認知的 OOP 不大相同。

Go 沒有類別和繼承機制,但是有結構(struct),而且:

  • 我們可以將任何函式附加(attach)至同一個 package 中的任何具象型別。換言之,如果函式和型別隸屬不同 package,那就不行。比如說,我們不能函式附加至 Go 標準函式庫的 time.Duration
  • 型別能夠隱含地實作介面(無需明白宣告欲實作哪個介面)。

範例一:宣告一個結構型別

以下範例先定義了一個 person 結構,然後在程式中使用它。

type Person struct {
    name string
    age  int
}

func main() {
    fmt.Println("Hello, World!")

    james := Person{
        name: "James",
        age:  25,
    }
    fmt.Println(james.name, james.age)
}

注意:

  • age 成員賦值的時候,最後的逗號不可省略,否則編譯器會視為語法錯誤。這是 Go 設計者貼心的地方。
  • 這裡的結構型別 Person 是以英文大寫開頭,表示可以公開給其他套件使用。如果要限定同一套件才能使用,則名稱必須改為小寫開頭的 person

如果使用 new 來建立結構,會得到一個指向結構的指標;而使用 & 運算子也同樣會得到指向結構的指標。參考下範例所示:

var p1 *Person = new(Person)
p2 := new(Person)
p3 := &Person{}

fmt.Printf("%T\n", p1)  // 輸出 p1 的型別名稱
fmt.Printf("%T\n", p2)  // 輸出 p2 的型別名稱
fmt.Printf("%T\n", p3)  // 輸出 p3 的型別名稱

這裡的 p1p2p3 都是指向一個新建立的 Person 結構的指標,所以三次輸出的型別名稱都一樣是 *main.Person

範例二:使用匿名型別的結構

以下範例展示了如何使用匿名型別的結構,並且直接初始化。

func main() {
    james := struct {
        name string
        age  int
    }{
        name: "James",
        age:  25,
    }
    fmt.Println(james.name, james.age)
}

顯然,如果同樣的結構要使用很多次,應該使用範例一的寫法,也就是預先定義結構型別。

範例三:結構的欄位也可以是函式

func main() {
    animal := struct {
        name string
        speak func() string
    } {
        name: "cat",
        speak: func() string {
            return "meow"
        },
    }

    fmt.Println(fmt.Sprintf("動物名稱是 %s,牠說 %s", animal.name, animal.speak() ))
}

範例四:為結構附加方法

範例三的寫法是把函式加入結構的成員,這裡要示範的寫法有點像是替既有結構額外附加(擴充)一個方法。

type Animal struct {
    name string
}

func (a Animal) speak() string {
    switch a.name {
    case "cat":
        return "meow"
    case "dog":
        return "woof"
    default:
        return "nondescript animal noise?"
    }
}

func main() {
    a := Animal{
        name: "cat",
    }

    fmt.Println(a.speak())

    a.name = "dog"
    fmt.Println(a.speak())

    a.name = "llama"
    fmt.Println(a.speak())#
}

func (a Animal) speak() string { 這樣的寫法稱為 "a method with a receiver"。事實上,「方法」(method)這個名詞在 Go 語言中是有正式定義的:

A method is a function with a receiver.

參見 The Go Programming Language Specification: Method declarations

剛才的範例中,每次呼叫 a.speak() 時傳入的參數 a 都是一個新副本。如果想要讓 speak() 方法中修改原始傳入的 a 結構的內容,就要宣告成指標,像這樣:

func (a *Animal) speak() string {
    ...
}

這裡只需要修改一行程式碼而已,其他地方不變。

重點整理:

  • 方法(methods)是帶有一個 receiver 的函式,而 receiver 是寫在函式名稱前面的一個特殊參數,該參數的型別則表明了這是哪個型別的方法。
  • Receiver 有兩種:pointer receiver 和 value receiver。前者可以修改傳入物件的內容,後者不行。

熟悉物件導向程式語言的人可以把 receiver 參數理解為 thisself,即「當前的物件本身」。

範例五:結構成員可以匿名

type Animal struct {
    string
}

欲存取沒有名稱的欄位,必須使用欄位的型別:

func main() {
    a := Animal{
        "cat",
    }

    func (a Animal) speak() {
        log.Println(a.string)
    }
    fmt.Println(a.speak())

    a.string = "dog"
    fmt.Println(a.speak())
}

由於匿名欄位只能以其型別來存取,故這種寫法有個限制:只能有一個匿名欄位。

如果有給欄位命名,那麼即使只有一個欄位,也必須以名稱來存取該欄位,而不能用型別。

範例六:結構中的 tags

結構的欄位可以附加額外的描述資訊(metadata),稱為「標籤」(tags)。

Tags 的寫法是用一對 backtick 字元 ( ` ) 包住一組或多組 key: "value" 字串。每一組 key-value pair 是以空白字元隔開。

範例:

type Animal struct {
    name string `help: "動物的種類或名稱,只要是貓或狗就行。"`
}

這裡替 name 欄位加上了一個 tag。該 tag 的 key 是 help,而 value 是 "動物的...."

以下示範如何讀取欄位的 tag 內容:

func (a Animal) speak() string {
    switch a.name {
    case "cat":
        return "meow"
    case "dog":
        return "woof"
    default:
        if member, ok := reflect.TypeOf(a).FieldByName("name"); ok {
            return fmt.Sprintf("無效的動物名稱:%s", member.Tag.Get("help"))
        }
        return "nondescript animal noise?"
    }
}

這裡使用了 Go 的 reflection 套件來取得結構的執行時期型別資訊,並以 FieldByName 來取得結構成員。取得結構成員之後,便可以透過它的 Tag.Get("help") 方法來取得 tag key 為 "help" 的內容。

範例七:將 tags 用於 JSON 序列化

package main

import (
    "fmt"
    "encoding/json"
)

type Animal struct {
    Name string `json:"animal_name"`
    ScientificName string `json:"scientific_name"`
    Weight float32 `json:"animal_average_weight"`
}

func main() {
    a := Animal{
        Name: "cat",
        ScientificName: "Felis catus",
        Weight: 10.5,
    }

    output, err := json.Marshal(a)
    if err != nil {
        panic("couldn't encode json")
    }
    fmt.Println(string(output))
}

請注意這裡的 Animal 結構的所有欄位成員的名稱開頭第一個字元都是大寫英文字母,表示它們是公開給任何程式碼存取。如果欄位名稱以小寫英文字母開頭,將導致 encoding/json 套件的函式無法存取它們。

程式的執行結果如下:

{"animal_name":"cat","scientific_name":"Felis catus","animal_average_weight":10.5}

先這樣,也許有空時會再更新。   我的其他站點:      

Last modified: 2024-09-09