Go語言學習之路-11-方法與介面
編程方式
- 上面的文章通過func函數,使我們可以重複的使用程式碼,稱之為函數式編程
- 面向對象編程:通過對象 + 方法 ,讓操作基於一個對象,而不只是來回的掉函數(並且可以使用面向對象的其他優點)
面向對象的優點這裡不過多的贅述,感興趣的自己看下
舉個最簡單的例子:
func 吃飯(){}
func 睡覺(){}
func 打豆豆(){}
// 如果是小明要吃飯,睡覺、打豆豆,如果用函數的話只能傳參!來表示吃飯的是誰、睡覺的是誰,通過函數操作
// 如果是通過對象和方法呢?
xiaohong.吃飯()、xiaohong.睡覺()、xiaohong.打豆豆() // 通過對象來觸發動作、區別於過程和函數,它的操作是某一個對象
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 (接收器變數 接收器類型) 方法名(參數列表) (返回參數) {函數體}
// ShowString 普通的函數接收一個ShowString的類型參數
func showString(m MyInt) {
fmt.Printf("當前對象的值是:%d\n", m)
}
// ShowString MyInt的ShowString方法根據對象值輸出指定字元串
func (m MyInt) ShowString() {
fmt.Printf("當前對象的值是:%d\n", m)
}
接收器: 方法作用的目標(類型和方法的綁定)
func (接收器變數 接收器類型) 方法名(參數列表) (返回參數) {
函數體
}
備註:
- 接收器變數:接收器中的參數變數名在命名時,官方建議使用接收器類型名的第一個小寫字母,而不是 self、this 之類的命名。例如,Socket 類型的接收器變數應該命名為 s,Connector 類型的接收器變數應該命名為 c 等
- 接收器類型:接收器類型和參數類似,可以是指針類型和非指針類型
- 方法名、參數列表、返回參數:格式與函數定義一致
例子:
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是使用組合來實現的繼承,說的更精確一點,是使用組合來代替的繼承
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是一組方法的集合,它不關心屬性(數據),只關心行為(方法),它類似規則標準
為什麼要用介面
場景: 我有一個發送簡訊告警的程式碼如下,現在來個新人要新增微信的告警
問題:
-
邏輯程式碼產生了冗餘,邏輯都是一樣的:寫庫、判斷是否發送告警、發送告警,每個類型的告警都要寫一遍
-
一點約束都沒有,不管是參數還是方法名字(增加了後期閱讀和維護成本)
介面可以搞定上面的問題
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)
}
介面的定義
type 介面類型名 interface{
方法名1( 參數列表1 ) (返回值列表1)
方法名2( 參數列表2 ) 返回值列表2
…
}
* 介面名: <p style="color:red">介面是一個類型通過type關鍵字定義</p>, 一般介面名字是er結尾且具有實際的表現意義,比如我下面的例子
* 方法名:首字母大寫package外可以訪問,否則只能在自己的包內訪問
* 參數、返回值名稱可以省略,但是類型不能省略比如: call(string) string
```go
// Alerter 告警的介面類型
type Alerter interface {
InsertAlarm()
IsAlarm() bool
SendAlarm()
}
最終實現例子:
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
介面的嵌套
介面與介面間可以通過嵌套創造出新的介面,看下面的例子
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{}
所以:空介面是指沒有定義任何方法的介面,因此任何類型都實現了空介面,如下面例子
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的元素類型,來增強他們的承載元素的靈活性
一般情況下慎用,如果用不好他會使你的程式非常脆弱
空介面作為函數的參數的類型時
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的元素類型
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則表示斷言失敗)
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 修改為自己的路徑
當我們定義了一個空介面: