本篇翻譯自《Practical Go Lessons》 Chapter 16: Interfaces
1 你將在本章學到什麼?
- 什麼是型別介面?
- 如何定義介面。
- “實現一個介面”是什麼意思?
- 介面的優點
2 涵蓋的技術概念
- 介面 interface
- 具體實現 concrete implementation
- 實現一個介面
- 介面的方法集
3 介紹
剛開始程式設計時,介面似乎很難理解。通常,新手程式設計師並不能完全理解介面的潛力。本節旨在解釋什麼是介面,它的有趣之處在哪裡,以及如何建立介面。
4 介面的基本定義
- 介面是定義一組行為的契約
- 介面是一個純粹的設計物件,它們只是定義了一組行為(即方法),而沒有給出這些行為的任何實現。
- 介面是一種型別,它定義了一組方法而不實現它們
“實現” = “編寫方法的程式碼”,這是一個示例介面型別(來自標準包 io
):
type Reader interface {
Read(p []byte) (n int, err error)
}
這裡我們有一個名為 Reader
的介面型別,它指定了一種名為 Read
的方法。該方法沒有具體實現,唯一指定的是方法名稱及其簽名(引數型別和結果型別)。
4.0.0.1 介面型別的零值
介面型別的零值為 nil,例子:
var r io.Reader
log.Println(r)
// 2021/11/28 12:27:52 <nil>
5 基本示例
type Human struct {
Firstname string
Lastname string
Age int
Country string
}
type DomesticAnimal interface {
ReceiveAffection(from Human)
GiveAffection(to Human)
}
- 首先,我們宣告一個名為
Human
的型別 - 我們宣告瞭一個名為
DomesticAnimal
的新型別介面 - 這種型別的介面有一個由兩個方法組成的方法集:
ReceiveAffection
和GiveAffect
。
DomesticAnimal
是一個契約。
- 它告訴開發者,要成為
DomesticAnimal
,我們至少需要有兩種行為:ReceiveAffection
和GiveAffection
讓我們建立兩個型別:
type Cat struct {
Name string
}
type Dog struct {
Name string
}
我們有兩種新型別。為了讓他們遵守我們的介面 DomesticAnimal
的契約,
我們必須為每種型別定義介面指定的方法。
我們從 Cat
型別開始:
func (c Cat) ReceiveAffection(from Human) {
fmt.Printf("The cat named %s has received affection from Human named %s\n", c.Name, from.Firstname)
}
func (c Cat) GiveAffection(to Human) {
fmt.Printf("The cat named %s has given affection to Human named %s\n", c.Name, to.Firstname)
}
現在 Cat
型別實現了 DomesticAnimal
介面。我們現在對 Dog
型別做同樣的事情:
func (d Dog) ReceiveAffection(from Human) {
fmt.Printf("The dog named %s has received affection from Human named %s\n", d.Name, from.Firstname)
}
func (d Dog) GiveAffection(to Human) {
fmt.Printf("The dog named %s has given affection to Human named %s\n", d.Name, to.Firstname)
}
我們的 Dog
型別現在正確地實現了 DomesticAnimal
介面。現在我們可以建立一個函式,它接受一個帶有引數的介面:
func Pet(animal DomesticAnimal, human Human) {
animal.GiveAffection(human)
animal.ReceiveAffection(human)
}
Pet
函式將 DomesticAnimal
型別的介面作為第一個引數,將 Human
作為第二個引數。
在函式內部,我們呼叫了介面的兩個函式。
讓我們使用這個函式:
func main() {
// Create the Human
var john Human
john.Firstname = "John"
// Create a Cat
var c Cat
c.Name = "Maru"
// then a dog
var d Dog
d.Name = "Medor"
Pet(c, john)
Pet(d,john)
}
Dog
和Cat
型別實現了介面DomesticAnimal
的方法- 也就是說
Dog
和Cat
型別的任何變數都可以看作是DomesticAnimal
只要
Cat
實現的方法的函式簽名與介面定義一致就可以,不強制要求完全相同變數名和返回名。所以我們將函式func (c Cat) ReceiveAffection(from Human) {...}
改成func (c Cat) ReceiveAffection(f Human) {...}
也是可以的
6 編譯器在看著你!
遵守型別 T 的介面契約意味著實現介面的所有方法。讓我們試著欺騙編譯器看看會發生什麼:
// ...
// let's create a concrete type Snake
type Snake struct {
Name string
}
// we do not implement the methods ReceiveAffection and GiveAffection intentionally
//...
func main(){
var snake Snake
snake.Name = "Joe"
Pet(snake, john)
}
- 我們建立了一個新型別的
Snake
- 該型別沒有實現
DomesticAnimal
動物的任何方法 - 在主函式中,我們建立了一個新的
Snake
型別的變數 - 然後我們用這個變數作為第一個引數呼叫
Pet
函式
結果是編譯失敗:
./main.go:70:5: cannot use snake (type Snake) as type DomesticAnimal in argument to Pet:
Snake does not implement DomesticAnimal (missing GiveAffection method)
編譯器在未實現的按字母順序排列的第一個方法處檢查停止。
7 例子:database/sql/driver.Driver
我們來看看 Driver
介面(來自包database/sql/driver
)
type Driver interface {
Open(name string) (Conn, error)
}
- 存在不同種類的 SQL 資料庫,因此
Open
方法有多種實現。 - 為什麼?因為你不會使用相同的程式碼來啟動到 MySQL 資料庫和 Oracle 資料庫的連線。
- 通過構建介面,你可以定義一個可供多個實現使用的契約。
8 介面嵌入
你可以將介面嵌入到其他介面中。讓我們舉個例子:
// the Stringer type interface from the standard library
type Stringer interface {
String() string
}
// A homemade interface
type DomesticAnimal interface {
ReceiveAffection(from Human)
GiveAffection(to Human)
// embed the interface Stringer into the DomesticAnimal interface
Stringer
}
在上面的程式碼中,我們將介面 Stringer
嵌入到介面 DomesticAnimal
中。
因此,已經實現了 DomesticAnimal
的其他型別必須實現 Stringer
介面的方法。
- 通過介面嵌入,你可以在不重複的情況下向介面新增功能。
- 這也是有代價的,如果你從另一個模組嵌入一個介面,你的程式碼將與其耦合
- 其他模組介面的更改將迫使你重寫程式碼。
- 請注意,如果依賴模組遵循語義版本控制方案,則這種危險會得到緩和
- 你可以毫無畏懼地使用標準庫中的介面
9 來自標準庫的一些有用(和著名)的介面
9.1 Error 介面
type error interface {
Error() string
}
這個介面型別被大量使用,用於當函式或方法執行失敗是返會error
型別介面:
func (c *Communicator) SendEmailAsynchronously(email *Email) error {
//...
}
要建立一個 error ,我們通常呼叫: fmt.Errorf()
返回一個 error
型別的結果,或者使用 errors.New()
函式。
當然,你也可以建立實現error
介面的型別。
9.2 fmt.Stringer 介面
type Stringer interface {
String() string
}
使用 Stringer
介面,你可以定義在呼叫列印方法時如何將型別列印為字串(fmt.Errorf(),fmt.Println, fmt.Printf, fmt.Sprintf
...)
這有一個示例實現
type Human struct {
Firstname string
Lastname string
Age int
Country string
}
func (h Human) String() string {
return fmt.Sprintf("human named %s %s of age %d living in %s",h.Firstname,h.Lastname,h.Age,h.Country)
}
Human
現在實現了 Stringer
介面:
package main
func main() {
var john Human
john.Firstname = "John"
john.Lastname = "Doe"
john.Country = "USA"
john.Age = 45
fmt.Println(john)
}
輸出:
human named John Doe of age 45 living in the USA
9.3 sort.Interface 介面
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
通過在一個型別上實現 sort.Interface
介面,可以對一個型別的元素進行排序(通常,底層型別是一個切片)。
這是一個示例用法(來源:sort/example_interface_test.go):
type Person struct {
Age int
}
// ByAge implements sort.Interface for []Person based on
// the Age field.
type ByAge []Person
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
ByAge
型別實現了sort.Interface
- 底層型別是
Person
的一個切片
- 底層型別是
- 介面由三個方法組成:
Len() int
:返回集合內的元素數Less(i, j int) bool
:如果索引i
處的元素應該排在索引j
處的元素之前,則返回true
Swap(i, j int)
:交換索引i
&j
處的元素;換句話說,我們應該將位於索引j
的元素放在索引i
處,而位於索引i
的元素應該放在索引j
處。
然後我們可以使用sort.Sort
函式對ByAge
型別的變數進行排序
func main() {
people := []Person{
{"Bob", 31},
{"John", 42},
{"Michael", 17},
{"Jenny", 26},
}
sort.Sort(ByAge(people))
}
10 隱式實現
介面是隱式實現的。當你宣告一個型別時,你不必指定它實現了哪些介面。
11 PHP 和 JAVA
在其他語言中,你必須指定介面實現。
這是 Java 中的一個示例:
// JAVA
public class Cat implements DomesticAnimal{
public void receiveAffection(){
//...
}
public void giveAffection(){
//..
}
}
這是 PHP 中的另一個示例:
//PHP
<?php
class Cat implements DomesticAnimal {
public function receiveAffection():void {
// ...
}
public function giveAffection():void {
// ...
}
}
?>
你可以看到,在宣告實現介面的類時,必須新增關鍵字"implements"
。
你可能會問 Go 執行時如何處理這些隱式介面實現。我們將後面解釋介面值的機制。
12 空介面
Go 的空介面是你可以編寫的最簡單、體積更小的介面。它的方法集正好由 0 個方法組成。
interface{}
也就是說,每種型別都實現了空介面。你可能會問為什麼需要這麼無聊的空介面。根據定義,空介面值可以儲存任何型別的值。如果你想構建一個接受任何型別的方法,它會很有用。
讓我們從標準庫中舉一些例子。
- 在
log
包中,你有一個Fatal
方法,可以將任何型別的輸入變數作為輸入:
func (l *Logger) Fatal(v ...interface{}) { }
- 在
fmt
包中,我們還有許多方法將空介面作為輸入。例如Printf
函式:
func Printf(format string, a ...interface{}) (n int, err error) { }
12.1 型別轉換
接受空介面作為引數的函式通常需要知道其輸入引數的有效型別。
為此,該函式可以使用“型別開關”,這是一個 switch case 將比較型別而不是值。
這是從標準庫(檔案 runtime/error.go
,包 runtime
)中獲取的示例:
// printany prints an argument passed to panic.
// If panic is called with a value that has a String or Error method,
// it has already been converted into a string by preprintpanics.
func printany(i interface{}) {
switch v := i.(type) {
case nil:
print("nil")
case bool:
print(v)
case int:
print(v)
case int8:
print(v)
case int16:
print(v)
case int32:
print(v)
case int64:
print(v)
case uint:
print(v)
case uint8:
print(v)
case uint16:
print(v)
case uint32:
print(v)
case uint64:
print(v)
case uintptr:
print(v)
case float32:
print(v)
case float64:
print(v)
case complex64:
print(v)
case complex128:
print(v)
case string:
print(v)
default:
printanycustomtype(i)
}
}
12.2 關於空介面的使用
- 你應該非常小心地使用空介面。
- 當你別無選擇時,請使用空介面。
- 空介面不會向將使用你的函式或方法的人提供任何資訊,因此他們將不得不參考文件,這可能會令人沮喪。
你更喜歡哪種方法?
func (c Cart) ApplyCoupon(coupon Coupon) error {
//...
}
func (c Cart) ApplyCoupon2(coupon interface{}) (interface{},interface{}) {
//...
}
ApplyCoupon
方法嚴格指定它將接受和返回的型別。而 ApplyCoupon2
沒有在輸入和輸出中指定它的型別。作為呼叫方,ApplyCoupon2
的使用難度比 ApplyCoupon
大。
13 實際應用:購物車儲存
13.1 規則說明
你建立了一個電子商務網站;你必須儲存和檢索客戶購物車。必須支援以下兩種行為:
- 通過 ID 獲取購物車
- 將購物車資料放入資料庫
為這兩種行為提出一個介面。還要建立一個實現這兩個介面的型別(不要實現方法中的邏輯)。
13.2 答案
這是一個設計的介面:
type CartStore interface {
GetById(ID string) (*cart.Cart, error)
Put(cart *cart.Cart) (*cart.Cart, error)
}
實現介面的型別:
type CartStoreMySQL struct{}
func (c *CartStoreMySQL) GetById(ID string) (*cart.Cart, error) {
// implement me
}
func (c *CartStoreMySQL) Put(cart *cart.Cart) (*cart.Cart, error) {
// implement me
}
另一種實現介面的型別:
type CartStorePostgres struct{}
func (c *CartStorePostgres) GetById(ID string) (*cart.Cart, error) {
// implement me
}
func (c *CartStorePostgres) Put(cart *cart.Cart) (*cart.Cart, error) {
// implement me
}
- 你可以為你使用的每個資料庫模型建立一個特定的實現
- 新增對新資料庫引擎的支援很容易!你只需要建立一個實現介面的新型別。
14 為什麼要使用介面?
14.1 易於升級
當你在方法或函式中使用介面作為輸入時,你將程式設計為易於升級的。未來的開發人員(或未來的你)可以在不更改大部分程式碼的情況下建立新的實現。
假設你構建了一個執行資料庫讀取、插入和更新的應用程式。你可以使用兩種設計方法:
- 建立與你現在使用的資料庫引擎密切相關的型別和方法。
- 建立一個介面,列出資料庫引擎的所有操作和具體實現。
- 在第一種方法中,你建立將特定實現作為引數的方法。
- 通過這樣做,你將程式限制到一個實現。
- 在第二種方法中,你建立接受介面的方法。
- 改變實現就像建立一個實現介面的新型別一樣簡單。
14.2 提高團隊合作
團隊也可以從介面中受益。
在構建功能時,通常需要多個開發人員來完成這項工作。如果工作需要兩個團隊編寫的程式碼進行互動,他們可以就一個或多個介面達成一致。
然後,兩組開發人員可以處理他們的程式碼並使用商定的介面。他們甚至可以 mock 其他團隊的返回結果。通過這樣做,團隊不會被阻塞。
14.3 Benefit from a set of routines
在自定義型別上實現介面時,你可以不需要開發就使用的附加功能。讓我們從標準庫中舉一個例子:sort
包。這並不奇怪。這個包是用來進行排序的。這是 go
原始碼的摘錄:
// go v.1.10.1
package sort
//..
type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less reports whether the element with
// index i should sort before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}
// Sort sorts data.
// It makes one call to data.Len to determine n, and O(n*log(n)) calls to
// data.Less and data.Swap. The sort is not guaranteed to be stable.
func Sort(data Interface) {
n := data.Len()
quickSort(data, 0, n, maxDepth(n))
}
在第一行,我們宣告當前包:sort
。在接下來的幾行中,程式設計師宣告瞭一個名為 Interface
的介面。這個介面 Interface
指定了三個方法:Len、Less、Swap
。
在接下來的幾行中,函式 Sort
被宣告。它將介面型別 data
作為引數。這是一個非常有用的函式,可以對給定的資料進行排序。
我們如何在我們的一種型別上使用這個函式?實現介面
假設你有一個 User 型別:
type User struct {
firstname string
lastname string
totalTurnover float64
}
還有一個型別 Users
,它是 User
型別切片:
type Users []User
讓我們建立一個 Users
例項並用三個 User
型別的變數填充它:
user0 := User{firstname:"John", lastname:"Doe", totalTurnover:1000}
user1 := User{firstname:"Dany", lastname:"Boyu", totalTurnover:20000}
user2 := User{firstname:"Elisa", lastname:"Smith Brown", totalTurnover:70}
users := make([]Users,3)
users[0] = user0
users[1] = user1
users[2] = user2
如果我們想按營業額排序怎麼辦?我們可以從頭開始開發符合我們規範的排序演算法。或者我們可以只實現使用 sort 包中的內建函式.Sort 所需的介面。我們開始吧:
// Compute the length of the array. Easy...
func (users Users) Len() int {
return len(users)
}
// decide which instance is bigger than the other one
func (users Users) Less(i, j int) bool {
return users[i].totalTurnover < users[j].totalTurnover
}
// swap two elements of the array
func (users Users) Swap(i, j int) {
users[i], users[j] = users[j], users[i]
}
通過宣告這些函式,我們可以簡單地使用 Sort
函式:
sort.Sort(users)
fmt.Println(users)
// will output :
[{Elisa Smith Brown 70} {John Doe 1000} {Dany Boyu 20000}]
15 一點建議
- 儘量使用標準庫提供的介面
- 方法太多的介面很難實現(因為它需要編寫很多方法)。
16 隨堂測試
16.1 問題
- 舉一個介面嵌入另一個介面的例子。
- 判斷真假。嵌入介面中指定的方法不是介面方法集的一部分。
- 說出使用介面的兩個優點。
- 介面型別的零值是多少?
16.2 答案
- 舉一個介面嵌入另一個介面的例子。
type ReadWriter interface {
Reader
Writer
}
- 判斷真假。嵌入介面中指定的方法不是介面方法集的一部分。
錯。介面的方法集是由兩個部分組成:- 直接指定到介面中的方法
- 來自嵌入介面的方法
- 說出使用介面的兩個優點。
- 輕鬆地在開發人員之間拆分工作:
1.定義介面型別
2.一個人開發介面的實現
3.另一個人可以在其功能中使用介面型別
4.兩個人可以互不干擾地工作。 - 易於升級
1.當你建立一個介面時,你就建立了一個契約
2.不同的實現可以履行這個契約。
3.在一個專案的開始,通常有一個實現
4.隨著時間的推移,可能需要另一種實現方式。
- 輕鬆地在開發人員之間拆分工作:
- 介面型別的零值是多少?
nil
17 關鍵要點
- 介面就是契約
- 它指定方法(行為)而不實現它們。
type Cart interface {
GetById(ID string) (*cart.Cart, error)
Put(cart *cart.Cart) (*cart.Cart, error)
}
- 介面是一種型別(就像structs, arrays, maps,等)
- 我們將介面中指定的方法稱為介面的方法集。
- 一個型別可以實現多個介面。
- 無需明確型別實現了哪個介面
- 與其他需要宣告它的語言(PHP、Java 等)相反
- 一個介面可能嵌入到另一個介面中;在這種情況下,嵌入的介面方法被新增到介面中。
- 介面型別可以像任何其他型別一樣使用
- 介面型別的零值為
nil
。 - 任何型別實現空介面
interface{}
- 空介面指定了 0 個方法
- 要獲取空介面的具體型別,您可以使用 type switch:
switch v := i.(type) {
case nil:
print("nil")
case bool:
print(v)
case int:
print(v)
}
- 當我們可以通過各種方式實現一個行為時,我們或許可以建立一個介面。
- 例如:儲存(我們可以使用 MySQL、Postgres、DynamoDB、Redis 資料庫來儲存相同的資料)