1. 程式人生 > >Go語言學習之路-11-方法與介面

Go語言學習之路-11-方法與介面

[TOC] # 程式設計方式 * 上面的文章通過func函式,使我們可以重複的使用程式碼,稱之為函數語言程式設計 * 面向物件程式設計:通過物件 + 方法 ,讓操作基於一個物件,而不只是來回的掉函式(並且可以使用面向物件的其他優點) >面向物件的優點這裡不過多的贅述,感興趣的自己看下 舉個最簡單的例子: ```go func 吃飯(){} func 睡覺(){} func 打豆豆(){} // 如果是小明要吃飯,睡覺、打豆豆,如果用函式的話只能傳參!來表示吃飯的是誰、睡覺的是誰,通過函式操作 // 如果是通過物件和方法呢? xiaohong.吃飯()、xiaohong.睡覺()、xiaohong.打豆豆() // 通過物件來觸發動作、區別於過程和函式,它的操作是某一個物件 ``` # go語言物件方法 ## 自定義型別和方法 ```go package main import "fmt" func main() { var a MyInt = 1 a.ShowString() } // MyInt 自定義的int型別 type MyInt int // ShowString MyInt的ShowString方法根據物件值輸出指定字串 func (m MyInt) ShowString() { fmt.Printf("當前物件的值是:%d\n", m) } ``` 通過上面的方法可以看出,我們自定義了個型別:MyInt , 並給MyInt綁定了一個方法:

ShowString它是一個函式,仔細看下它和函式有什麼區別

* 函式定義: func 函式名(引數列表) (返回引數) {函式體} * 方法定義: func (接收器變數 接收器型別) 方法名(引數列表) (返回引數) {函式體} ```go // ShowString 普通的函式接收一個ShowString的型別引數 func showString(m MyInt) { fmt.Printf("當前物件的值是:%d\n", m) } // ShowString MyInt的ShowString方法根據物件值輸出指定字串 func (m MyInt) ShowString() { fmt.Printf("當前物件的值是:%d\n", m) } ``` ## 接收器: 方法作用的目標(型別和方法的繫結) ![](https://img2020.cnblogs.com/blog/831021/202101/831021-20210122091007037-1813897933.png) ``` func (接收器變數 接收器型別) 方法名(引數列表) (返回引數) { 函式體 } ``` 備註: * 接收器變數:接收器中的引數變數名在命名時,官方建議使用接收器型別名的第一個小寫字母,而不是 self、this 之類的命名。例如,Socket 型別的接收器變數應該命名為 s,Connector 型別的接收器變數應該命名為 c 等 * 接收器型別:接收器型別和引數類似,可以是指標型別和非指標型別 * 方法名、引數列表、返回引數:格式與函式定義一致 例子: ```go package main import "fmt" func main() { p1 := &Person{"eson", 2} p1.Eat() p1.Sleep() p1.Play("足球") } // Person 自定義的Person型別 type Person struct { name string age uint8 } // Eat Person的吃飯方法 func (p *Person) Eat() { fmt.Printf("%s正在吃飯....\n", p.name) } // Sleep Person的睡覺方法 func (p *Person) Sleep() { fmt.Printf("%s正在睡覺....\n", p.name) } // Play Person的玩遊戲方法 func (p *Person) Play(game string) { fmt.Printf("%s正在玩:%s....\n", p.name, game) } ``` ## go面向物件總結 * 任何自定義型別都可以定義方法(內建型別,介面定義方法不可以自定義方法) * 方法通過接收者方式和型別進行繫結達到面向物件 * 一般都用struct型別當做方法的接受者 & 並且通過指標來傳遞型別的值方便修改 # 方法的繼承 >
在Go中沒有extends關鍵字,也就意味著Go並沒有原生級別的繼承支援! Go是使用組合來實現的繼承,說的更精確一點,是使用組合來代替的繼承 ```go package main import "fmt" func main() { cat := &Cat{ Animal: &Animal{ Name: "cat", }, } cat.Eat() // cat is eating } // Animal 動物的結構體 type Animal struct { Name string } // Eat 方法與Animal結構體繫結 func (a *Animal) Eat() { fmt.Printf("%v is eating", a.Name) fmt.Println() } // Cat 結構體通過組合的方式實現繼承 type Cat struct { *Animal } ``` # go語言介面

在Go語言中介面(interface)是一種型別,一種抽象的型別

interface是一組方法的集合,它不關心屬性(資料),只關心行為(方法),它類似規則標準 ## 為什麼要用介面 場景: 我有一個傳送簡訊告警的程式碼如下,現在來個新人要新增微信的告警 問題: *

邏輯程式碼產生了冗餘,邏輯都是一樣的:寫庫、判斷是否傳送告警、傳送告警,每個型別的告警都要寫一遍

*

一點約束都沒有,不管是引數還是方法名字(增加了後期閱讀和維護成本)

> 介面可以搞定上面的問題 ```go package main import "fmt" func main() { var input string fmt.Scanln(&input) // 接收一個告警訊息,接收到後需要做 // 寫庫 // 判斷這個模組告警是否關閉(需要傳送) // 傳送告警 switch input { case "smse": // 簡訊告警 alarms := &SmsAlarms{ModuleName: "nginx", PhoneNumber: 1234567890} // 寫庫 alarms.InsertAlarm() isSend := alarms.IsAlarm() if isSend { // 如果需要傳送告警就傳送 alarms.SendAlarm() } case "wechat": // 簡訊告警 alarms := &WechatAlarms{ModuleName: "nginx", Account: "[email protected]"} // 寫庫 alarms.InputAlarm() isSend := alarms.IAlarm() if isSend { // 如果需要傳送告警就傳送 alarms.SAlarm() } } } // SmsAlarms 簡訊告警 type SmsAlarms struct { ModuleName string PhoneNumber int } // InsertAlarm 簡訊告警的寫庫方法 func (s *SmsAlarms) InsertAlarm() { fmt.Printf("虛擬碼邏輯:簡訊告警--->模組:%s 把告警寫入資料庫\n", s.ModuleName) } // IsAlarm 簡訊告警判斷這個模組告警是否關閉(需要傳送) func (s *SmsAlarms) IsAlarm() bool { fmt.Printf("虛擬碼邏輯:簡訊告警--->模組:%s 判斷模組是否關閉告警\n", s.ModuleName) return true } // SendAlarm 簡訊告警傳送 func (s *SmsAlarms) SendAlarm() { fmt.Printf("虛擬碼邏輯:簡訊告警--->模組:%s 告警傳送完畢....\n", s.ModuleName) } // WechatAlarms 微信告警 type WechatAlarms struct { ModuleName string Account string } // InputAlarm 微信告警的寫庫方法 func (s *WechatAlarms) InputAlarm() { fmt.Printf("虛擬碼邏輯:微信告警--->模組:%s 把告警寫入資料庫\n", s.ModuleName) } // IAlarm 簡訊告警判斷這個模組告警是否關閉(需要傳送) func (s *WechatAlarms) IAlarm() bool { fmt.Printf("虛擬碼邏輯:微信告警--->模組:%s 判斷模組是否關閉告警\n", s.ModuleName) return true } // SAlarm 簡訊告警傳送 func (s *WechatAlarms) SAlarm() { fmt.Printf("虛擬碼邏輯:微信告警--->模組:%s 告警傳送完畢....\n", s.ModuleName) } ``` ## 介面的定義 ```go type 介面型別名 interface{ 方法名1( 引數列表1 ) (返回值列表1) 方法名2( 引數列表2 ) 返回值列表2 … } * 介面名:

介面是一個型別通過type關鍵字定義

, 一般介面名字是er結尾且具有實際的表現意義,比如我下面的例子 * 方法名:首字母大寫package外可以訪問,否則只能在自己的包內訪問 * 引數、返回值名稱可以省略,但是型別不能省略比如: call(string) string ```go // Alerter 告警的介面型別 type Alerter interface { InsertAlarm() IsAlarm() bool SendAlarm() } ``` 最終實現例子: ```go package main import "fmt" func main() { var input string fmt.Scanln(&input) // 宣告告警介面變數 var alarms Alerter switch input { case "smse": // 簡訊告警 alarms = &SmsAlarms{ModuleName: "nginx", PhoneNumber: 1234567890} case "wechat": // 簡訊告警 alarms = &WechatAlarms{ModuleName: "nginx", Account: "[email protected]"} default: fmt.Printf("不要傳送告警\n") } // 統一的告警寫庫方法 alarms.InsertAlarm() // 統一判斷是否需要傳送告警 isSend := alarms.IsAlarm() if isSend { alarms.SendAlarm() } } // Alerter 告警的介面型別 type Alerter interface { InsertAlarm() IsAlarm() bool SendAlarm() } // SmsAlarms 簡訊告警 type SmsAlarms struct { ModuleName string PhoneNumber int } // InsertAlarm 簡訊告警的寫庫方法 func (s *SmsAlarms) InsertAlarm() { fmt.Printf("虛擬碼邏輯:簡訊告警--->模組:%s 把告警寫入資料庫\n", s.ModuleName) } // IsAlarm 簡訊告警判斷這個模組告警是否關閉(需要傳送) func (s *SmsAlarms) IsAlarm() bool { fmt.Printf("虛擬碼邏輯:簡訊告警--->模組:%s 判斷模組是否關閉告警\n", s.ModuleName) return true } // SendAlarm 簡訊告警傳送 func (s *SmsAlarms) SendAlarm() { fmt.Printf("虛擬碼邏輯:簡訊告警--->模組:%s 告警傳送完畢....\n", s.ModuleName) } // WechatAlarms 微信告警 type WechatAlarms struct { ModuleName string Account string } // InsertAlarm 微信告警的寫庫方法 func (s *WechatAlarms) InsertAlarm() { fmt.Printf("虛擬碼邏輯:微信告警--->模組:%s 把告警寫入資料庫\n", s.ModuleName) } // IsAlarm 微信告警判斷這個模組告警是否關閉(需要傳送) func (s *WechatAlarms) IsAlarm() bool { fmt.Printf("虛擬碼邏輯:微信告警--->模組:%s 判斷模組是否關閉告警\n", s.ModuleName) return true } // SendAlarm 微信告警傳送 func (s *WechatAlarms) SendAlarm() { fmt.Printf("虛擬碼邏輯:微信告警--->模組:%s 告警傳送完畢....\n", s.ModuleName) } ``` ##介面的作用總結 通過上面的例子可以發現,如果想傳送告警 *

首先必須遵循介面定義的方法名稱和引數,達到了約束

*

後面在想增加其他型別的告警比如郵件告警的時候,程式碼邏輯哪裡只需增加一個email告警的賦值即可,介面約束了告警怎麼玩,也簡化了重複的邏輯NICE

## 介面的巢狀 介面與介面間可以通過巢狀創造出新的介面,看下面的例子 ```go package main import "fmt" func main() { var a Animaler a = &Cat{Name: "小花"} a.Eat("貓糧") a.Walk("花園") } // Animaler 定義一動物的介面 type Animaler interface { Eater Walker } // Eater 定義一個吃的介面 type Eater interface { Eat(string) } // Walker 定義一個行走的介面 type Walker interface { Walk(string) } // Cat 定義一個貓的結構體 type Cat struct { Name string } // Eat 小貓的Eat方法 func (c *Cat) Eat(food string) { fmt.Printf("小貓:%s正在吃:%s\n", c.Name, food) } // Walk 小貓的Walk方法 func (c *Cat) Walk(place string) { fmt.Printf("小貓:%s正在%s行走....\n", c.Name, place) } ``` # 空介面 一個型別如果實現了一個 interface 的所有方法就說該型別實現了這個 interface,空的 interface 沒有方法,所以可以認為所有的型別都實現了 interface{} 所以:空介面是指沒有定義任何方法的介面,因此任何型別都實現了空介面,如下面例子 ```go package main import "fmt" func main() { var x interface{} s := "Hello World" x = s fmt.Printf("s的型別是: %T, x的型別是: %T, x的值是: %v\n", s, x, x) i := 100 x = i fmt.Printf("s的型別是: %T, x的型別是: %T, x的值是: %v\n", s, x, x) } ``` ## 空介面的應用場景 * 作為函式的引數型別,讓函式可以接收任意型別的型別 * 作為陣列、切片、map的元素型別,來增強他們的承載元素的靈活性 一般情況下慎用,如果用不好他會使你的程式非常脆弱 ### 空介面作為函式的引數的型別時 ```go package main import "fmt" func main() { // 可以傳遞任意型別的值 xt("Hello World!") xt(100) } func xt(x interface{}) { fmt.Printf("x的型別是: %T, x的值是:%v\n", x, x) } ``` ### 切片或者map的元素型別 ```go package main import "fmt" func main() { list := []interface{}{10, "a", []int{1, 2, 3}} fmt.Printf("%v\n", list) info := map[string]interface{}{"age": 18, "addr": "河北", "hobby": []string{"籃球", "旅遊"}} fmt.Printf("%v\n", info) } ``` ## 型別斷言 空介面可以儲存任意型別的值,如果使用了空介面,如何在執行的時候獲取它到底是什麼型別的資料呢? > x.(T) * x:表示型別為interface{}的變數 * T:表示斷言x可能是的型別 呼叫: x.(T)語法後返回兩個參: * 數第一個引數是x轉化為T型別後的變數 * 第二個值是一個布林值(為true則表示斷言成功,為false則表示斷言失敗) ```go package main import "fmt" func main() { var x interface{} x = "Hello World" // x.(T) v, ok := x.(string) if ok { fmt.Printf("型別斷言:string, 它的值是:%v\n", v) } else { fmt.Printf("%v\n", ok) } } ``` ### 型別斷言的本質(感興趣的可以看下沒必要深究) 靜態語言在編寫、編譯的時候可以準確的知道某個變數的型別,那執行中它是如何獲取變數的型別的呢?通過型別元資料 每個型別都有自己的型別元資料,我們看看空介面它可以儲存任意型別的資料,所以只需要知道 * 儲存的型別是是什麼 * 存哪裡 > 原始碼在這裡: /usr/local/Cellar/go/1.15.8/libexec/src/runtime/type.go 修改為自己的路徑 當我們定義了一個空介面: ![](https://img2020.cnblogs.com/blog/831021/202102/831021-20210221234820072-2263126