https://waterflow.link/articles/
成都創(chuàng)新互聯(lián)公司專注于網(wǎng)站建設(shè)|成都網(wǎng)站維護(hù)公司|優(yōu)化|托管以及網(wǎng)絡(luò)推廣,積累了大量的網(wǎng)站設(shè)計(jì)與制作經(jīng)驗(yàn),為許多企業(yè)提供了網(wǎng)站定制設(shè)計(jì)服務(wù),案例作品覆蓋成都銅雕雕塑等行業(yè)。能根據(jù)企業(yè)所處的行業(yè)與銷售的產(chǎn)品,結(jié)合品牌形象的塑造,量身策劃品質(zhì)網(wǎng)站。
接口提供了一種指定對(duì)象行為的方法。 我們使用接口來(lái)創(chuàng)建多個(gè)對(duì)象可以實(shí)現(xiàn)的通用抽象。 Go 接口不同的原因在于它們是隱式的。 沒(méi)有像 implements 這樣的顯式關(guān)鍵字來(lái)標(biāo)記對(duì)象 A實(shí)現(xiàn)了接口B。
為了理解接口的強(qiáng)大,我們可以看下標(biāo)準(zhǔn)庫(kù)中兩個(gè)常用的接口:io.Reader 和 io.Writer。 io 包為 I/O 原語(yǔ)提供抽象。 在這些抽象中,io.Reader 從數(shù)據(jù)源讀取數(shù)據(jù),io.Writer 將數(shù)據(jù)寫(xiě)入目標(biāo)。
io.Reader 包含一個(gè) Read 方法:
type Reader interface {
Read(p []byte) (n int, err error)
}
io.Reader 接口的自定義實(shí)現(xiàn)應(yīng)該接收一個(gè)字節(jié)切片p,把數(shù)據(jù)讀取到p中并返回讀取的字節(jié)數(shù)或錯(cuò)誤。
io.Writer 定義了一個(gè)方法,Write:
type Writer interface {
Write(p []byte) (n int, err error)
}
io.Writer 的自定義實(shí)現(xiàn)應(yīng)該將來(lái)自切片的數(shù)據(jù)p寫(xiě)入底層數(shù)據(jù)流并返回寫(xiě)入的字節(jié)數(shù)或錯(cuò)誤。
因此,這兩個(gè)接口都提供了基本的抽象:
io.Reader
從一個(gè)源對(duì)象讀取數(shù)據(jù)io.Writer
將數(shù)據(jù)寫(xiě)到一個(gè)目標(biāo)對(duì)象假設(shè)我們需要實(shí)現(xiàn)一個(gè)將一個(gè)文件的內(nèi)容復(fù)制到另一個(gè)文件的函數(shù)。 我們可以創(chuàng)建一個(gè)特定的函數(shù)copyFile,它將使用 io.Reader 和 io.Writer 抽象創(chuàng)建一個(gè)更通用的函數(shù):
package main
import (
"io"
"log"
"os"
)
func main() {
// 1 打開(kāi)一個(gè)源文件
source, err := os.Open("a.txt")
if err != nil {
log.Fatal(err)
}
defer source.Close()
// 2 創(chuàng)建一個(gè)目標(biāo)文件
dest, err := os.Create("b.txt")
if err != nil {
log.Fatal(err)
}
defer dest.Close()
// 從把源數(shù)據(jù)復(fù)制到目標(biāo)
err = CopyFile(source, dest)
if err != nil {
log.Fatal(err)
}
}
// 復(fù)制
func CopyFile(source io.Reader, dest io.Writer) error {
var buffer = make([]byte, 1024)
for {
n, err := source.Read(buffer)
if err == nil {
_, err = dest.Write(buffer[:n])
if err != nil {
return err
}
}
if err == io.EOF {
_, err = dest.Write(buffer)
if err != nil {
return err
}
return nil
}
return err
}
}
該函數(shù)適用于 *os.File 參數(shù)(因?yàn)?*os.File 實(shí)現(xiàn)了 io.Reader 和 io.Writer)以及任何其他可以實(shí)現(xiàn)這些接口的類型。 例如,我們可以創(chuàng)建自己的 io.Writer 寫(xiě)入數(shù)據(jù)庫(kù),并且代碼將保持不變。 它增加了函數(shù)的通用性; 因此,他是可重用的,這很重要。
此外,為這個(gè)函數(shù)編寫(xiě)單元測(cè)試更容易,因?yàn)槲覀兛梢允褂锰峁┯杏脤?shí)現(xiàn)的字符串和字節(jié)包,而不是處理文件:
package main
import (
"bytes"
"strings"
"testing"
)
func TestCopyFile(t *testing.T) {
input := "hahahha"
source := strings.NewReader(input)
dest := bytes.NewBuffer(make([]byte, 0))
err := CopyFile(source, dest)
if err != nil {
t.Fatal(err)
}
got := dest.String()
if got != input {
t.Errorf("input is %s, got is %s, want is %s", input, got, input)
}
}
在上面例子中,source 是 *strings.Reader,而 dest 是 *bytes.Buffer。 在這里,我們?cè)诓粍?chuàng)建任何文件的情況下測(cè)試 CopyFile ,歸功于CopyFile的參數(shù)使用的是接口,只要我們參數(shù)實(shí)現(xiàn)了這倆個(gè)接口就可以運(yùn)行單元測(cè)試。
我們什么時(shí)候應(yīng)該在 Go 中創(chuàng)建接口? 讓我們看一下通常認(rèn)為接口帶來(lái)價(jià)值的三個(gè)具體用例:
2.1、通用行為
在多種類型實(shí)現(xiàn)共同行為時(shí)使用接口。 在這種情況下,我們可以分解出接口內(nèi)部的行為。 如果我們查看標(biāo)準(zhǔn)庫(kù),我們可以找到許多此類用例的示例。 例如,可以通過(guò)2個(gè)方法讓共享資源變得安全:
因此,sync包中添加了以下接口:
type Locker interface {
Lock()
Unlock()
}
該接口具有強(qiáng)大的可重用潛力,因?yàn)樗瑢?duì)任何共享資源進(jìn)行不同方式保護(hù)的常見(jiàn)行為。
我們都知道sync.Mutex是不支持鎖的可重入的,但是有時(shí)我們希望同一個(gè)協(xié)程可以給資源重復(fù)上鎖,而不會(huì)引起報(bào)錯(cuò)。因此,加鎖和解鎖就可以被抽象化,我們可以依賴 sync.Locker。
所以我們就可以很輕松的實(shí)現(xiàn)可重入鎖,像下面這樣:
package main
import (
"fmt"
"github.com/petermattis/goid"
"log"
"sync"
"sync/atomic"
)
type RecursiveMutex struct {
sync.Mutex
owner int64
recursion int32
}
// 1
func (m *RecursiveMutex) Lock() {
gid := goid.Get()
if atomic.LoadInt64(&m.owner) == gid {
m.recursion++
return
}
m.Mutex.Lock()
atomic.StoreInt64(&m.owner, gid)
m.recursion = 1
}
// 2
func (m *RecursiveMutex) Unlock() {
gid := goid.Get()
if atomic.LoadInt64(&m.owner) != gid {
panic(fmt.Sprintf("Wrong the owner (%d): %d!", m.owner, gid))
}
m.recursion--
if m.recursion != 0 {
return
}
atomic.StoreInt64(&m.owner, -1)
m.Mutex.Unlock()
}
func main() {
l := &RecursiveMutex{}
foo1(l)
}
func foo1(l *RecursiveMutex) {
log.Println("in foo")
l.Lock()
bar1(l)
l.Unlock()
}
func bar1(l *RecursiveMutex) {
l.Lock()
log.Println("in bar")
l.Unlock()
}
如果我們依賴抽象而不是具體的實(shí)現(xiàn),則可以用另一個(gè)具體實(shí)現(xiàn)取替換,甚至不必更改我們的代碼。 這就是 里氏替換原則。
解耦的好處之一可能與單元測(cè)試有關(guān)。 假設(shè)我們要實(shí)現(xiàn)一個(gè) StoreCourseware 方法來(lái)創(chuàng)建一個(gè)課件。 我們決定直接依賴具體實(shí)現(xiàn):
// 課件模型
type Courseware struct {
id int64
}
type Store struct {
}
func (s Store) StoreCourseware(courseware Courseware) error {
// 需要走數(shù)據(jù)庫(kù)
return nil
}
type CoursewareService struct {
store Store
}
func (cw CoursewareService) CreateCourseware(id int64) error {
courseware := Courseware{id: id}
return cw.store.StoreCourseware(courseware)
}
現(xiàn)在,如果我們想測(cè)試這個(gè)方法怎么辦? 因?yàn)?CoursewareService 依賴于實(shí)際實(shí)現(xiàn)來(lái)存儲(chǔ)課件,所以我們不得不通過(guò)集成測(cè)試對(duì)其進(jìn)行測(cè)試,這需要啟動(dòng) MySQL 實(shí)例(除非我們使用諸如 go-sqlmock 之類的替代技術(shù),但這不是本節(jié)要討論的內(nèi)容)。 盡管集成測(cè)試很有幫助,但這并不總是我們想要做的。 為了使我們代碼有更大的靈活性,我們應(yīng)該將 CoursewareService 與實(shí)際實(shí)現(xiàn)分離,這可以通過(guò)如下接口完成:
// 課件模型
type Courseware struct {
id int64
}
// 添加課件的一種實(shí)現(xiàn)
type Store struct {
}
func (s Store) StoreCourseware(courseware Courseware) error {
// 需要走數(shù)據(jù)庫(kù)
return nil
}
// 添加課件的接口,只要實(shí)現(xiàn)接口不管走mysql還是內(nèi)存
type CoursewareStorer interface {
StoreCourseware (courseware Courseware) error
}
type CoursewareService struct {
store CoursewareStorer
}
func (cw CoursewareService) CreateCourseware(id int64) error {
courseware := Courseware{id: id}
return cw.store.StoreCourseware(courseware)
}
因?yàn)楝F(xiàn)在存儲(chǔ)客戶是通過(guò)一個(gè)接口完成的,這給了我們更多的靈活性來(lái)測(cè)試我們想要的方法。 例如,我們可以:
假設(shè)我們實(shí)現(xiàn)了一個(gè)自定義配置包來(lái)處理動(dòng)態(tài)配置。 我們通過(guò)一個(gè) Config 結(jié)構(gòu)保存配置,該結(jié)構(gòu)還公開(kāi)了兩種方法:Get 和 Set。 以下是該代碼的實(shí)現(xiàn):
type Config struct {
rabbitmq string
cpu int
}
func (c *Config) Rabbitmq() string {
return c.rabbitmq
}
func (c *Config) SetRabbitmq(value string) {
c.rabbitmq = value
}
現(xiàn)在,假設(shè)Config有個(gè)cpu配置,但是在我們的代碼中,我們不希望更新他,讓他只讀。 如果我們不想更改配置包,如何從語(yǔ)義上強(qiáng)制執(zhí)行此配置是只讀的? 通過(guò)創(chuàng)建一個(gè)將行為限制為只讀的抽象:
type ConfigCPUGetter interface {
Get() int
}
然后,在我們的代碼中,我們可以依賴 ConfigCPUGetter 而不是具體的實(shí)現(xiàn):
type Foo struct {
threshold ConfigCPUGetter
}
func NewFoo(threshold ConfigCPUGetter) Foo {
return Foo{threshold: threshold}
}
func (f Foo) Bar() {
threshold := f.threshold.Get()
// ...
}
在這個(gè)例子中,配置 getter 被注入到 NewFoo 工廠方法中。 它不會(huì)影響此函數(shù)的客戶端,因?yàn)樗匀豢梢栽趯?shí)現(xiàn) ConfigCPUGetter 時(shí)傳遞 Config 結(jié)構(gòu)。 然后,我們只能讀取 Bar 方法中的配置,不能修改它。 因此,我們還可以出于各種原因使用接口將類型限制為特定行為。
在 Go 項(xiàng)目中過(guò)度使用接口是很常見(jiàn)的。也許開(kāi)發(fā)人員的背景是 C# 或 Java,他們發(fā)現(xiàn)在具體類型之前創(chuàng)建接口是很自然的。然而,這不是 Go 中的工作方式。
正如我們所討論的,接口是用來(lái)創(chuàng)建抽象的。當(dāng)編程遇到抽象時(shí),主要的警告是記住應(yīng)該發(fā)現(xiàn)抽象,而不是創(chuàng)建抽象。這是什么意思?這意味著如果沒(méi)有直接的理由,我們不應(yīng)該開(kāi)始在我們的代碼中創(chuàng)建抽象。我們不應(yīng)該使用接口進(jìn)行設(shè)計(jì),而是等待具體的需求。換句話說(shuō),我們應(yīng)該在需要時(shí)創(chuàng)建接口,而不是在我們預(yù)見(jiàn)到可能需要它時(shí)。
如果我們過(guò)度使用接口,主要問(wèn)題是什么?答案是它們使代碼流更加復(fù)雜。添加無(wú)用的間接級(jí)別不會(huì)帶來(lái)任何價(jià)值;它創(chuàng)建了一個(gè)毫無(wú)價(jià)值的抽象,使代碼更難閱讀、理解和推理。如果我們沒(méi)有充分的理由添加接口,并且不清楚接口如何使代碼變得更好,我們應(yīng)該挑戰(zhàn)這個(gè)接口的目的。為什么不直接調(diào)用實(shí)現(xiàn)呢?
注意當(dāng)通過(guò)接口調(diào)用方法時(shí),我們也可能會(huì)遇到性能開(kāi)銷。它需要在哈希表的數(shù)據(jù)結(jié)構(gòu)中查找以找到接口指向的具體類型。但這在許多情況下都不是問(wèn)題,因?yàn)殚_(kāi)銷很小。
總之,在我們的代碼中創(chuàng)建抽象時(shí)我們應(yīng)該小心——應(yīng)該發(fā)現(xiàn)抽象,而不是創(chuàng)建抽象。對(duì)于我們軟件開(kāi)發(fā)人員來(lái)說(shuō),通過(guò)根據(jù)我們認(rèn)為以后可能需要的東西來(lái)猜測(cè)完美的抽象級(jí)別是什么來(lái)過(guò)度設(shè)計(jì)我們的代碼是很常見(jiàn)的。應(yīng)該避免這個(gè)過(guò)程,因?yàn)樵诖蠖鄶?shù)情況下,它會(huì)用不必要的抽象污染我們的代碼,使其閱讀起來(lái)更加復(fù)雜。
我們不要試圖抽象地解決問(wèn)題,而是解決現(xiàn)在必須解決的問(wèn)題。最后但同樣重要的是,如果不清楚接口如何使代碼變得更好,我們可能應(yīng)該考慮刪除它以使我們的代碼更簡(jiǎn)單。