程式設計方式
- 上面的文章通過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 修改為自己的路徑
當我們定義了一個空介面: