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 修改為自己的路徑

當我們定義了一個空介面: