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 true
0 false
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