GO千練 — 方法和接口

方法

方法概念

Go 沒有類,但是 GO 可以爲結構體類型定義方法,請記住:方法只是一個帶接收者參數的函數。
方法就是一類帶特殊的 接收者 參數的函數。方法接收者在它自己的參數列表內,位於 func 關鍵字和方法名之間。請參考如下 nextTenYears() 方法的定義:

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func (p Person) nextTenYears() int {
    return p.Age + 10
}

func main() {
    p := Person{"張三", 10}

    fmt.Printf("%v 10 年後 %v 歲", p.Name, p.nextTenYears())
}

方法特殊接受者

除了給結構體定義方法,還可以爲非結構體類型聲明方法。

但是我們只能爲在同一包內定義的類型的接收者聲明方法,而不能爲其它包內定義的類型(包括 int 之類的內建類型)的接收者聲明方法。

內置類型

package main

import "fmt"

type MyInt int

func (m MyInt) Abs() int {
    if m < 0 {
        return int(-m)
    }

    return int(m)
}

func main() {
    m := MyInt(-10)
    fmt.Printf("%v 的絕對值是 %v", m, m.Abs())

}

指針接收者

GO 可以爲指針接收者聲明方法。
這意味着對於某類型 T,接收者的類型可以用 *T 的文法。(注意:T 不能是像 *int 這樣的指針)

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func (p *Person) nextTenYearsNew() {
    p.Age = p.Age + 10
}

func main() {
    p := Person{"張三", 20}
    fmt.Println(p)

    p.nextTenYearsNew()
    fmt.Println(p)
}

方法與指針重定向

對比

通過比較 nextTenYears() 和 nextTenYearsNew() 方法對比,會發現:
如果參數爲指針類型的話,則帶指針參數的函數必須接受一個指針,無法接受一個值。
如果接收者爲指針的方法被調用時,接收者既能爲值又能爲指針。
原因:Go 會將語句 p.nextTenYearsNew() 解釋爲 (&p).nextTenYearsNew() 執行。

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func nextTenYears(p *Person) {
    (*p).Age = (*p).Age + 10
}

func (p *Person) nextTenYearsNew() {
    p.Age = p.Age + 10
}

func main() {
    p := Person{"張三", 20}
    fmt.Println(p)

    // 新年齡設置值
    nextTenYears(&p)
    fmt.Println(p)

    //如下下會報:cannot use p (variable of type Person) as type *Person in argument to nextTenYears
    // nextTenYears(p)

    // 方法內部指針直接設置新年齡,無需返回
    p.nextTenYearsNew()
    fmt.Println(p)

    // 這樣是編譯通過並運行成功的
    p1 := &p
    p1.nextTenYearsNew()
    fmt.Println(p)
}

// 打印輸出
{張三 20}
{張三 30}
{張三 40}
{張三 50}

相反的:
接受一個值作爲參數的函數必須接受一個指定類型的值。
而以值爲接收者的方法被調用時,接收者既能爲值又能爲指針。
原因:方法調用 p.nextTenYearsNew() 會被解釋爲  (*p).nextTenYearsNew()。

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

// 由於值類型是值傳遞,則需要返回值
func nextTenYears(p Person) int {
    return p.Age + 10
}

// 由於值類型是值傳遞,則需要返回值
func (p Person) nextTenYearsNew() int {
    return p.Age + 10
}

func main() {
    p := Person{"張三", 20}
    fmt.Println(p)

    // 需要返回新年齡設置值
    p.Age = nextTenYears(p)
    fmt.Println(p)

    //如下下會報:cannot use &p (value of type *Person) as type Person in argument to nextTenYears
    // p.Age = nextTenYears(&p)

    // 方法內部指針直接設置新年齡,無需返回
    p.Age = p.nextTenYearsNew()
    fmt.Println(p)

    // 這樣是編譯通過並運行成功的
    p1 := &p
    p.Age = p1.nextTenYearsNew()
    fmt.Println(p)
}

結論

我們應該選擇值還是指針作爲接收者呢?

我們應該選擇指針,原因有二:
首先,方法能夠修改其接收者指向的值。
其次,這樣可以避免在每次調用方法時複製該值。若值的類型爲大型結構體時,這樣做會更加高效。

接口

接口概念

Go 語言提供了另外一種數據類型,即接口接口類型 是由一組方法簽名定義的集合,它把所有的具有共性的方法定義在一起,任何其它類型只要實現了這些方法就是實現了這個接口。

/* 定義接口 */
type interface_name interface {
   method_name1 [return_type]
   method_name2 [return_type]
   method_name3 [return_type]
   ...
   method_namen [return_type]
}

/* 定義結構體 */
type struct_name struct {
   /* variables */
}

/* 實現接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
   /* 方法實現 */
}

// ...

func (struct_name_variable struct_name) method_namen() [return_type] {
   /* 方法實現*/
}

實例代碼:

package main

import "fmt"

type Phone interface {
    call()
}

type IPhone struct {
}

type Android struct {
}

func (iphone IPhone) call() {
    fmt.Println("蘋果手機打電話...")
}

func (android Android) call() {
    fmt.Println("安卓手機打電話...")
}

func main() {
    iphone := new(IPhone)
    iphone.call()

    android := new(Android)
    android.call()
}

接口值

在 GO 中接口也是值。它們可以像其它值一樣傳遞。接口值可以用作函數的參數或返回值。

接口值可以看做包含值和具體類型的元組:

// value 具體值
// type 具體類型
(value, type)

接口值保存了一個具體底層類型的具體值。接口值調用方法時會執行其底層類型的同名方法。

package main

import "fmt"

type Phone interface {
    call()
}

type IPhone struct {
}

type Android struct {
}

func (iphone IPhone) call() {
    fmt.Println("蘋果手機打電話...")
}

func (android Android) call() {
    fmt.Println("安卓手機打電話...")
}

func main() {
    var phone Phone
    phone = new(IPhone)
    // 打印接口底層的類型
    fmt.Printf("phone 的類型:%T\n", phone)
    phone.call()

    fmt.Println()

    phone = new(Android)
    // 打印接口底層的類型
    fmt.Printf("phone 的類型:%T\n", phone)

    phone.call()
}

底層值爲 nil 的接口值

即便接口內的具體值爲 nil,方法仍然會被 nil 接收者調用。
在一些語言中,這會觸發一個空指針異常,但在 Go 中通常會寫一些方法來優雅地處理它

package main

import "fmt"

type Phone interface {
    call()
}

type IPhone struct {
    Name string
}

func (iphone *IPhone) call() {
    if iphone == nil {
        fmt.Println("iphone 是 nil")
    } else {
        fmt.Printf("%v 手機打電話...\n", iphone.Name)
    }
}
func main() {
    var iphone *IPhone
    // 打印接口底層的類型
    fmt.Printf("phone  的值:%v ,類型:%T\n", iphone, iphone)
    iphone.call()
}

注意: 保存了 nil 具體值的接口其自身並不爲 nil(因爲還有類型 type)。

nil 接口值

nil 接口值既不保存值也不保存具體類型。

注意:爲 nil 接口調用方法會產生運行時錯誤,因爲接口的元組內並未包含能夠指明該調用哪個 具體方法 的類型

package main

import "fmt"

type Phone interface {
    call()
}

func main() {
    var phone Phone
    // 打印接口底層的類型
    fmt.Printf("phone  的值:%v ,類型:%T\n", phone, phone)
}

// 打印輸出
phone  的值:<nil> ,類型:<nil>

空接口

指定了零個方法的接口值被稱爲:空接口

// 空接口
interface{}

空接口可保存任何類型的值。原因是:每個類型都至少實現了零個方法。

作用:空接口被用來處理未知類型的值。例如,fmt.Print 可接受類型爲 interface{} 的任意數量的參數。

package main

import "fmt"

func main() {
    var i interface{}
    describe(i)

    i = 20
    describe(i)

    i = "GO"
    describe(i)
}

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

// 打印輸出
(<nil>, <nil>)
(20, int)
(GO, string)

類型斷言

GO 類型斷言提供了訪問接口值底層具體值的方式。結構如下:

// 該語句斷言接口值 i 保存了具體類型 T,並將其底層類型爲 T 的值賦予變量 t。
// 若 i 並未保存 T 類型的值,該語句就會觸發一個恐慌(個人理解類似 Java 的異常)。
t := i.(T)

爲了判斷 一個接口值是否保存了一個特定的類型,類型斷言可返回兩個值:其底層值以及一個報告斷言是否成功的布爾值。

// 若 i 保存了一個 T,那麼 t 將會是其底層值,而 ok 爲 true。
// 否則,ok 將爲 false 而 t 將爲 T 類型的零值,程序並不會產生恐慌(個人理解類似 Java 的異常)。
t, ok := i.(T)

實例代碼:

package main

import "fmt"

func main() {
    var i interface{} = "GO"

    s := i.(string)
    fmt.Println(s)

    s, ok := i.(string)
    fmt.Println(s, ok)

    f, ok := i.(float64)
    fmt.Println(f, ok)

    // 報錯(panic)
    f = i.(float64) 
    fmt.Println(f)
}

// 打印結果
GO
GO truefalse
panic: interface conversion: interface {} is string, not float64

類型選擇

GO 類型選擇是一種按順序從幾個類型斷言中選擇分支的結構。
類型選擇與一般的 switch 語句相似,不過類型選擇中的 case 爲類型(而非值), 它們針對給定接口值所存儲的值的類型進行比較。

// 類型選擇中的聲明與類型斷言 i.(T) 的語法相同,只是具體類型 T 被替換成了關鍵字 type。
switch v := i.(type) {
    case T:
    // v 的類型爲 T
    case S:
    // v 的類型爲 S
    default:
    // 沒有匹配,v 與 i 的類型相同
}
package main

import "fmt"

func switchType(i interface{}) {
    switch i.(type) {
    case int:
        fmt.Printf("i 是int類型,值爲:%v\n", i)
    case string:
        fmt.Printf("i 是string類型,值爲:%v\n", i)
    default:
        fmt.Printf("這是 default 分支,值爲:%v\n", i)

    }
}

func main() {
    switchType(10)
    switchType("go")
    switchType(true)
}

Go 內建接口——錯誤 Error

Go 程序使用 error 值來表示錯誤狀態。
error 類型是一個內建接口:

type error interface {
    Error() string
}

error 的實例:

package main

import (
    "fmt"
    "time"
)

type MyError struct {
    Time time.Time
    Msg  string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("%v, %s", e.Time, e.Msg)
}

func run() *MyError {
    return &MyError{time.Now()"這是一個錯誤!"}
}

func main() {
    if e := run(); e != nil {
        fmt.Println(e)
    }
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/AFac_bWlcDOhmSMjfS8d7g