深入探討Go語(yǔ)言中的預(yù)防性接口為什么是不必要的
引言
在 Go 社區(qū)中,有一種從其他語(yǔ)言帶來(lái)的常見(jiàn)模式:預(yù)防性接口(Preemptive Interface)。雖然這種模式在 Java 等語(yǔ)言中很有價(jià)值,但在 Go 中往往會(huì)成為反模式。讓我們來(lái)深入探討原因。
什么是預(yù)防性接口
預(yù)防性接口是指開(kāi)發(fā)者在實(shí)際需要抽象之前就預(yù)先定義接口的做法。這里有一個(gè)簡(jiǎn)單的例子:
// 預(yù)防性接口模式
type Logger interface {
Log(message string) error
Logf(format string, args ...interface{}) error
SetLevel(level string) error
}
type fileLogger struct {
path string
level string
}
// 返回接口而不是具體類型
func NewLogger(path string) Logger {
return &fileLogger{path: path}
}
這種模式通常被認(rèn)為是"最佳實(shí)踐",因?yàn)樗坪跄艽龠M(jìn)代碼的靈活性和可測(cè)試性。但要理解為什么這在 Go 中可能不是最佳方案,我們需要先了解類型系統(tǒng)的根本差異。
類型系統(tǒng)的差異:Java vs Go
讓我們通過(guò)一個(gè)具體的例子來(lái)說(shuō)明 Java 和 Go 在接口實(shí)現(xiàn)上的根本區(qū)別。
Java 的方式
在 Java 中,一個(gè)類必須顯式聲明它實(shí)現(xiàn)了哪些接口??催@個(gè)例子:
// 最初的代碼
public class FileStorage {
public void save(byte[] data) throws IOException {
// 保存到文件的具體實(shí)現(xiàn)
}
}
// 使用方
public class DocumentService {
private FileStorage fileStorage;
public void processDocument(byte[] content) {
fileStorage.save(content);
}
}
現(xiàn)在,如果我們想讓 DocumentService 支持多種存儲(chǔ)方式(比如同時(shí)支持文件存儲(chǔ)和云存儲(chǔ)),我們會(huì)遇到一個(gè)問(wèn)題:
// 定義新接口
public interface Storage {
void save(byte[] data) throws IOException;
}
// 即使 FileStorage 有完全相同的方法簽名
// Java 仍然會(huì)報(bào)錯(cuò),因?yàn)?FileStorage 沒(méi)有顯式實(shí)現(xiàn) Storage 接口
public class DocumentService {
private Storage storage; // 編譯錯(cuò)誤:FileStorage 沒(méi)有實(shí)現(xiàn) Storage 接口
public void processDocument(byte[] content) {
storage.save(content);
}
}
在 Java 中,我們必須采取以下方案之一:
1.修改原始類(如果我們有權(quán)限):
public class FileStorage implements Storage { // 顯式實(shí)現(xiàn)接口
@Override
public void save(byte[] data) throws IOException {
// 原有的實(shí)現(xiàn)
}
}
2.創(chuàng)建適配器類(如果無(wú)法修改原始類):
public class FileStorageAdapter implements Storage {
private FileStorage fileStorage;
public FileStorageAdapter(FileStorage fileStorage) {
this.fileStorage = fileStorage;
}
@Override
public void save(byte[] data) throws IOException {
fileStorage.save(data);
}
}
這就是為什么在 Java 中,開(kāi)發(fā)者傾向于預(yù)先定義接口 - 因?yàn)楹笃谔砑咏涌趯?shí)現(xiàn)會(huì)帶來(lái)額外的工作量。
Go 的方式
同樣的場(chǎng)景在 Go 中處理起來(lái)優(yōu)雅得多:
// 原始代碼
type FileStorage struct {}
func (f *FileStorage) Save(data []byte) error {
// 保存到文件
return nil
}
// 使用方
func ProcessDocument(fs *FileStorage, data []byte) error {
return fs.Save(data)
}
當(dāng)我們想要支持多種存儲(chǔ)方式時(shí),我們只需要:
// 定義接口
type Storage interface {
Save(data []byte) error
}
// FileStorage 自動(dòng)滿足 Storage 接口,不需要任何修改
func ProcessDocument(s Storage, data []byte) error {
return s.Save(data)
}
關(guān)鍵區(qū)別在于:
- Java 中,即使一個(gè)類有完全匹配的方法,也必須顯式聲明它實(shí)現(xiàn)了某個(gè)接口
- Go 中,只要類型有匹配的方法簽名,就自動(dòng)滿足接口,不需要顯式聲明
- Go 的這種設(shè)計(jì)使得接口可以在使用處定義,而不是在實(shí)現(xiàn)處定義
這就是為什么在 Go 中,預(yù)防性接口通常是不必要的 - 我們可以在真正需要抽象的時(shí)候才定義接口,而不會(huì)帶來(lái)任何額外的工作量。
預(yù)防性接口的負(fù)面影響
1. 接口膨脹
預(yù)防性接口往往會(huì)不必要地變得龐大:
// 不要這樣做
type Storage interface {
Save(data []byte) error
Load(id string) ([]byte, error)
Delete(id string) error
List() ([]string, error)
GetMetadata(id string) (Metadata, error)
UpdateMetadata(id string, metadata Metadata) error
// 方法越來(lái)越多...
}
2. 降低可組合性
Go 的接口系統(tǒng)在小而專注的接口上發(fā)揮最大作用:
// 這樣做更好
type Saver interface {
Save(data []byte) error
}
type Loader interface {
Load(id string) ([]byte, error)
}
// 需要時(shí)可以組合小接口
type Storage interface {
Saver
Loader
}
3. 隱藏實(shí)現(xiàn)細(xì)節(jié)
預(yù)防性接口可能使代碼更難導(dǎo)航和理解:
// 不夠清晰 - 實(shí)際實(shí)現(xiàn)在哪里?
func NewStorage() Storage {
return &mysteriousImpl{}
}
// 更清晰 - 我可以準(zhǔn)確看到我得到什么
func NewFileStorage(path string) *FileStorage {
return &FileStorage{path: path}
}
Go 的最佳實(shí)踐
接受接口,返回結(jié)構(gòu)體:這個(gè)原則在需要靈活性的地方(輸入)提供靈活性,在需要清晰性的地方(輸出)提供清晰性。
保持接口小巧:?jiǎn)畏椒ń涌谧顝?qiáng)大且易于組合:
type Reader interface {
Read(p []byte) (n int, err error)
}
在使用方定義接口:讓代碼的使用者定義他們需要的接口。
從具體開(kāi)始:從具體類型開(kāi)始,只在需要時(shí)才提取接口,比如:
- 需要在測(cè)試中模擬行為時(shí)
- 需要支持多個(gè)實(shí)現(xiàn)時(shí)
- 需要解耦包時(shí)
總結(jié)
Go 的隱式接口實(shí)現(xiàn)是一個(gè)強(qiáng)大的特性,它讓我們可以在真正需要抽象的時(shí)候才引入抽象。與 Java 不同,我們不需要預(yù)先定義接口來(lái)保證未來(lái)的靈活性。相反,我們應(yīng)該:
- 從具體類型開(kāi)始
- 在需要時(shí)才提取接口
- 保持接口小而專注
- 讓使用者定義他們需要的接口
記住:在 Go 中,好的抽象來(lái)自于實(shí)際需求,而不是對(duì)未來(lái)可能性的預(yù)期。當(dāng)你發(fā)現(xiàn)多個(gè)包都在使用相似的行為模式時(shí),那才是提取接口的好時(shí)機(jī)。
到此這篇關(guān)于深入探討Go語(yǔ)言中的預(yù)防性接口為什么是不必要的的文章就介紹到這了,更多相關(guān)Go語(yǔ)言預(yù)防性接口內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang語(yǔ)言實(shí)現(xiàn)gRPC的具體使用
本文主要介紹了Golang語(yǔ)言實(shí)現(xiàn)gRPC的具體使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08
Golang學(xué)習(xí)筆記(五):函數(shù)
這篇文章主要介紹了Golang學(xué)習(xí)筆記(五):函數(shù)的相關(guān)資料,本文講解了基本語(yǔ)法、多返回值及命名返回參數(shù)、參數(shù)傳遞:傳值與傳指針、參數(shù)傳遞:可變參數(shù)、匿名函數(shù)、函數(shù)作為值、類型等內(nèi)容,需要的朋友可以參考下2015-05-05
golang語(yǔ)言實(shí)現(xiàn)的文件上傳與文件下載功能示例
這篇文章主要介紹了golang語(yǔ)言實(shí)現(xiàn)的文件上傳與文件下載功能,結(jié)合實(shí)例形式分析了Go語(yǔ)言實(shí)現(xiàn)的文件傳輸相關(guān)操作技巧,需要的朋友可以參考下2020-02-02
Go語(yǔ)言中make和new函數(shù)的用法與區(qū)別
這篇文章介紹了Go語(yǔ)言中make和new函數(shù)的用法與區(qū)別,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07
深入了解Golang中的Slice底層實(shí)現(xiàn)
本文主要為大家詳細(xì)介紹了Golang中slice的底層實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2023-02-02
Go緩沖channel和非緩沖channel的區(qū)別說(shuō)明
這篇文章主要介紹了Go緩沖channel和非緩沖channel的區(qū)別說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04
詳解如何使用unsafe標(biāo)準(zhǔn)庫(kù)突破Golang中的類型限制
在使用c語(yǔ)言編程時(shí),常常因?yàn)轭愋偷膯?wèn)題大傷腦筋,而,golang提供了一些方式用于喜歡hack的用戶,下面我們就來(lái)講講如何使用unsafe標(biāo)準(zhǔn)庫(kù)突破Golang中的類型限制吧2024-03-03

