日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > windows >内容正文

windows

Go 接口-契约介绍

發布時間:2023/11/16 windows 48 coder
生活随笔 收集整理的這篇文章主要介紹了 Go 接口-契约介绍 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Go 接口-契約介紹

目錄
  • Go 接口-契約介紹
    • 一、接口基本介紹
      • 1.1 接口類型介紹
      • 1.2 為什么要使用接口
      • 1.3 面向接口編程
      • 1.4 接口的定義
    • 二、空接口
      • 2.1 空接口的定義
      • 2.2 空接口的應用
        • 2.2.1 空接口作為函數的參數
        • 2.2.2 空接口作為map的值
      • 2.3 接口類型變量
      • 2.4 類型斷言
    • 三、盡量定義“小接口”
      • 3.1 “小接口”介紹
      • 3.2 小接口優勢
        • 3.2.1 第一點:接口越小,抽象程度越高
        • 3.2.2 第二點:小接口易于實現和測試
    • 四、定義小接口,可以遵循的幾點
      • 4.1 首先,別管接口大小,先抽象出接口
      • 4.2 第二,將大接口拆分為小接口
      • 4.3 最后,我們要注意接口的單一契約職責

一、接口基本介紹

1.1 接口類型介紹

接口是一種抽象類型,它定義了一組方法的契約,它規定了需要實現的所有方法。是由 typeinterface 關鍵字定義的一組方法集合,其中,方法集合唯一確定了這個接口類型所表示的接口。

一個接口類型通常由一組方法簽名組成,這些方法定義了對象必須實現的操作。接口的方法簽名包括方法的名稱、輸入參數、返回值等信息,但不包括方法的實際實現。例如:

type Writer interface {
    Write([]byte) (int, error)
}

上面的代碼定義了一個名為 Writer 的接口,它有一個 Write 方法,該方法接受一個 []byte 類型的參數并返回兩個值,一個整數和一個錯誤。任何類型只要實現了這個 Write 方法的簽名,就可以被認為是 Writer 接口的實現。

總之,Go語言提倡面向接口編程。

1.2 為什么要使用接口

現在假設我們的代碼世界里有很多小動物,下面的代碼片段定義了貓和狗,它們餓了都會叫。

package main

import "fmt"

type Cat struct{}

func (c Cat) Say() {
	fmt.Println("喵喵喵~")
}

type Dog struct{}

func (d Dog) Say() {
	fmt.Println("汪汪汪~")
}

func main() {
	c := Cat{}
	c.Say()
	d := Dog{}
	d.Say()
}

這個時候又跑來了一只羊,羊餓了也會發出叫聲。

type Sheep struct{}

func (s Sheep) Say() {
	fmt.Println("咩咩咩~")
}

我們接下來定義一個餓肚子的場景。

// MakeCatHungry 貓餓了會喵喵喵~
func MakeCatHungry(c Cat) {
	c.Say()
}

// MakeSheepHungry 羊餓了會咩咩咩~
func MakeSheepHungry(s Sheep) {
	s.Say()
}

接下來會有越來越多的小動物跑過來,我們的代碼世界該怎么拓展呢?

在餓肚子這個場景下,我們可不可以把所有動物都當成一個“會叫的類型”來處理呢?當然可以!使用接口類型就可以實現這個目標。 我們的代碼其實并不關心究竟是什么動物在叫,我們只是在代碼中調用它的Say()方法,這就足夠了。

我們可以約定一個Sayer類型,它必須實現一個Say()方法,只要餓肚子了,我們就調用Say()方法。

type Sayer interface {
    Say()
}

然后我們定義一個通用的MakeHungry函數,接收Sayer類型的參數。

// MakeHungry 餓肚子了...
func MakeHungry(s Sayer) {
	s.Say()
}

我們通過使用接口類型,把所有會叫的動物當成Sayer類型來處理,只要實現了Say()方法都能當成Sayer類型的變量來處理。

var c cat
MakeHungry(c)
var d dog
MakeHungry(d)

在電商系統中我們允許用戶使用多種支付方式(支付寶支付、微信支付、銀聯支付等),我們的交易流程中可能不太在乎用戶究竟使用什么支付方式,只要它能提供一個實現支付功能的Pay方法讓調用方調用就可以了。

再比如我們需要在某個程序中添加一個將某些指標數據向外輸出的功能,根據不同的需求可能要將數據輸出到終端、寫入到文件或者通過網絡連接發送出去。在這個場景下我們可以不關注最終輸出的目的地是什么,只需要它能提供一個Write方法讓我們把內容寫入就可以了。

Go語言中為了解決類似上面的問題引入了接口的概念,接口類型區別于我們之前章節中介紹的那些具體類型,讓我們專注于該類型提供的方法,而不是類型本身。使用接口類型通常能夠讓我們寫出更加通用和靈活的代碼。

1.3 面向接口編程

PHP、Java等語言中也有接口的概念,不過在PHP和Java語言中需要顯式聲明一個類實現了哪些接口,在Go語言中使用隱式聲明的方式實現接口。只要一個類型實現了接口中規定的所有方法,那么它就實現了這個接口。

Go語言中的這種設計符合程序開發中抽象的一般規律,例如在下面的代碼示例中,我們的電商系統最開始只設計了支付寶一種支付方式:

type ZhiFuBao struct {
	// 支付寶
}

// Pay 支付寶的支付方法
func (z *ZhiFuBao) Pay(amount int64) {
  fmt.Printf("使用支付寶付款:%.2f元。\n", float64(amount/100))
}

// Checkout 結賬
func Checkout(obj *ZhiFuBao) {
	// 支付100元
	obj.Pay(100)
}

func main() {
	Checkout(&ZhiFuBao{})
}

隨著業務的發展,根據用戶需求添加支持微信支付。

type WeChat struct {
	// 微信
}

// Pay 微信的支付方法
func (w *WeChat) Pay(amount int64) {
	fmt.Printf("使用微信付款:%.2f元。\n", float64(amount/100))
}

在實際的交易流程中,我們可以根據用戶選擇的支付方式來決定最終調用支付寶的Pay方法還是微信支付的Pay方法。

// Checkout 支付寶結賬
func CheckoutWithZFB(obj *ZhiFuBao) {
	// 支付100元
	obj.Pay(100)
}

// Checkout 微信支付結賬
func CheckoutWithWX(obj *WeChat) {
	// 支付100元
	obj.Pay(100)
}

實際上,從上面的代碼示例中我們可以看出,我們其實并不怎么關心用戶選擇的是什么支付方式,我們只關心調用Pay方法時能否正常運行。這就是典型的“不關心它是什么,只關心它能做什么”的場景。

在這種場景下我們可以將具體的支付方式抽象為一個名為Payer的接口類型,即任何實現了Pay方法的都可以稱為Payer類型。

// Payer 包含支付方法的接口類型
type Payer interface {
	Pay(int64)
}

此時只需要修改下原始的Checkout函數,它接收一個Payer類型的參數。這樣就能夠在不修改既有函數調用的基礎上,支持新的支付方式。

// Checkout 結賬
func Checkout(obj Payer) {
	// 支付100元
	obj.Pay(100)
}

func main() {
	Checkout(&ZhiFuBao{}) // 之前調用支付寶支付

	Checkout(&WeChat{}) // 現在支持使用微信支付
}

像類似的例子在我們編程過程中會經常遇到:

  • 比如一個網上商城可能使用支付寶、微信、銀聯等方式去在線支付,我們能不能把它們當成“支付方式”來處理呢?
  • 比如三角形,四邊形,圓形都能計算周長和面積,我們能不能把它們當成“圖形”來處理呢?
  • 比如滿減券、立減券、打折券都屬于電商場景下常見的優惠方式,我們能不能把它們當成“優惠券”來處理呢?

接口類型是Go語言提供的一種工具,在實際的編碼過程中是否使用它由你自己決定,但是通常使用接口類型可以使代碼更清晰易讀。

1.4 接口的定義

每個接口類型由任意個方法簽名組成,接口的定義格式如下:

type 接口類型名 interface{
    方法名1( 參數列表1 ) 返回值列表1
    方法名2( 參數列表2 ) 返回值列表2
    …
}

其中:

  • 接口類型名:Go語言的接口在命名時,一般會在單詞后面添加er,如有寫操作的接口叫Writer,有關閉操作的接口叫closer等。接口名最好要能突出該接口的類型含義。
  • 方法名:當方法名首字母是大寫且這個接口類型名首字母也是大寫時,這個方法可以被接口所在的包(package)之外的代碼訪問。
  • 參數列表、返回值列表:參數列表和返回值列表中的參數變量名可以省略。

下面是一個典型的接口類型 MyInterface 的定義:

type MyInterface interface {
    M1(int) error
    M2(io.Writer, ...string)
}

通過這個定義,我們可以看到,接口類型 MyInterface 所表示的接口的方法集合,包含兩個方法 M1M2之所以稱 M1M2 為“方法”,更多是從這個接口的實現者的角度考慮的。但從上面接口類型聲明中各個“方法”的形式上來看,這更像是不帶有 func 關鍵字的函數名 + 函數簽名(參數列表 + 返回值列表)的組合。

在接口類型的方法集合中聲明的方法,它的參數列表不需要寫出形參名字,返回值列表也是如此。也就是說,方法的參數列表中形參名字與返回值列表中的具名返回值,都不作為區分兩個方法的憑據。

比如下面的 MyInterface 接口類型的定義與上面的 MyInterface 接口類型定義都是等價的:

type MyInterface interface {
    M1(a int) error
    M2(w io.Writer, strs ...string)
}

type MyInterface interface {
    M1(n int) error
    M2(w io.Writer, args ...string)
}

不過,Go 語言要求接口類型聲明中的方法必須是具名的,并且方法名字在這個接口類型的方法集合中是唯一的。前面我們在學習類型嵌入時就學到過:Go 1.14 版本以后,Go 接口類型允許嵌入的不同接口類型的方法集合存在交集,但前提是交集中的方法不僅名字要一樣,它的方法簽名部分也要保持一致,也就是參數列表與返回值列表也要相同,否則 Go 編譯器照樣會報錯。

比如下面示例中 Interface3 嵌入了 Interface1Interface2,但后兩者交集中的 M1 方法的函數簽名不同,導致了編譯出錯:

type Interface1 interface {
    M1()
}
type Interface2 interface {
    M1(string) 
    M2()
}

type Interface3 interface{
    Interface1
    Interface2 // 編譯器報錯:duplicate method M1
    M3()
}

上面舉的例子中的方法都是首字母大寫的導出方法,所以在 Go 接口類型的方法集合中放入首字母小寫的非導出方法也是合法的,并且我們在 Go 標準庫中也找到了帶有非導出方法的接口類型定義,比如 context 包中的 canceler 接口類型,它的代碼如下:

// $GOROOT/src/context.go

// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}

但這樣的例子并不多。通過對標準庫這為數不多的例子,我們可以看到,如果接口類型的方法集合中包含非導出方法,那么這個接口類型自身通常也是非導出的,它的應用范圍也僅局限于包內。不過,在日常實際編碼過程中,我們極少使用這種帶有非導出方法的接口類型,我們簡單了解一下就可以了。

二、空接口

除了上面這種常規情況,還有空接口(empty interface)類型這種特殊情況。

2.1 空接口的定義

空接口是指沒有定義任何方法的接口類型。因此任何類型都可以視為實現了空接口。也正是因為空接口類型的這個特性,空接口類型的變量可以存儲任意類型的值。

比如下面的 EmptyInterface 接口類型:

type EmptyInterface interface {

}

這個方法集合為空的接口類型就被稱為空接口類型,但通常我們不需要自己顯式定義這類空接口類型,我們直接使用 interface{} 這個類型字面值作為所有空接口類型的代表就可以了。

2.2 空接口的應用

2.2.1 空接口作為函數的參數

空接口(interface{})作為函數的參數是一種非常靈活的方式,因為它可以接受任何類型的參數。這在處理未知類型的數據或編寫通用函數時非常有用。以下是一個示例,展示了如何使用空接口作為函數參數:

package main

import "fmt"

func PrintValue(value interface{}) {
    fmt.Println(value)
}

func main() {
    PrintValue(42)                 // 整數
    PrintValue("Hello, Go!")       // 字符串
    PrintValue(3.14159)            // 浮點數
    PrintValue([]int{1, 2, 3})     // 切片
}

在上面的示例中,PrintValue 函數接受一個空接口類型的參數,這意味著它可以接受任何類型的值。在 main 函數中,我們調用 PrintValue 函數并傳遞不同類型的參數,它們都可以被正確處理和打印。

2.2.2 空接口作為map的值

空接口也可以用作map的值類型,這使得map可以存儲不同類型的值。這在需要將各種類型的數據關聯到特定鍵時非常有用。以下是一個示例:

package main

import "fmt"

func main() {
    data := make(map[string]interface{})

    data["name"] = "Alice"
    data["age"] = 30
    data["isStudent"] = false

    fmt.Println(data["name"])       // 輸出: Alice
    fmt.Println(data["age"])        // 輸出: 30
    fmt.Println(data["isStudent"])  // 輸出: false
}

在上面的示例中,我們創建了一個map,其中值的類型是interface{},這意味著map可以存儲不同類型的值。我們使用字符串鍵將字符串、整數和布爾值關聯到map中,并在后續通過鍵來訪問這些值。

2.3 接口類型變量

接口類型一旦被定義后,它就和其他 Go 類型一樣可以用于聲明變量,比如:

var err error   // err是一個error接口類型的實例變量
var r io.Reader // r是一個io.Reader接口類型的實例變量

這些類型為接口類型的變量被稱為接口類型變量,如果沒有被顯式賦予初值,接口類型變量的默認值為 nil如果要為接口類型變量顯式賦予初值,我們就要為接口類型變量選擇合法的右值。

Go 規定:如果一個類型 T 的方法集合是某接口類型 I 的方法集合的等價集合或超集,我們就說類型 T 實現了接口類型 I,那么類型 T 的變量就可以作為合法的右值賦值給接口類型 I 的變量。

如果一個變量的類型是空接口類型,由于空接口類型的方法集合為空,這就意味著任何類型都實現了空接口的方法集合,所以我們可以將任何類型的值作為右值,賦值給空接口類型的變量,比如下面例子:

var i interface{} = 15 // ok
i = "hello, golang" // ok
type T struct{}
var t T
i = t  // ok
i = &t // ok

空接口類型的這一可接受任意類型變量值作為右值的特性,讓它成為 Go 加入泛型語法之前唯一一種具有“泛型”能力的語法元素,包括 Go 標準庫在內的一些通用數據結構與算法的實現,都使用了空類型 interface{}作為數據元素的類型,這樣我們就無需為每種支持的元素類型單獨做一份代碼拷貝了。

2.4 類型斷言

Go 語言還支持接口類型變量賦值的“逆操作”,也就是通過接口類型變量“還原”它的右值的類型與值信息,這個過程被稱為“類型斷言(Type Assertion)”。類型斷言通常使用下面的語法形式:

v, ok := i.(T) 

其中 i 是某一個接口類型變量,如果 T 是一個非接口類型且 T 是想要還原的類型,那么這句代碼的含義就是斷言存儲在接口類型變量 i 中的值的類型為 T

如果接口類型變量 i 之前被賦予的值確為 T 類型的值,那么這個語句執行后,左側“comma, ok”語句中的變量 ok 的值將為 true,變量 v 的類型為 T,它的值會是之前變量 i 的右值。如果 i 之前被賦予的值不是 T 類型的值,那么這個語句執行后,變量 ok 的值為 false,變量 v 的類型還是那個要還原的類型,但它的值是類型 T 的零值。

類型斷言也支持下面這種語法形式:

v := i.(T)

但在這種形式下,一旦接口變量 i 之前被賦予的值不是 T 類型的值,那么這個語句將拋出 panic。如果變量 i 被賦予的值是 T 類型的值,那么變量 v 的類型為 T,它的值就會是之前變量 i 的右值。由于可能出現 panic,所以我們并不推薦使用這種類型斷言的語法形式。

為了加深你的理解,接下來我們通過一個例子來直觀看一下類型斷言的語義:

var a int64 = 13
var i interface{} = a
v1, ok := i.(int64) 
fmt.Printf("v1=%d, the type of v1 is %T, ok=%t\n", v1, v1, ok) // v1=13, the type of v1 is int64, ok=true
v2, ok := i.(string)
fmt.Printf("v2=%s, the type of v2 is %T, ok=%t\n", v2, v2, ok) // v2=, the type of v2 is string, ok=false
v3 := i.(int64) 
fmt.Printf("v3=%d, the type of v3 is %T\n", v3, v3) // v3=13, the type of v3 is int64
v4 := i.([]int) // panic: interface conversion: interface {} is int64, not []int
fmt.Printf("the type of v4 is %T\n", v4) 

你可以看到,這個例子的輸出結果與我們之前講解的是一致的。

在這段代碼中,如果 v, ok := i.(T) 中的 T 是一個接口類型,那么類型斷言的語義就會變成:斷言 i 的值實現了接口類型 T。如果斷言成功,變量 v 的類型為 i 的值的類型,而并非接口類型 T。如果斷言失敗,v 的類型信息為接口類型 T,它的值為 nil,下面我們再來看一個 T 為接口類型的示例:

type MyInterface interface {
    M1()
}

type T int
               
func (T) M1() {
    println("T's M1")
}              
               
func main() {  
    var t T    
    var i interface{} = t
    v1, ok := i.(MyInterface)
    if !ok {   
        panic("the value of i is not MyInterface")
    }          
    v1.M1()    
    fmt.Printf("the type of v1 is %T\n", v1) // the type of v1 is main.T
               
    i = int64(13)
    v2, ok := i.(MyInterface)
    fmt.Printf("the type of v2 is %T\n", v2) // the type of v2 is <nil>
    // v2 = 13 //  cannot use 1 (type int) as type MyInterface in assignment: int does not implement MyInterface (missing M1   method) 
}

我們看到,通過the type of v2 is <nil>,我們其實是看不出斷言失敗后的變量 v2 的類型的,但通過最后一行代碼的編譯器錯誤提示,我們能清晰地看到 v2 的類型信息為 MyInterface

其實,接口類型的類型斷言還有一個變種,那就是 type switch ,這個你可以去看看【go 流程控制之switch 語句介紹】。

三、盡量定義“小接口”

3.1 “小接口”介紹

接口類型的背后,是通過把類型的行為抽象成契約,建立雙方共同遵守的約定,這種契約將雙方的耦合降到了最低的程度。和生活工作中的契約有繁有簡,簽署方式多樣一樣,代碼間的契約也有多有少,有大有小,而且達成契約的方式也有所不同。 而 Go 選擇了去繁就簡的形式,這主要體現在以下兩點上:

  • 隱式契約,無需簽署,自動生效:Go 語言中接口類型與它的實現者之間的關系是隱式的,不需要像其他語言(比如 Java)那樣要求實現者顯式放置“implements”進行修飾,實現者只需要實現接口方法集合中的全部方法便算是遵守了契約,并立即生效了。
  • 更傾向于“小契約”:這點也不難理解。你想,如果契約太繁雜了就會束縛了手腳,缺少了靈活性,抑制了表現力。所以 Go 選擇了使用“小契約”,表現在代碼上就是盡量定義小接口,即方法個數在 1~3 個之間的接口。Go 語言之父 Rob Pike 曾說過的“接口越大,抽象程度越弱”,這也是 Go 社區傾向定義小接口的另外一種表述。

Go 對小接口的青睞在它的標準庫中體現得淋漓盡致,這里我給出了標準庫中一些我們日常開發中常用的接口的定義:

// $GOROOT/src/builtin/builtin.go
type error interface {
    Error() string
}

// $GOROOT/src/io/io.go
type Reader interface {
    Read(p []byte) (n int, err error)
}

// $GOROOT/src/net/http/server.go
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

type ResponseWriter interface {
    Header() Header
    Write([]byte) (int, error)
    WriteHeader(int)
}

我們看到,上述這些接口的方法數量在 1~3 個之間,這種“小接口”的 Go 慣例也已經被 Go 社區項目廣泛采用。我統計了早期版本的 Go 標準庫(Go 1.13 版本)、Docker 項目(Docker 19.03 版本)以及 Kubernetes 項目(Kubernetes 1.17 版本)中定義的接口類型方法集合中方法數量,你可以看下:

從圖中我們可以看到,無論是 Go 標準庫,還是 Go 社區知名項目,它們基本都遵循了“盡量定義小接口”的慣例,接口方法數量在 1~3 范圍內的接口占了絕大多數。那么在編碼層面,小接口究竟有哪些優勢呢?

3.2 小接口優勢

3.2.1 第一點:接口越小,抽象程度越高

計算機程序本身就是對真實世界的抽象與再建構。抽象就是對同類事物去除它具體的、次要的方面,抽取它相同的、主要的方面。不同的抽象程度,會導致抽象出的概念對應的事物的集合不同。抽象程度越高,對應的集合空間就越大;抽象程度越低,也就是越具像化,更接近事物真實面貌,對應的集合空間越小。

我們舉一個生活中的簡單例子。你可以看下這張示意圖,它是對生活中不同抽象程度的形象詮釋:

這張圖中我們分別建立了三個抽象:

  • 會飛的。這個抽象對應的事物集合包括:蝴蝶、蜜蜂、麻雀、天鵝、鴛鴦、海鷗和信天翁;
  • 會游泳的。它對應的事物集合包括:鴨子、海豚、人類、天鵝、鴛鴦、海鷗和信天翁;
  • 會飛且會游泳的。這個抽象對應的事物集合包括:天鵝、鴛鴦、海鷗和信天翁。

我們看到,“會飛的”、“會游泳的”這兩個抽象對應的事物集合,要大于“會飛且會游泳的”所對應的事物集合空間,也就是說“會飛的”、“會游泳的”這兩個抽象程度更高。

我們將上面的抽象轉換為 Go 代碼看看:

// 會飛的
type Flyable interface {
  Fly()
}

// 會游泳的
type Swimable interface {
  Swim()
}

// 會飛且會游泳的
type FlySwimable interface {
  Flyable
  Swimable
}

我們用上述定義的接口替換上圖中的抽象,再得到這張示意圖:

我們可以直觀地看到,這張圖中的 Flyable 只有一個 Fly 方法,FlySwimable 則包含兩個方法 FlySwim。我們看到,具有更少方法的 Flyable 的抽象程度相對于 FlySwimable 要高,包含的事物集合(7 種動物)也要比 FlySwimable 的事物集合(4 種動物)大。也就是說,接口越小(接口方法少),抽象程度越高,對應的事物集合越大。

而這種情況的極限恰恰就是無方法的空接口 interface{},空接口的這個抽象對應的事物集合空間包含了 Go 語言世界的所有事物。

3.2.2 第二點:小接口易于實現和測試

Go 推崇通過組合的方式構建程序。Go 開發人員一般會嘗試通過嵌入其他已有接口類型的方式來構建新接口類型,就像通過嵌入 io.Reader 和 io.Writer 構建 io.ReadWriter 那樣。

那構建時,如果有眾多候選接口類型供我們選擇,我們會怎么選擇呢?

顯然,我們會選擇那些新接口類型需要的契約職責,同時也要求不要引入我們不需要的契約職責。在這樣的情況下,擁有單一或少數方法的小接口便更有可能成為我們的目標,而那些擁有較多方法的大接口,可能會因引入了諸多不需要的契約職責而被放棄。由此可見,小接口更契合 Go 的組合思想,也更容易發揮出組合的威力。

四、定義小接口,可以遵循的幾點

保持簡單有時候比復雜更難。小接口雖好,但如何定義出小接口是擺在所有 Gopher 面前的一道難題。這道題沒有標準答案,但有一些點可供我們在實踐中考量遵循。

4.1 首先,別管接口大小,先抽象出接口

要設計和定義出小接口,前提是需要先有接口。

Go 語言還比較年輕,它的設計哲學和推崇的編程理念可能還沒被廣大 Gopher 100% 理解、接納和應用于實踐當中,尤其是 Go 所推崇的基于接口的組合思想。

盡管接口不是 Go 獨有的,但專注于接口是編寫強大而靈活的 Go 代碼的關鍵。因此,在定義小接口之前,我們需要先針對問題領域進行深入理解,聚焦抽象并發現接口,就像下圖所展示的那樣,先針對領域對象的行為進行抽象,形成一個接口集合:

初期,我們先不要介意這個接口集合中方法的數量,因為對問題域的理解是循序漸進的,在第一版代碼中直接定義出小接口可能并不現實。而且,標準庫中的 io.Readerio.Writer 也不是在 Go 剛誕生時就有的,而是在發現對網絡、文件、其他字節數據處理的實現十分相似之后才抽象出來的。并且越偏向業務層,抽象難度就越高,這或許也是前面圖中 Go 標準庫小接口(1~3 個方法)占比略高于 Docker 和 Kubernetes 的原因。

4.2 第二,將大接口拆分為小接口

有了接口后,我們就會看到接口被用在了代碼的各個地方。一段時間后,我們就來分析哪些場合使用了接口的哪些方法,是否可以將這些場合使用的接口的方法提取出來,放入一個新的小接口中,就像下面圖示中的那樣:

這張圖中的大接口 1 定義了多個方法,一段時間后,我們發現方法 1 和方法 2 經常用在場合 1 中,方法 3 和方法 4 經常用在場合 2 中,方法 5 和方法 6 經常用在場合 3 中,大接口 1 的方法呈現出一種按業務邏輯自然分組的狀態。

這個時候我們可以將這三組方法分別提取出來放入三個小接口中,也就是將大接口 1 拆分為三個小接口 A、B 和 C。拆分后,原應用場合 1~3 使用接口 1 的地方就可以無縫替換為接口 A、B、C 了。

4.3 最后,我們要注意接口的單一契約職責

那么,上面已經被拆分成的小接口是否需要進一步拆分,直至每個接口都只有一個方法呢?這個依然沒有標準答案,不過你依然可以考量一下現有小接口是否需要滿足單一契約職責,就像 io.Reader 那樣。如果需要,就可以進一步拆分,提升抽象程度。

總結

以上是生活随笔為你收集整理的Go 接口-契约介绍的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。