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

天帥發表於2021-02-28

程式設計方式

  • 上面的文章通過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: "test@qq.com"}
		// 寫庫
		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: "test@qq.com"}
	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 修改為自己的路徑

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

相關文章