真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

golang中的接口

0.1、索引

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)站。

1、概念

接口提供了一種指定對(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
	}
}
  1. 我們利用os.Open打開(kāi)一個(gè)文件,該函數(shù)返回一個(gè)*os.File句柄,*os.File 實(shí)現(xiàn)了 io.Reader 和 io.Writer
  2. 我們使用os.Create創(chuàng)建一個(gè)新的文件,該函數(shù)返回一個(gè)*os.File句柄
  3. 我們使用copyFile函數(shù),該函數(shù)有兩個(gè)參數(shù),source為一個(gè)實(shí)現(xiàn)io.Reader接口的參數(shù),dest為一個(gè)實(shí)現(xiàn) io.Writer接口的參數(shù)

該函數(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è)試。

2、什么時(shí)候使用接口

我們什么時(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()
}
  1. 實(shí)現(xiàn)sync.Locker的Lock方法
  2. 實(shí)現(xiàn)sync.Locker的Unlock方法

2.2、解耦

如果我們依賴抽象而不是具體的實(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è)試我們想要的方法。 例如,我們可以:

  • 通過(guò)集成測(cè)試使用具體實(shí)現(xiàn)
  • 通過(guò)單元測(cè)試使用模擬(或任何類型的測(cè)試替身)

2.3、限制行為

假設(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 方法中的配置,不能修改它。 因此,我們還可以出于各種原因使用接口將類型限制為特定行為。

3、接口污染

在 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)單。


本文標(biāo)題:golang中的接口
本文網(wǎng)址:http://weahome.cn/article/dsoidph.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部