Golang 筆記 2 函數、結構體、接口、指針

一、函數

  Go中函數是一等(first-class)類型。我們可以把函數當作值來傳遞和使用。Go中的函數可以返回多個結果。
  函數類型字面量由關鍵字func、由圓括號包裹聲明列表、空格以及可以由圓括號包裹的結果聲明列表組成。其中參數聲明列表中的單個參數聲明之間是由英文逗號分隔的。每個參數聲明由參數名稱、空格和參數類型組成。參數聲明列表中的參數名稱是可以被統一省略的。結果聲明列表的編寫方式與此相同。結果聲明列表中的結果也是可以被省略的,並且在只有一個無名稱的結果聲明時還可以省略括號。例:

func (input1 string, input2 string) 
string

  這一類型字面量表示了一個接受兩個字符串類型的參數且會返回一個字符串類型的結果的函數。如果我們在它的左邊加入type關鍵字和一個標識符作爲名稱的話就變成了一個函數類型聲明:

type MyFunc func(input1 string, input2 string) 
string

  函數值(或簡稱函數)的寫法與此不完全相同。編寫函數的時候需要先寫關鍵字func和函數名稱,後跟參數聲明列表和結果聲明列表,最後是花括號包裹的語句列表。例如:

func myFunc(part1 string, part2 string) 
(result string) {
    result = part1 + part2
    return
}

  如果結果聲明是帶名稱的,那麼它就相當於一個已被聲明但未被顯示賦值的變量。我們可以爲它賦值且在return語句中省略掉需要返回的結果值。該函數還有一種更常規的寫法:

func myFunc(part1 string, part2 string) 
string {
    return part1 + part2
}

  函數myFunc是函數類型MyFunc的一個實現。實際上,只要一個函數的參數聲明列表和結果聲明列表中的數據類型順序和名稱與某一個函數類型完全一致,前者就是後者的一個實現。
  我們可以聲明一個函數類型的變量,如:

var splice func(string, string) string // 等價於var splice MyFunc

  然後把函數myFunc賦給它:

splice = myFunc

  如此一來,我們就可以在這個變量之上實施調用動作了:

splice("1", "2")

  實際上,這是一個調用表達式。上面的代碼可以簡化爲:

var splice = func(part1 string, part2 string) 
string {
    return part1 + part2
}

  在這個示例中,我們直接使用了一個匿名函數來初始化splice變量。顧名思義,匿名函數就是不帶名稱的函數值。匿名函數直接由函數類型字面量和由花括號包裹的語句列表組成。注意,這裏的函數類型字面量中的參數名稱是不能被省略的。
  還可以進一步簡化--省去splice變量。例:

var result = func(part1 string, part2 string)
string {
    return part1 + part2
}("1", "2");

  函數類型的零值是nil。

二、結構體和方法

  Go語言的結構體類型Struct比函數類型更靈活。它可以封裝屬性和操作。前者即是結構體類型中的字段,而後者則是結構體類型所擁有的方法。
  結構體類型的字面量由關鍵字type、類型名稱、關鍵字struct以及花括號包裹的若干字段聲明組成,其中,每個字段聲明獨佔一行並由字段名稱(可選)和字段類型組成。例:

type Person struct {
    Name string
    Gender string
    Age uint8
}

  結構體類型Person中有三個字段,分別是Name、Gender和Age。我們可以用字面量創建出一個該類型的值,像這樣:

Person{Name:"Robert",Gender:"Male",Age:33}

  如果這裏鍵值對的順序與其類型中的字段聲明完全相同的話,可以統一省略所有字段的名稱,就像這樣:

Person{"Robert", "Male", 33}

  我們在編寫某個結構體類型的值字面量時可以只對它的部分字段賦值,甚至不對它的任何字段賦值。這時未被顯式賦值的字段的值則爲其類型的零值。
  與代表函數值的字面量相似。我們在寫一個結構體值的字面量時不需要先擬好其類型。這樣的結構體字面量被稱爲匿名結構體。與匿名函數類似,我們在編寫匿名結構體時需要先寫明其類型特徵(包含字段聲明),再寫出它的值初始化部分。我們依照結構體類型Person創建一個匿名結構體:

p := struct {
    Name string
    Gender string
    Age uint8
}{"Robert", "Male", 33}

  匿名結構體最大的用處是在內部臨時創建一個結構以封裝數據,而不必正式爲其聲明相關規則。
  結構體類型可以擁有若干方法(匿名結構體是不可能擁有方法的)。所謂方法,其實就是一種特殊的函數。它可以依附於某個自定義類型。方法的特殊在於它的聲明包含了一個接收者聲明。這裏的接收者指代它所依附的那個類型。以結構體類型Person爲例,下面是依附於它的一個名爲Grow的方法的聲明:

func (person *Person) Grow() {
    person.Age++
}

  如上所示,在關鍵字func和名稱Grow之間的那個圓括號及其包括的內容就是接收者聲明。其中的內容由兩部分組成。第一部分是代表它所依附的那個類型的值的標識符。第二部分是它依附的那個類型的名稱。後者表明了依附關係,而前者則使得在該方法中的代碼可以使用到該類型的值(也稱爲當前值)。代表當前值的那個標識符可被稱爲接收者標識符,或簡稱爲接收者。例:

p := Person{"Robert", "Male", 33}
p.Grow()

  我們可以直接在Person類型的變量p之上應用調用表達式來調用它的方法Grow。注意,此時方法Grow的接收者標識符person指代的正是變量p的值。這也是“當前值”這個詞的由來。在Grow方法中,我們通過使用選擇表達式選擇了當前值的字段Age。並使其自增。因此,在語句p.Grow()被執行之後,p所代表的那個人就又年長了一歲(p的Age字段的值已變成34)。
  在Grow方法的接收者聲明中的那個類型是*Person,而不是Person。實際上,前者是後者的指針類型。這也使得person指代的是p的指針,而不是它本身。   結構體類型屬於值類型。它的零值並不是nil。而是其中字段的值爲相應類型的零值的值。結構體類型Person的靈值若用字面量來表示的話是Person{}

三、接口

  在Go語言中,一個接口類型總是代表着某一種類型(即所有實現它的類型)的行爲。一個接口類型的聲明通常會包含關鍵字type、類型名稱、關鍵字interface以及由花括號包裹的若干方法聲明。例:

type Animal interface {
    Grow()
    Move(string) string
}

  接口類型中的方法聲明是普通的方法聲明的簡化形式。它們只包括方法名稱、參數聲明列表和結果聲明列表。其中的參數的名稱和結果的名稱都可以被省略。未省略應該是這樣的:

Move(new string) (old string)

  如果一個數據類型所擁有的方法集合中包含了某一個接口類型中的所有方法聲明的實現,那麼就可以說這個數據類型實現了那個接口類型。所謂實現了一個接口中的方法是指,具有與該方法相同的聲明並且添加了實現部分(由花括號包裹的若干條語句)。相同的方法聲明意味着完全一致的名稱、參數類型列表和結果類型列表。其中,參數類型列表即爲參數聲明列表中除去參數名稱的部分。一致的參數類型列表意味着其長度以及順序的完全相同。對於結果類型列表也是如此。   我們無需在一個數據類型中聲明它實現了哪個接口。只要滿足了"方法集合爲超集"的條件,就建立了“實現”關係。   現在我們已經認爲*Person類型實現了Animal接口。但是Go語言編譯器是否也這麼認爲呢?這顯然需要一種顯式的判定方法。在Go語言中,這種判定可以用類型斷言來實現。不過,在這裏,我們不能在一個非接口類型的值上應用類型斷言來判定它是否屬於某一個接口類型。我們必須先把前者轉換成空接口類型的值。這就涉及到了Go的類型轉換。
  Go語言的類型轉換規則定義了是否能夠以及怎樣把一個類型的值轉換另一個類型的值。另一方面,所謂空接口類型即是不包含任何方法聲明的接口類型,用interface{}表示,常簡稱爲空接口。Go中的包含預定義的任何數據類型都可以被看作是空接口的實現。我們可直接使用類型轉換表達式把一個*Person類型轉換成空接口類型的值,例:

p := Person{"Robert", "Male", 33, "Beijing"}
v := interface{}(&p)

  在類型字面量後跟由圓括號包裹的值(或能夠代表它的變量、常量或表達式)就構成了一個類型轉換表達式,意爲將後者轉換爲前者類型的值。在這裏,我們把表達式,意爲將後者轉換爲前者類型的值。在這裏,我們把表達式&p的求值結果轉換成了一個空接口類型的值,並由變量v代表。表達式&p(&是取地址符)的求值結果是一個*Person類型的值,及p的指針。
  在這之後,我們就可以在v上應用類型斷言了,即:

h,ok := v.(Animal)

  類型斷言表達式v.(Animal)的求值結果可以有兩個。第一個結果是被轉換後的那個目標類型(這裏是Animal)的值,而第二個結果則是轉換操作成功與否的標誌。顯然,ok代表了一個bool類型的值。它也是這裏判定實現關係的重要依據。

四、指針

  指針操作涉及到兩個操作符--&和*。這兩個操作符均有多個用途。但是當它們作爲地址操作符出現時,&的作用是取址,*的作用是取值。   當*出現在一個類型之前(如*Person和*[3]string)時就不能被看作是操作符了,而應該被視爲一個符號。如此組合而成的標識符所表達的含義是作爲第二部分的那個類型的指針類型。我們也可以把其中的第二部分所代表的類型稱爲基底類型。   現在後頭看結構體類型Person。它及其兩個方法的完整聲明如下:

type Person strcut {
    Name    string
    Gender  string
    Age     uint8
    Address string
}

func (person *Person) Grow() {
    person.Age++
}

func (person *Person) Move(newAddres string) string {
    old := person.Address
    person.Address = newAddress
    return old
}

  注意,Person的兩個方法Grow和Move的接收者類型都是*Person,而不是Person。只要一個方法的接收者類型是其所屬類型的指針類型而不是該類型本身,那麼就可以稱該方法爲一個指針方法。上面的Grow方法和Move方法都是Person類型的指針方法。
  相對的,如果一個方法的接收者類型就是其所屬的類型本身。那麼我們就可以把它叫做值方法。

func (person Person) Grow() {
    person.Age++
}

  方法的接收者標識符所代表的是該方法當前所屬的那個值的一個副本,而不是該值本身。   之所以表達式person.Age成立,是因爲如果Go語言發現person是指針並且指向的那個值有Age字段,那麼就會把該表達式視爲(*person).Age。

  擁有指針方法Grow和Move的指針類型*Person是接口類型Animal的實現類型,但是它的基底類型Person卻不是。這樣的表象隱藏了另一條規則:一個指針類型擁有以它以及以它的基底類型爲接收者類型的所有方法,而它的基底類型卻至擁有以它本身爲接收者類型的方法。

  我們在基底類型的值上仍然可以調用它的指針方法。例如,若我們有一個Person類型的變量bp,則調用表達式bp.Grow()是合法的。這是因爲,如果Go語言發現我們調用的Grow方法是bp的指針方法,那麼它會把調用表達式是爲(&bp).Grow()。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章