1. 程式人生 > >Go 系列教程-5 基礎知識

Go 系列教程-5 基礎知識

Go 系列教程 —— 15. 指標

什麼是指標?

指標是一種儲存變數記憶體地址(Memory Address)的變數。

指標示意圖

如上圖所示,變數 b 的值為 156,而 b 的記憶體地址為 0x1040a124。變數 a 儲存了 b 的地址。我們就稱 a 指向了 b

指標的宣告

指標變數的型別為 *T,該指標指向一個 T 型別的變數。

接下來我們寫點程式碼。

package main

import (
    "fmt"
)

func main() {
    b := 255
    var a *int = &b
    fmt.Printf("Type of a is %T\n", a)
    fmt.Println("address of b is", a)
}

線上執行程式

& 操作符用於獲取變數的地址。上面程式的第 9 行我們把 b 的地址賦值給 *int 型別的 a。我們稱 a 指向了 b。當我們列印 a 的值時,會打印出 b 的地址。程式將輸出:

Type of a is *int  
address of b is 0x1040a124

由於 b 可能處於記憶體的任何位置,你應該會得到一個不同的地址。

指標的零值(Zero Value)

指標的零值是 nil

package main

import (  
    "fmt"
)

func main() {  
    a := 25
    var b *int
    if b == nil {
        fmt.Println("b is", b)
        b = &a
        fmt.Println("b after initialization is", b)
    }
}

線上執行程式

上面的程式中,b 初始化為 nil,接著將 a 的地址賦值給 b。程式會輸出:

b is <nil>  
b after initialisation is 0x1040a124

指標的解引用

指標的解引用可以獲取指標所指向的變數的值。將 a 解引用的語法是 *a

通過下面的程式碼,可以看到如何使用解引用。

package main  
import (  
    "fmt"
)

func main() {  
    b := 255
    a := &b
    fmt.Println("address of b is", a)
    fmt.Println("value of b is", *a)
}

線上執行程式

在上面程式的第 10 行,我們將 a 解引用,並列印了它的值。不出所料,我們會打印出 b 的值。程式會輸出:

address of b is 0x1040a124  
value of b is 255

我們再編寫一個程式,用指標來修改 b 的值。

package main

import (  
    "fmt"
)

func main() {  
    b := 255
    a := &b
    fmt.Println("address of b is", a)
    fmt.Println("value of b is", *a)
    *a++
    fmt.Println("new value of b is", b)
}

線上執行程式

在上面程式的第 12 行中,我們把 a 指向的值加 1,由於 a 指向了 b,因此 b 的值也發生了同樣的改變。於是 b 的值變為 256。程式會輸出:

address of b is 0x1040a124  
value of b is 255  
new value of b is 256

向函式傳遞指標引數

package main

import (  
    "fmt"
)

func change(val *int) {  
    *val = 55
}
func main() {  
    a := 58
    fmt.Println("value of a before function call is",a)
    b := &a
    change(b)
    fmt.Println("value of a after function call is", a)
}

線上執行程式

在上面程式中的第 14 行,我們向函式 change 傳遞了指標變數 b,而 b 儲存了 a 的地址。程式的第 8 行在 change 函式內使用解引用,修改了 a 的值。該程式會輸出:

value of a before function call is 58  
value of a after function call is 55

不要向函式傳遞陣列的指標,而應該使用切片

假如我們想要在函式內修改一個數組,並希望呼叫函式的地方也能得到修改後的陣列,一種解決方案是把一個指向陣列的指標傳遞給這個函式。

package main

import (  
    "fmt"
)

func modify(arr *[3]int) {  
    (*arr)[0] = 90
}

func main() {  
    a := [3]int{89, 90, 91}
    modify(&a)
    fmt.Println(a)
}

線上執行程式

在上面程式的第 13 行中,我們將陣列的地址傳遞給了 modify 函式。在第 8 行,我們在 modify 函式裡把 arr 解引用,並將 90 賦值給這個陣列的第一個元素。程式會輸出 [90 90 91]

a[x] 是 (*a)[x] 的簡寫形式,因此上面程式碼中的 (*arr)[0] 可以替換為 arr[0]。下面我們用簡寫形式重寫以上程式碼。

package main

import (  
    "fmt"
)

func modify(arr *[3]int) {  
    arr[0] = 90
}

func main() {  
    a := [3]int{89, 90, 91}
    modify(&a)
    fmt.Println(a)
}

線上執行程式

該程式也會輸出 [90 90 91]

這種方式向函式傳遞一個數組指標引數,並在函式內修改陣列。儘管它是有效的,但卻不是 Go 語言慣用的實現方式。我們最好使用切片來處理。

接下來我們用切片來重寫之前的程式碼。

package main

import (  
    "fmt"
)

func modify(sls []int) {  
    sls[0] = 90
}

func main() {  
    a := [3]int{89, 90, 91}
    modify(a[:])
    fmt.Println(a)
}

線上執行程式

在上面程式的第 13 行,我們將一個切片傳遞給了 modify 函式。在 modify 函式中,我們把切片的第一個元素修改為 90。程式也會輸出 [90 90 91]所以別再傳遞陣列指標了,而是使用切片吧。上面的程式碼更加簡潔,也更符合 Go 語言的習慣。

Go 不支援指標運算

Go 並不支援其他語言(例如 C)中的指標運算。

package main

func main() {  
    b := [...]int{109, 110, 111}
    p := &b
    p++
}

線上執行程式

上面的程式會丟擲編譯錯誤:main.go:6: invalid operation: p++ (non-numeric type *[3]int)

 

 

Go 系列教程 —— 16. 結構體

 

什麼是結構體?

結構體是使用者定義的型別,表示若干個欄位(Field)的集合。有時應該把資料整合在一起,而不是讓這些資料沒有聯絡。這種情況下可以使用結構體。

例如,一個職員有 firstNamelastName 和 age 三個屬性,而把這些屬性組合在一個結構體 employee 中就很合理。

結構體的宣告

type Employee struct {
    firstName string
    lastName  string
    age       int
}

在上面的程式碼片段裡,聲明瞭一個結構體型別 Employee,它有 firstNamelastName 和 age 三個欄位。通過把相同型別的欄位宣告在同一行,結構體可以變得更加緊湊。在上面的結構體中,firstName 和 lastName 屬於相同的 string 型別,於是這個結構體可以重寫為:

type Employee struct {
    firstName, lastName string
    age, salary         int
}

上面的結構體 Employee 稱為 命名的結構體(Named Structure)。我們建立了名為 Employee 的新型別,而它可以用於建立 Employee 型別的結構體變數。

宣告結構體時也可以不用宣告一個新型別,這樣的結構體型別稱為 匿名結構體(Anonymous Structure)

var employee struct {
    firstName, lastName string
    age int
}

上述程式碼片段建立一個匿名結構體 employee

建立命名的結構體

通過下面程式碼,我們定義了一個命名的結構體 Employee

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName, lastName string
    age, salary         int
}

func main() {

    //creating structure using field names
    emp1 := Employee{
        firstName: "Sam",
        age:       25,
        salary:    500,
        lastName:  "Anderson",
    }

    //creating structure without using field names
    emp2 := Employee{"Thomas", "Paul", 29, 800}

    fmt.Println("Employee 1", emp1)
    fmt.Println("Employee 2", emp2)
}

線上執行程式

在上述程式的第 7 行,我們建立了一個命名的結構體 Employee。而在第 15 行,通過指定每個欄位名的值,我們定義了結構體變數 emp1。欄位名的順序不一定要與宣告結構體型別時的順序相同。在這裡,我們改變了 lastName 的位置,將其移到了末尾。這樣做也不會有任何的問題。

在上面程式的第 23 行,定義 emp2 時我們省略了欄位名。在這種情況下,就需要保證欄位名的順序與宣告結構體時的順序相同。

該程式將輸出:

Employee 1 {Sam Anderson 25 500}
Employee 2 {Thomas Paul 29 800}

建立匿名結構體

package main

import (
    "fmt"
)

func main() {
    emp3 := struct {
        firstName, lastName string
        age, salary         int
    }{
        firstName: "Andreah",
        lastName:  "Nikola",
        age:       31,
        salary:    5000,
    }

    fmt.Println("Employee 3", emp3)
}

線上執行程式

在上述程式的第 3 行,我們定義了一個匿名結構體變數 emp3。上面我們已經提到,之所以稱這種結構體是匿名的,是因為它只是建立一個新的結構體變數 em3,而沒有定義任何結構體型別。

該程式會輸出:

Employee 3 {Andreah Nikola 31 5000}

結構體的零值(Zero Value)

當定義好的結構體並沒有被顯式地初始化時,該結構體的欄位將預設賦為零值。

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName, lastName string
    age, salary         int
}

func main() {  
    var emp4 Employee //zero valued structure
    fmt.Println("Employee 4", emp4)
}

線上執行程式

該程式定義了 emp4,卻沒有初始化任何值。因此 firstName 和 lastName 賦值為 string 的零值("")。而 age 和 salary 賦值為 int 的零值(0)。該程式會輸出:

Employee 4 { 0 0}

當然還可以為某些欄位指定初始值,而忽略其他欄位。這樣,忽略的欄位名會賦值為零值。

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName, lastName string
    age, salary         int
}

func main() {  
    emp5 := Employee{
        firstName: "John",
        lastName:  "Paul",
    }
    fmt.Println("Employee 5", emp5)
}

線上執行程式

在上面程式中的第 14 行和第 15 行,我們初始化了 firstName 和 lastName,而 age 和 salary 沒有進行初始化。因此 age 和 salary 賦值為零值。該程式會輸出:

Employee 5 {John Paul 0 0}

訪問結構體的欄位

點號操作符 . 用於訪問結構體的欄位。

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName, lastName string
    age, salary         int
}

func main() {  
    emp6 := Employee{"Sam", "Anderson", 55, 6000}
    fmt.Println("First Name:", emp6.firstName)
    fmt.Println("Last Name:", emp6.lastName)
    fmt.Println("Age:", emp6.age)
    fmt.Printf("Salary: $%d", emp6.salary)
}

線上執行程式

上面程式中的 emp6.firstName 訪問了結構體 emp6 的欄位 firstName。該程式輸出:

First Name: Sam  
Last Name: Anderson  
Age: 55  
Salary: $6000

還可以建立零值的 struct,以後再給各個欄位賦值。

package main

import (
    "fmt"
)

type Employee struct {  
    firstName, lastName string
    age, salary         int
}

func main() {  
    var emp7 Employee
    emp7.firstName = "Jack"
    emp7.lastName = "Adams"
    fmt.Println("Employee 7:", emp7)
}

線上執行程式

在上面程式中,我們定義了 emp7,接著給 firstName 和 lastName 賦值。該程式會輸出:

Employee 7: {Jack Adams 0 0}

結構體的指標

還可以建立指向結構體的指標。

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName, lastName string
    age, salary         int
}

func main() {  
    emp8 := &Employee{"Sam", "Anderson", 55, 6000}
    fmt.Println("First Name:", (*emp8).firstName)
    fmt.Println("Age:", (*emp8).age)
}

線上執行程式

在上面程式中,emp8 是一個指向結構體 Employee 的指標。(*emp8).firstName 表示訪問結構體 emp8 的 firstName 欄位。該程式會輸出:

First Name: Sam
Age: 55

Go 語言允許我們在訪問 firstName 欄位時,可以使用 emp8.firstName 來代替顯式的解引用 (*emp8).firstName

package main

import (  
    "fmt"
)

type Employee struct {  
    firstName, lastName string
    age, salary         int
}

func main() {  
    emp8 := &Employee{"Sam", "Anderson", 55, 6000}
    fmt.Println("First Name:", emp8.firstName)
    fmt.Println("Age:", emp8.age)
}

線上執行程式

在上面的程式中,我們使用 emp8.firstName 來訪問 firstName 欄位,該程式會輸出:

First Name: Sam
Age: 55

匿名欄位

當我們建立結構體時,欄位可以只有型別,而沒有欄位名。這樣的欄位稱為匿名欄位(Anonymous Field)。

以下程式碼建立一個 Person 結構體,它含有兩個匿名欄位 string 和 int

type Person struct {  
    string
    int
}

我們接下來使用匿名欄位來編寫一個程式。

package main

import (  
    "fmt"
)

type Person struct {  
    string
    int
}

func main() {  
    p := Person{"Naveen", 50}
    fmt.Println(p)
}

線上執行程式

在上面的程式中,結構體 Person 有兩個匿名欄位。p := Person{"Naveen", 50} 定義了一個 Person 型別的變數。該程式輸出 {Naveen 50}

雖然匿名欄位沒有名稱,但其實匿名欄位的名稱就預設為它的型別。比如在上面的 Person 結構體裡,雖說欄位是匿名的,但 Go 預設這些欄位名是它們各自的型別。所以 Person 結構體有兩個名為 string 和 int 的欄位。

package main

import (  
    "fmt"
)

type Person struct {  
    string
    int
}

func main() {  
    var p1 Person
    p1.string = "naveen"
    p1.int = 50
    fmt.Println(p1)
}

線上執行程式

在上面程式的第 14 行和第 15 行,我們訪問了 Person 結構體的匿名欄位,我們把欄位型別作為欄位名,分別為 "string" 和 "int"。上面程式的輸出如下:

{naveen 50}

巢狀結構體(Nested Structs)

結構體的欄位有可能也是一個結構體。這樣的結構體稱為巢狀結構體。

package main

import (  
    "fmt"
)

type Address struct {  
    city, state string
}
type Person struct {  
    name string
    age int
    address Address
}

func main() {  
    var p Person
    p.name = "Naveen"
    p.age = 50
    p.address = Address {
        city: "Chicago",
        state: "Illinois",
    }
    fmt.Println("Name:", p.name)
    fmt.Println("Age:",p.age)
    fmt.Println("City:",p.address.city)
    fmt.Println("State:",p.address.state)
}

線上執行程式

上面的結構體 Person 有一個欄位 address,而 address 也是結構體。該程式輸出:

Name: Naveen  
Age: 50  
City: Chicago  
State: Illinois

提升欄位(Promoted Fields)

如果是結構體中有匿名的結構體型別欄位,則該匿名結構體裡的欄位就稱為提升欄位。這是因為提升欄位就像是屬於外部結構體一樣,可以用外部結構體直接訪問。我知道這種定義很複雜,所以我們直接研究下程式碼來理解吧。

type Address struct {  
    city, state string
}
type Person struct {  
    name string
    age  int
    Address
}

在上面的程式碼片段中,Person 結構體有一個匿名欄位 Address,而 Address 是一個結構體。現在結構體 Address 有 city 和 state 兩個欄位,訪問這兩個欄位就像在 Person 裡直接宣告的一樣,因此我們稱之為提升欄位。

package main

import (
    "fmt"
)

type Address struct {
    city, state string
}
type Person struct {
    name string
    age  int
    Address
}

func main() {  
    var p Person
    p.name = "Naveen"
    p.age = 50
    p.Address = Address{
        city:  "Chicago",
        state: "Illinois",
    }
    fmt.Println("Name:", p.name)
    fmt.Println("Age:", p.age)
    fmt.Println("City:", p.city) //city is promoted field
    fmt.Println("State:", p.state) //state is promoted field
}

線上執行程式

在上面程式碼中的第 26 行和第 27 行,我們使用了語法 p.city 和 p.state,訪問提升欄位 city 和 state 就像它們是在結構體 p 中宣告的一樣。該程式會輸出:

Name: Naveen  
Age: 50  
City: Chicago  
State: Illinois

匯出結構體和欄位

如果結構體名稱以大寫字母開頭,則它是其他包可以訪問的匯出型別(Exported Type)。同樣,如果結構體裡的欄位首字母大寫,它也能被其他包訪問到。

讓我們使用自定義包,編寫一個程式來更好地去理解它。

在你的 Go 工作區的 src 目錄中,建立一個名為 structs 的資料夾。另外在 structs 中再建立一個目錄 computer

在 computer 目錄中,在名為 spec.go 的檔案中儲存下面的程式。

package computer

type Spec struct { //exported struct  
    Maker string //exported field
    model string //unexported field
    Price int //exported field
}

上面的程式碼片段中,建立了一個 computer 包,裡面有一個匯出結構體型別 SpecSpec 有兩個匯出欄位 Maker 和 Price,和一個未匯出的欄位 model。接下來我們會在 main 包中匯入這個包,並使用 Spec 結構體。

package main

import "structs/computer"  
import "fmt"

func main() {  
    var spec computer.Spec
    spec.Maker = "apple"
    spec.Price = 50000
    fmt.Println("Spec:", spec)
}

包結構如下所示:

src  
   structs
        computer
            spec.go
        main.go

在上述程式的第 3 行,我們匯入了 computer 包。在第 8 行和第 9 行,我們訪問了結構體 Spec 的兩個匯出欄位 Maker 和 Price。執行命令 go install structs 和 workspacepath/bin/structs,執行該程式。

如果我們試圖訪問未匯出的欄位 model,編譯器會報錯。將 main.go 的內容替換為下面的程式碼。

package main

import "structs/computer"  
import "fmt"

func main() {  
    var spec computer.Spec
    spec.Maker = "apple"
    spec.Price = 50000
    spec.model = "Mac Mini"
    fmt.Println("Spec:", spec)
}

在上面程式的第 10 行,我們試圖訪問未匯出的欄位 model。如果執行這個程式,編譯器會產生錯誤:spec.model undefined (cannot refer to unexported field or method model)

結構體相等性(Structs Equality)

結構體是值型別。如果它的每一個欄位都是可比較的,則該結構體也是可比較的。如果兩個結構體變數的對應欄位相等,則這兩個變數也是相等的

package main

import (  
    "fmt"
)

type name struct {  
    firstName string
    lastName string
}


func main() {  
    name1 := name{"Steve", "Jobs"}
    name2 := name{"Steve", "Jobs"}
    if name1 == name2 {
        fmt.Println("name1 and name2 are equal")
    } else {
        fmt.Println("name1 and name2 are not equal")
    }

    name3 := name{firstName:"Steve", lastName:"Jobs"}
    name4 := name{}
    name4.firstName = "Steve"
    if name3 == name4 {
        fmt.Println("name3 and name4 are equal")
    } else {
        fmt.Println("name3 and name4 are not equal")
    }
}

線上執行程式

在上面的程式碼中,結構體型別 name 包含兩個 string 型別。由於字串是可比較的,因此可以比較兩個 name 型別的結構體變數。

上面程式碼中 name1 和 name2 相等,而 name3 和 name4 不相等。該程式會輸出:

name1 and name2 are equal  
name3 and name4 are not equal

如果結構體包含不可比較的欄位,則結構體變數也不可比較。

package main

import (  
    "fmt"
)

type image struct {  
    data map[int]int
}

func main() {  
    image1 := image{data: map[int]int{
        0: 155,
    }}
    image2 := image{data: map[int]int{
        0: 155,
    }}
    if image1 == image2 {
        fmt.Println("image1 and image2 are equal")
    }
}

線上執行程式

在上面程式碼中,結構體型別 image 包含一個 map 型別的欄位。由於 map 型別是不可比較的,因此 image1 和 image2 也不可比較。如果執行該程式,編譯器會報錯:main.go:18: invalid operation: image1 == image2 (struct containing map[int]int cannot be compared)

 

Go 系列教程 —— 17. 方法

 

什麼是方法?

方法其實就是一個函式,在 func 這個關鍵字和方法名中間加入了一個特殊的接收器型別。接收器可以是結構體型別或者是非結構體型別。接收器是可以在方法的內部訪問的。

下面就是建立一個方法的語法。

func (t Type) methodName(parameter list) {
}

上面的程式碼片段建立了一個接收器型別為 Type 的方法 methodName

方法示例

讓我們來編寫一個簡單的小程式,它會在結構體型別上建立一個方法並呼叫它。

package main

import (
    "fmt"
)

type Employee struct {
    name     string
    salary   int
    currency string
}

/*
  displaySalary() 方法將 Employee 做為接收器型別
*/
func (e Employee) displaySalary() {
    fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}

func main() {
    emp1 := Employee {
        name:     "Sam Adolf",
        salary:   5000,
        currency: "$",
    }
    emp1.displaySalary() // 呼叫 Employee 型別的 displaySalary() 方法
}

線上執行程式

在上面程式的第 16 行,我們在 Employee 結構體型別上建立了一個 displaySalary 方法。displaySalary()方法在方法的內部訪問了接收器 e Employee。在第 17 行,我們使用接收器 e,並列印 employee 的 name、currency 和 salary 這 3 個欄位。

在第 26 行,我們呼叫了方法 emp1.displaySalary()

程式輸出:Salary of Sam Adolf is $5000

為什麼我們已經有函數了還需要方法呢?

上面的程式已經被重寫為只使用函式,沒有方法。

package main

import (
    "fmt"
)

type Employee struct {
    name     string
    salary   int
    currency string
}

/*
displaySalary()方法被轉化為一個函式,把 Employee 當做引數傳入。
*/
func displaySalary(e Employee) {
    fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}

func main() {
    emp1 := Employee{
        name:     "Sam Adolf",
        salary:   5000,
        currency: "$",
    }
    displaySalary(emp1)
}

線上執行程式

在上面的程式中,displaySalary 方法被轉化為一個函式,Employee 結構體被當做引數傳遞給它。這個程式也產生完全相同的輸出:Salary of Sam Adolf is $5000

既然我們可以使用函式寫出相同的程式,那麼為什麼我們需要方法?這有著幾個原因,讓我們一個個的看看。

  • Go 不是純粹的面向物件程式語言,而且Go不支援類。因此,基於型別的方法是一種實現和類相似行為的途徑。

  • 相同的名字的方法可以定義在不同的型別上,而相同名字的函式是不被允許的。假設我們有一個 Square 和 Circle 結構體。可以在 Square 和 Circle 上分別定義一個 Area 方法。見下面的程式。

package main

import (
    "fmt"
    "math"
)

type Rectangle struct {
    length int
    width  int
}

type Circle struct {
    radius float64
}

func (r Rectangle) Area() int {
    return r.length * r.width
}

func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

func main() {
    r := Rectangle{
        length: 10,
        width:  5,
    }
    fmt.Printf("Area of rectangle %d\n", r.Area())
    c := Circle{
        radius: 12,
    }
    fmt.Printf("Area of circle %f", c.Area())
}

線上執行程式

該程式輸出:

Area of rectangle 50
Area of circle 452.389342

上面方法的屬性被使用在介面中。我們將在接下來的教程中討論這個問題。

指標接收器與值接收器

到目前為止,我們只看到了使用值接收器的方法。還可以建立使用指標接收器的方法。值接收器和指標接收器之間的區別在於,在指標接收器的方法內部的改變對於呼叫者是可見的,然而值接收器的情況不是這樣的。讓我們用下面的程式來幫助理解這一點。

package main

import (
    "fmt"
)

type Employee struct {
    name string
    age  int
}

/*
使用值接收器的方法。
*/
func (e Employee) changeName(newName string) {
    e.name = newName
}

/*
使用指標接收器的方法。
*/
func (e *Employee) changeAge(newAge int) {
    e.age = newAge
}

func main() {
    e := Employee{
        name: "Mark Andrew",
        age:  50,
    }
    fmt.Printf("Employee name before change: %s", e.name)
    e.changeName("Michael Andrew")
    fmt.Printf("\nEmployee name after change: %s", e.name)

    fmt.Printf("\n\nEmployee age before change: %d", e.age)
    (&e).changeAge(51)
    fmt.Printf("\nEmployee age after change: %d", e.age)
}

線上執行程式

在上面的程式中,changeName 方法有一個值接收器 (e Employee),而 changeAge 方法有一個指標接收器 (e *Employee)。在 changeName 方法中對 Employee 結構體的欄位 name 所做的改變對呼叫者是不可見的,因此程式在呼叫 e.changeName("Michael Andrew") 這個方法的前後打印出相同的名字。由於 changeAge 方法是使用指標 (e *Employee) 接收器的,所以在呼叫 (&e).changeAge(51) 方法對 age 欄位做出的改變對呼叫者將是可見的。該程式輸出如下:

Employee name before change: Mark Andrew
Employee name after change: Mark Andrew

Employee age before change: 50
Employee age after change: 51

在上面程式的第 36 行,我們使用 (&e).changeAge(51) 來呼叫 changeAge 方法。由於 changeAge 方法有一個指標接收器,所以我們使用 (&e) 來呼叫這個方法。其實沒有這個必要,Go語言讓我們可以直接使用 e.changeAge(51)e.changeAge(51) 會自動被Go語言解釋為 (&e).changeAge(51)

下面的程式重寫了,使用 e.changeAge(51) 來代替 (&e).changeAge(51),它輸出相同的結果。

package main

import (
    "fmt"
)

type Employee struct {
    name string
    age  int
}

/*
使用值接收器的方法。
*/
func (e Employee) changeName(newName string) {
    e.name = newName
}

/*
使用指標接收器的方法。
*/
func (e *Employee) changeAge(newAge int) {
    e.age = newAge
}

func main() {
    e := Employee{
        name: "Mark Andrew",
        age:  50,
    }
    fmt.Printf("Employee name before change: %s", e.name)
    e.changeName("Michael Andrew")
    fmt.Printf("\nEmployee name after change: %s", e.name)

    fmt.Printf("\n\nEmployee age before change: %d", e.age)
    e.changeAge(51)
    fmt.Printf("\nEmployee age after change: %d", e.age)
}

線上執行程式

那麼什麼時候使用指標接收器,什麼時候使用值接收器?

一般來說,指標接收器可以使用在:對方法內部的接收器所做的改變應該對呼叫者可見時。

指標接收器也可以被使用在如下場景:當拷貝一個結構體的代價過於昂貴時。考慮下一個結構體有很多的欄位。在方法內使用這個結構體做為值接收器需要拷貝整個結構體,這是很昂貴的。在這種情況下使用指標接收器,結構體不會被拷貝,只會傳遞一個指標到方法內部使用。

在其他的所有情況,值接收器都可以被使用。

匿名欄位的方法

屬於結構體的匿名欄位的方法可以被直接呼叫,就好像這些方法是屬於定義了匿名欄位的結構體一樣。

package main

import (
    "fmt"
)

type address struct {
    city  string
    state string
}

func (a address) fullAddress() {
    fmt.Printf("Full address: %s, %s", a.city, a.state)
}

type person struct {
    firstName string
    lastName  string
    address
}

func main() {
    p := person{
        firstName: "Elon",
        lastName:  "Musk",
        address: address {
            city:  "Los Angeles",
            state: "California",
        },
    }

    p.fullAddress() //訪問 address 結構體的 fullAddress 方法
}

線上執行程式

在上面程式的第 32 行,我們通過使用 p.fullAddress() 來訪問 address 結構體的 fullAddress() 方法。明確的呼叫 p.address.fullAddress() 是沒有必要的。該程式輸出:

Full address: Los Angeles, California

在方法中使用值接收器 與 在函式中使用值引數

這個話題很多Go語言新手都弄不明白。我會盡量講清楚。

當一個函式有一個值引數,它只能接受一個值引數。

當一個方法有一個值接收器,它可以接受值接收器和指標接收器。

讓我們通過一個例子來理解這一點。

package main

import (
    "fmt"
)

type rectangle struct {
    length int
    width  int
}

func area(r rectangle) {
    fmt.Printf("Area Function result: %d\n", (r.length * r.width))
}

func (r rectangle) area() {
    fmt.Printf("Area Method result: %d\n", (r.length * r.width))
}

func main() {
    r := rectangle{
        length: 10,
        width:  5,
    }
    area(r)
    r.area()

    p := &r
    /*
       compilation error, cannot use p (type *rectangle) as type rectangle
       in argument to area
    */
    //area(p)

    p.area()//通過指標呼叫值接收器
}

線上執行程式

第 12 行的函式 func area(r rectangle) 接受一個值引數,方法 func (r rectangle) area() 接受一個值接收器。

在第 25 行,我們通過值引數 area(r) 來呼叫 area 這個函式,這是合法的。同樣,我們使用值接收器來呼叫 area 方法 r.area(),這也是合法的。

在第 28 行,我們建立了一個指向 r 的指標 p。如果我們試圖把這個指標傳遞到只能接受一個值引數的函式 area,編譯器將會報錯。所以我把程式碼的第 33 行註釋了。如果你把這行的程式碼註釋去掉,編譯器將會丟擲錯誤 compilation error, cannot use p (type *rectangle) as type rectangle in argument to area.。這將會按預期丟擲錯誤。

現在到了棘手的部分了,在第35行的程式碼 p.area() 使用指標接收器 p 呼叫了只接受一個值接收器的方法 area。這是完全有效的。原因是當 area 有一個值接收器時,為了方便Go語言把 p.area() 解釋為 (*p).area()

該程式將會輸出:

Area Function result: 50
Area Method result: 50
Area Method result: 50

在方法中使用指標接收器 與 在函式中使用指標引數

和值引數相類似,函式使用指標引數只接受指標,而使用指標接收器的方法可以使用值接收器和指標接收器。

package main

import (
    "fmt"
)

type rectangle struct {
    length int
    width  int
}

func perimeter(r *rectangle) {
    fmt.Println("perimeter function output:", 2*(r.length+r.width))

}

func (r *rectangle) perimeter() {
    fmt.Println("perimeter method output:", 2*(r.length+r.width))
}

func main() {
    r := rectangle{
        length: 10,
        width:  5,
    }
    p := &r //pointer to r
    perimeter(p)
    p.perimeter()

    /*
        cannot use r (type rectangle) as type *rectangle in argument to perimeter
    */
    //perimeter(r)

    r.perimeter()//使用值來呼叫指標接收器
}

線上執行程式

在上面程式的第 12 行,定義了一個接受指標引數的函式 perimeter。第 17 行定義了一個有一個指標接收器的方法。

在第 27 行,我們呼叫 perimeter 函式時傳入了一個指標引數。在第 28 行,我們通過指標接收器呼叫了 perimeter 方法。所有一切看起來都這麼完美。

在被註釋掉的第 33 行,我們嘗試通過傳入值引數 r 呼叫函式 perimeter。這是不被允許的,因為函式的指標引數不接受值引數。如果你把這行的程式碼註釋去掉並把程式執行起來,編譯器將會丟擲錯誤 main.go:33: cannot use r (type rectangle) as type *rectangle in argument to perimeter.

在第 35 行,我們通過值接收器 r 來呼叫有指標接收器的方法 perimeter。這是被允許的,為了方便Go語言把程式碼 r.perimeter() 解釋為 (&r).perimeter()。該程式輸出:

perimeter function output: 30
perimeter method output: 30
perimeter method output: 30

在非結構體上的方法

到目前為止,我們只在結構體型別上定義方法。也可以在非結構體型別上定義方法,但是有一個問題。為了在一個型別上定義一個方法,方法的接收器型別定義和方法的定義應該在同一個包中。到目前為止,我們定義的所有結構體和結構體上的方法都是在同一個 main 包中,因此它們是可以執行的。

package main

func (a int) add(b int) {
}

func main() {

}

線上執行程式

在上面程式的第 3 行,我們嘗試把一個 add 方法新增到內建的型別 int。這是不允許的,因為 add 方法的定義和 int 型別的定義不在同一個包中。該程式會丟擲編譯錯誤 cannot define new methods on non-local type int

讓該程式工作的方法是為內建型別 int 建立一個類型別名,然後建立一個以該類型別名為接收器的方法。

package main

import "fmt"

type myInt int

func (a myInt) add(b myInt) myInt {
    return a + b
}

func main() {
    num1 := myInt(5)
    num2 := myInt(10)
    sum := num1.add(num2)
    fmt.Println("Sum is", sum)
}

線上執行程式

在上面程式的第5行,我們為 int 建立了一個類型別名 myInt。在第7行,我們定義了一個以 myInt 為接收器的的方法 add

該程式將會打印出 Sum is 15

 

 

Go 系列教程 —— 18. 介面(一)

 

什麼是介面?

在面向物件的領域裡,介面一般這樣定義:介面定義一個物件的行為。介面只指定了物件應該做什麼,至於如何實現這個行為(即實現細節),則由物件本身去確定。

在 Go 語言中,介面就是方法簽名(Method Signature)的集合。當一個型別定義了介面中的所有方法,我們稱它實現了該介面。這與面向物件程式設計(OOP)的說法很類似。介面指定了一個型別應該具有的方法,並由該型別決定如何實現這些方法

例如,WashingMachine 是一個含有 Cleaning() 和 Drying() 兩個方法的介面。任何定義了 Cleaning() 和 Drying() 的型別,都稱它實現了 WashingMachine 介面。

介面的宣告與實現

讓我們編寫程式碼,建立一個介面並且實現它。

package main

import (  
    "fmt"
)

//interface definition
type VowelsFinder interface {  
    FindVowels() []rune
}

type MyString string

//MyString implements VowelsFinder
func (ms MyString) FindVowels() []rune {  
    var vowels []rune
    for _, rune := range ms {
        if rune == 'a' || rune == 'e' || rune == 'i' || rune == 'o' || rune == 'u' {
            vowels = append(vowels, rune)
        }
    }
    return vowels
}

func main() {  
    name := MyString("Sam Anderson")
    var v VowelsFinder
    v = name // possible since MyString implements VowelsFinder
    fmt.Printf("Vowels are %c", v.FindVowels())

}

線上執行程式

在上面程式的第 8 行,建立了一個名為 VowelsFinder 的介面,該介面有一個 FindVowels() []rune 的方法。

在接下來的一行,我們建立了一個 MyString 型別。

在第 15 行,我們給接受者型別(Receiver Type) MyString 添加了方法 FindVowels() []rune。現在,我們稱 MyString 實現了 VowelsFinder 介面。這就和其他語言(如 Java)很不同,其他一些語言要求一個類使用 implement 關鍵字,來顯式地宣告該類實現了介面。而在 Go 中,並不需要這樣。如果一個型別包含了介面中宣告的所有方法,那麼它就隱式地實現了 Go 介面

在第 28 行,v 的型別為 VowelsFindername 的型別為 MyString,我們把 name 賦值給了 v。由於 MyString 實現了 VowelFinder,因此這是合法的。在下一行,v.FindVowels() 呼叫了 MyString 型別的 FindVowels 方法,列印字串 Sam Anderson 裡所有的母音。該程式輸出 Vowels are [a e o]

祝賀!你已經建立並實現了你的第一個介面。

介面的實際用途

前面的例子教我們建立並實現了介面,但還沒有告訴我們介面的實際用途。在上面的程式裡,如果我們使用 name.FindVowels(),而不是 v.FindVowels(),程式依然能夠照常執行,但介面並沒有體現出實際價值。

因此,我們現在討論一下介面的實際應用場景。

我們編寫一個簡單程式,根據公司員工的個人薪資,計算公司的總支出。為了簡單起見,我們假定支出的單位都是美元。

package main

import (  
    "fmt"
)

type SalaryCalculator interface {  
    CalculateSalary() int
}

type Permanent struct {  
    empId    int
    basicpay int
    pf       int
}

type Contract struct {  
    empId  int
    basicpay int
}

//salary of permanent employee is sum of basic pay and pf
func (p Permanent) CalculateSalary() int {  
    return p.basicpay + p.pf
}

//salary of contract employee is the basic pay alone
func (c Contract) CalculateSalary() int {  
    return c.basicpay
}

/*
total expense is calculated by iterating though the SalaryCalculator slice and summing  
the salaries of the individual employees  
*/
func totalExpense(s []SalaryCalculator) {  
    expense := 0
    for _, v := range s {
        expense = expense + v.CalculateSalary()
    }
    fmt.Printf("Total Expense Per Month $%d", expense)
}

func main() {  
    pemp1 := Permanent{1, 5000, 20}
    pemp2 := Permanent{2, 6000, 30}
    cemp1 := Contract{3, 3000}
    employees := []SalaryCalculator{pemp1, pemp2, cemp1}
    totalExpense(employees)

}

線上執行程式

上面程式的第 7 行聲明瞭一個 SalaryCalculator 介面型別,它只有一個方法 CalculateSalary() int

在公司裡,我們有兩類員工,即第 11 行和第 17 行定義的結構體:Permanent 和 Contract。長期員工(Permanent)的薪資是 basicpay 與 pf 相加之和,而合同員工(Contract)只有基本工資 basicpay。在第 23 行和第 28 行中,方法 CalculateSalary 分別實現了以上關係。由於 Permanent 和 Contract 都聲明瞭該方法,因此它們都實現了 SalaryCalculator 介面。

第 36 行宣告的 totalExpense 方法體現出了介面的妙用。該方法接收一個 SalaryCalculator 介面的切片([]SalaryCalculator)作為引數。在第 49 行,我們向 totalExpense 方法傳遞了一個包含 Permanent 和 Contact 型別的切片。在第 39 行中,通過呼叫不同型別對應的 CalculateSalary 方法,totalExpense 可以計算得到支出。

這樣做最大的優點是:totalExpense 可以擴充套件新的員工型別,而不需要修改任何程式碼。假如公司增加了一種新的員工型別 Freelancer,它有著不同的薪資結構。Freelancer只需傳遞到 totalExpense 的切片引數中,無需 totalExpense 方法本身進行修改。只要 Freelancer 也實現了 SalaryCalculator 介面,totalExpense 就能夠實現其功能。

該程式輸出 Total Expense Per Month $14050

介面的內部表示

我們可以把介面看作內部的一個元組 (type, value)。 type 是介面底層的具體型別(Concrete Type),而 value 是具體型別的值。

我們編寫一個程式來更好地理解它。

package main

import (  
    "fmt"
)

type Test interface {  
    Tester()
}

type MyFloat float64

func (m MyFloat) Tester() {  
    fmt.Println(m)
}

func describe(t Test) {  
    fmt.Printf("Interface type %T value %v\n", t, t)
}

func main() {  
    var t Test
    f := MyFloat(89.7)
    t = f
    describe(t)
    t.Tester()
}

線上執行程式

Test 介面只有一個方法 Tester(),而 MyFloat 型別實現了該介面。在第 24 行,我們把變數 fMyFloat 型別)賦值給了 tTest 型別)。現在 t 的具體型別為 MyFloat,而 t 的值為 89.7。第 17 行的 describe 函式打印出了介面的具體型別和值。該程式輸出:

Interface type main.MyFloat value 89.7  
89.7

空介面

沒有包含方法的介面稱為空介面。空介面表示為 interface{}。由於空介面沒有方法,因此所有型別都實現了空介面。

package main

import (  
    "fmt"
)

func describe(i interface{}) {  
    fmt.Printf("Type = %T, value = %v\n", i, i)
}

func main() {  
    s := "Hello World"
    describe(s)
    i := 55
    describe(i)
    strt := struct {
        name string
    }{
        name: "Naveen R",
    }
    describe(strt)
}

線上執行程式

在上面的程式的第 7 行,describe(i interface{}) 函式接收空介面作為引數,因此,可以給這個函式傳遞任何型別。

在第 13 行、第 15 行和第 21 行,我們分別給 describe 函式傳遞了 stringint 和 struct。該程式列印:

Type = string, value = Hello World  
Type = int, value = 55  
Type = struct { name string }, value = {Naveen R}

型別斷言

型別斷言用於提取介面的底層值(Underlying Value)。

在語法 i.(T) 中,介面 i 的具體型別是 T,該語法用於獲得介面的底層值。

一段程式碼勝過千言。下面編寫個關於型別斷言的程式。

package main

import (  
    "fmt"
)

func assert(i interface{}) {  
    s := i.(int) //get the underlying int value from i
    fmt.Println(s)
}
func main() {  
    var s interface{} = 56
    assert(s)
}

線上執行程式

在第 12 行,s 的具體型別是 int。在第 8 行,我們使用了語法 i.(int) 來提取 i 的底層 int 值。該程式會列印 56

在上面程式中,如果具體型別不是 int,會發生什麼呢?接下來看看。

package main

import (  
    "fmt"
)

func assert(i interface{}) {  
    s := i.(int) 
    fmt.Println(s)
}
func main() {  
    var s interface{} = "Steven Paul"
    assert(s)
}

線上執行程式

在上面程式中,我們把具體型別為 string 的 s 傳遞給了 assert 函式,試圖從它提取出 int 值。該程式會報錯:panic: interface conversion: interface {} is string, not int.

要解決該問題,我們可以使用以下語法:

v, ok := i.(T)

如果 i 的具體型別是 T,那麼 v 賦值為 i 的底層值,而 ok 賦值為 true

如果 i 的具體型別不是 T,那麼 ok 賦值為 falsev 賦值為 T 型別的零值,此時程式不會報錯

package main

import (  
    "fmt"
)

func assert(i interface{}) {  
    v, ok := i.(int)
    fmt.Println(v, ok)
}
func main() {  
    var s interface{} = 56
    assert(s)
    var i interface{} = "Steven Paul"
    assert(i)
}

線上執行程式

當給 assert 函式傳遞 Steven Paul 時,由於 i 的具體型別不是 intok 賦值為 false,而 v 賦值為 0(int 的零值)。該程式列印:

56 true  
0 false

型別選擇(Type Switch)

型別選擇用於將介面的具體型別與很多 case 語句所指定的型別進行比較。它與一般的 switch 語句類似。唯一的區別在於型別選擇指定的是型別,而一般的 switch 指定的是值。

型別選擇的語法類似於型別斷言。型別斷言的語法是 i.(T),而對於型別選擇,型別 T 由關鍵字 type 代替。下面看看程式是如何工作的。

package main

import (  
    "fmt"
)

func findType(i interface{}) {  
    switch i.(type) {
    case string:
        fmt.Printf("I am a string and my value is %s\n", i.(string))
    case int:
        fmt.Printf("I am an int and my value is %d\n", i.(int))
    default:
        fmt.Printf("Unknown type\n")
    }
}
func main() {  
    findType("Naveen")
    findType(77)
    findType(89.98)
}

線上執行程式

在上述程式的第 8 行,switch i.(type) 表示一個型別選擇。每個 case 語句都把 i 的具體型別和一個指定型別進行了比較。如果 case 匹配成功,會打印出相應的語句。該程式輸出:

I am a string and my value is Naveen  
I am an int and my value is 77  
Unknown type

第 20 行中的 89.98 的型別是 float64,沒有在 case 上匹配成功,因此最後一行列印了 Unknown type

還可以將一個型別和介面相比較。如果一個型別實現了介面,那麼該型別與其實現的介面就可以互相比較

為了闡明這一點,下面寫一個程式。

package main

import "fmt"

type Describer interface {  
    Describe()
}
type Person struct {  
    name string
    age  int
}

func (p Person) Describe() {  
    fmt.Printf("%s is %d years old", p.name, p.age)
}

func findType(i interface{}) {  
    switch v := i.(type) {
    case Describer:
        v.Describe()
    default:
        fmt.Printf("unknown type\n")
    }
}

func main() {  
    findType("Naveen")
    p := Person{
        name: "Naveen R",
        age:  25,
    }
    findType(p)
}

線上執行程式

在上面程式中,結構體 Person 實現了 Describer 介面。在第 19 行的 case 語句中,v 與介面型別 Describer 進行了比較。p 實現了 Describer,因此滿足了該 case 語句,於是當程式執行到第 32 行的 findType(p) 時,程式呼叫了 Describe() 方法。

該程式輸出:

unknown type  
Naveen R is 25 years old

 

 

Go 系列教程 —— 19. 介面(二)

 

實現介面:指標接受者與值接受者

介面(一)上的所有示例中,我們都是使用值接受者(Value Receiver)來實現介面的。我們同樣可以使用指標接受者(Pointer Receiver)來實現介面。只不過在用指標接受者實現介面時,還有一些細節需要注意。我們通過下面的程式碼來理解吧。

package main

import "fmt"

type Describer interface {  
    Describe()
}
type Person struct {  
    name string
    age  int
}

func (p Person) Describe() { // 使用值接受者實現  
    fmt.Printf("%s is %d years old\n", p.name, p.age)
}

type Address struct {
    state   string
    country string
}

func (a *Address) Describe() { // 使用指標接受者實現
    fmt.Printf("State %s Country %s", a.state, a.country)
}

func main() {  
    var d1 Describer
    p1 := Person{"Sam", 25}
    d1 = p1
    d1.Describe()
    p2 := Person{"James", 32}
    d1 = &p2
    d1.Describe()

    var d2 Describer
    a := Address{"Washington", "USA"}

    /* 如果下面一行取消註釋會導致編譯錯誤:
       cannot use a (type Address) as type Describer
       in assignment: Address does not implement
       Describer (Describe method has pointer
       receiver)
    */
    //d2 = a

    d2 = &a // 這是合法的
    // 因為在第 22 行,Address 型別的指標實現了 Describer 介面
    d2.Describe()

}

線上執行程式

在上面程式中的第 13 行,結構體 Person 使用值接受者,實現了 Describer 介面。

我們在討論方法的時候就已經提到過,使用值接受者宣告的方法,既可以用值來呼叫,也能用指標呼叫。不管是一個值,還是一個可以解引用的指標,呼叫這樣的方法都是合法的

p1 的型別是 Person,在第 29 行,p1 賦值給了 d1。由於 Person 實現了介面變數 d1,因此在第 30 行,會列印 Sam is 25 years old

接下來在第 32 行,d1 又賦值為 &p2,在第 33 行同樣列印輸出了 James is 32 years old。棒棒噠。:)

在 22 行,結構體 Address 使用指標接受者實現了 Describer 介面。

在上面程式裡,如果去掉第 45 行的註釋,我們會得到編譯錯誤:main.go:42: cannot use a (type Address) as type Describer in assignment: Address does not implement Describer (Describe method has pointer receiver)。這是因為在第 22 行,我們使用 Address 型別的指標接受者實現了介面 Describer,而接下來我們試圖用 a 來賦值 d2。然而 a 屬於值型別,它並沒有實現 Describer 介面。你應該會很驚訝,因為我們曾經學習過,使用指標接受者的方法,無論指標還是值都可以呼叫它。那麼為什麼第 45 行的程式碼就不管用呢?

其原因是:對於使用指標接受者的方法,用一個指標或者一個可取得地址的值來呼叫都是合法的。但介面中儲存的具體值(Concrete Value)並不能取到地址,因此在第 45 行,對於編譯器無法自動獲取 a 的地址,於是程式報錯

第 47 行就可以成功執行,因為我們將 a 的地址 &a 賦值給了 d2

程式的其他部分不言而喻。該程式會列印:

Sam is 25 years old  
James is 32 years old  
State Washington Country USA

實現多個介面

型別可以實現多個介面。我們看看下面程式是