Go的隱式接口機制

正確使用Interface
不要照使用C++/Java等OOP語言中接口的方式去使用interface。
Go的Interface的抽象不僅可以用于dynamic-dispatch
在工程上、它最大的作用是:隔離實現和抽象、實現完全的dependency inversion
以及interface segregation(SOLID principle中的I和D)。
我們推薦大家在Client-side定義你需要的dependency的interface
即把你需要的依賴抽象為接口、而不是在實現處定義整理出一個interface。這也是Go標準庫里的通行做法。

舉一個小例子

不建議這個

package tcp// DON'T DO THIS 🚫
type Server interface {Start()
}type server struct { ... }
func (s *server) Start() { ... }
func NewServer() Server { return &server{ ... } }// ......package consumer
import "tcp"
func StartServer(s tcp.Server) { s.Start() }

建議用👇這個 才是正確的

package tcp
type Server struct { ... }
func (s *Server) Start() { ... }
func NewServer() Server { return &Server{ ... } }package consumer
// DO THIS 👍
type Server interface {Start()
}func StartServer(s Server) { s.Start() }

舉一個具體的例子

舉一個具體的例子來解釋這個Go語言接口的使用建議

這個建議的核心思想是:接口應該由使用方(客戶端/消費者)來定義、而不是由提供方(實現者)來定義。
這樣做可以更好地實現依賴倒置和接口隔離
假設我們有兩個包:

  1. notification 包:這個包負責發送通知、比如郵件通知、短信通知。
  2. user_service 包:這個包處理用戶相關的業務邏輯、比如用戶注冊后需要發送一封歡迎通知。

不建議的做法:定義在 notification 包 (提供方)

// notification/notification.go
package notificationimport "fmt"// 接口由 notification 包定義
type Notifier interface {SendNotification(recipient string, message string) error// 假設這個接口未來可能還會增加其他方法、比如 GetStatus(), Retry() 等
}// 郵件通知的具體實現
type EmailNotifier struct{}func (en *EmailNotifier) SendNotification(recipient string, message string) error {fmt.Printf("向 %s 發送郵件: %s\n", recipient, message)return nil
}func NewEmailNotifier() Notifier { // 返回接口類型return &EmailNotifier{}
}// user_service/service.go
package user_serviceimport ("example.com/project/notification" // user_service 依賴 notification 包"fmt"
)type UserService struct {notifier notification.Notifier // 依賴 notification 包定義的接口
}func NewUserService(notifier notification.Notifier) *UserService {return &UserService{notifier: notifier}
}func (s *UserService) RegisterUser(email string, username string) {fmt.Printf("用戶 %s 注冊成功。\n", username)// ...其他注冊邏輯...message := fmt.Sprintf("歡迎您,%s!", username)err := s.notifier.SendNotification(email, message) // 調用 notification.Notifier 的方法if err != nil {fmt.Printf("發送通知失敗: %v\n", err)}
}// main.go
// import (
//     "example.com/project/notification"
//     "example.com/project/user_service"
// )
// func main() {
//     emailNotifier := notification.NewEmailNotifier()
//     userService := user_service.NewUserService(emailNotifier)
//     userService.RegisterUser("test@example.com", "張三")
// }

問題分析:

  1. 強耦合:user_service 包直接依賴了 notification 包中定義的 Notifier 接口。如果 notification.Notifier 接口發生變化(比如 SendNotification 方法簽名改變、或增加了新方法)user_service 包即使不使用這些新變化、也可能需要修改。
  2. 接口可能過于寬泛:notification.Notifier 接口可能為了通用性而定義了多個方法。但 user_service 可能只需要 SendNotification 這一個功能。它被迫依賴了一個比其實際需求更大的接口。
  3. 依賴方向:高層模塊 (user_service) 依賴了底層模塊 (notification) 的抽象

建議的做法:定義在 user_service 包 (消費方)

// notification/notification.go
package notificationimport "fmt"// EmailNotifier 是一個具體的類型,它有自己的方法
// 這里不再定義 Notifier 接口
type EmailNotifier struct{}func (en *EmailNotifier) Send(recipient string, message string) error { // 方法名可以不同,但為了例子清晰,我們保持類似fmt.Printf("向 %s 發送郵件: %s\n", recipient, message)return nil
}func NewEmailNotifier() *EmailNotifier { // 返回具體類型return &EmailNotifier{}
}// 短信通知的具體實現
type SMSNotifier struct{}func (sn *SMSNotifier) Send(recipient string, message string) error {fmt.Printf("向 %s 發送短信: %s\n", recipient, message)return nil
}func NewSMSNotifier() *SMSNotifier { // 返回具體類型return &SMSNotifier{}
}// user_service/service.go
package user_serviceimport "fmt"// user_service 包定義了它自己需要的接口
// 這個接口只包含 UserService 真正需要的方法
type MessageSender interface {Send(to string, msg string) error
}type UserService struct {sender MessageSender // 依賴自己定義的 MessageSender 接口
}// 構造函數接受任何滿足 MessageSender 接口的類型
func NewUserService(s MessageSender) *UserService {return &UserService{sender: s}
}func (s *UserService) RegisterUser(email string, username string) {fmt.Printf("用戶 %s 注冊成功。\n", username)// ...其他注冊邏輯...message := fmt.Sprintf("歡迎您,%s!", username)err := s.sender.Send(email, message) // 調用 MessageSender 接口的方法if err != nil {fmt.Printf("發送通知失敗: %v\n", err)}
}// main.go
// import (
//     "example.com/project/notification"
//     "example.com/project/user_service"
// )
// func main() {
//     // 創建具體的 EmailNotifier 實例
//     emailNotifier := notification.NewEmailNotifier()
//     // emailNotifier 是 *notification.EmailNotifier 類型
//     // 它有一個 Send(recipient string, message string) error 方法
//     // 這個方法簽名與 user_service.MessageSender 接口完全匹配
//     // 因此,emailNotifier 隱式地實現了 user_service.MessageSender 接口//     userService1 := user_service.NewUserService(emailNotifier) // 可以直接傳遞
//     userService1.RegisterUser("test@example.com", "張三")//     fmt.Println("---")//     // 創建具體的 SMSNotifier 實例
//     smsNotifier := notification.NewSMSNotifier()
//     // smsNotifier 也隱式地實現了 user_service.MessageSender 接口
//     userService2 := user_service.NewUserService(smsNotifier)
//     userService2.RegisterUser("13800138000", "李四")
// }

為什么推薦的做法更好?

  1. user_service 的獨立性:
  • user_service 包現在只依賴于它自己定義的 MessageSender 接口。它不關心 notification 包內部是如何定義的、也不關心 notification 包是否有其他接口或類型。
  • 如果 notification.EmailNotifier 的其他方法(假設它有其他方法)改變了,或者 notification 包增加了一個全新的 PushNotifier,user_service 包完全不受影響,因為它只關心滿足 MessageSender 接口的 Send 方法。
  1. 明確的契約:
  • user_service 包通過 MessageSender 接口明確聲明我需要一個能做 Send(to string, msg string) error 操作的東西。
  • notification.EmailNotifier 或 notification.SMSNotifier 恰好提供了這樣一個方法、所以它們可以被用作 user_service.MessageSender。這是 Go 語言隱式接口實現的強大之處。
  1. 接口隔離原則 (ISP):
  • user_service.MessageSender 接口非常小且專注、只包含 user_service 包真正需要的方法。它沒有被 notification 包中可能存在的其他通知相關操作(如獲取狀態、重試等)所污染
  1. 依賴倒置原則 (DIP):
  • 在不推薦的做法中、高層模塊 user_service 依賴于低層模塊 notification 的抽象 (notification.Notifier)。
  • 在推薦的做法中、高層模塊 user_service 定義了自己的抽象 (user_service.MessageSender)。低層模塊 notification 的具體實現
    (notification.EmailNotifier、notification.SMSNotifier) 通過實現這個抽象來服務于高層模塊。
    依賴關系被倒置了:不是 user_service 依賴 notification 的接口、而是 notification 的實現滿足了 user_service 定義的接口

總結

  • 不推薦:提供方(如 tcp 包或 notification 包)定義接口、并讓其構造函數返回該接口類型。消費方(如 consumer 包或 user_service 包)導入提供方的包、并使用提供方定義的接口。
  • 推薦:消費方(如 consumer 包或 user_service 包)定義自己需要的接口、這個接口只包含它必需的方法。提供方(如 tcp 包或 notification 包)提供具體的結構體類型及其方法、構造函數返回具體的結構體指針。只要提供方的具體類型的方法集滿足了消費方定義的接口、就可以在消費方使用這個具體類型的實例。
    這種做法使得消費方更加獨立、靈活,也更容易測試、代碼的耦合度更低。它充分利用了 Go 語言的隱式接口特性

舉例一個再簡單一點的

我們來看一個最精簡的例子。
假設我們有兩個包:

  1. printer 包:提供一個打印功能。
  2. app 包:需要使用打印功能。

不推薦的做法 (接口在 printer 包)

// printer/printer.go
package printer// DON'T DO THIS 🚫
type PrinterAPI interface { // 接口定義在 printer 包Print(msg string)
}type consolePrinter struct{}func (cp *consolePrinter) Print(msg string) {println("PrinterAPI says:", msg)
}func NewConsolePrinter() PrinterAPI { // 返回接口return &consolePrinter{}
}// app/app.go
package appimport "example.com/printer" // 依賴 printer 包func Run(p printer.PrinterAPI) { // 使用 printer 包定義的接口p.Print("Hello from App")
}// main.go
// import (
//  "example.com/app"
//  "example.com/printer"
// )
// func main() {
//  myPrinter := printer.NewConsolePrinter()
//  app.Run(myPrinter)
// }

這里app 包依賴于 printer 包定義的 PrinterAPI 接口。

推薦的做法 (接口在 app 包)

// printer/printer.go
package printer// 這里不定義接口
type ConsolePrinter struct{} // 具體的打印機類型func (cp *ConsolePrinter) Output(data string) { // 具體的方法println("ConsolePrinter outputs:", data)
}func NewConsolePrinter() *ConsolePrinter { // 返回具體類型return &ConsolePrinter{}
}// app/app.go
package app// DO THIS 👍
type StringWriter interface { // app 包定義自己需要的接口Output(data string)
}func Run(sw StringWriter) { // 使用自己定義的接口sw.Output("Hello from App")
}// main.go
// import (
//  "example.com/app"
//  "example.com/printer"
// )
// func main() {
//  myConsolePrinter := printer.NewConsolePrinter() // *printer.ConsolePrinter 類型
//  // myConsolePrinter 有一個 Output(data string) 方法,
//  // 與 app.StringWriter 接口匹配。
//  // 所以它可以被傳遞給 app.Run()
//  app.Run(myConsolePrinter)
// }

核心區別和優勢 (推薦做法):

  1. app包定義需求:app 包說:我需要一個能 Output(string) 的東西、我叫它 StringWriter。
  2. printer包提供實現:printer.ConsolePrinter 恰好有一個名為 Output 且簽名相同的方法
  3. 解耦:
  • app 包不關心 printer 包內部有沒有其他接口、或者 ConsolePrinter 有沒有其他方法。
  • 如果 printer.ConsolePrinter 的其他不相關方法變了、app 包不受影響。
  • printer 包也不知道 app 包的存在、它只是提供了一個具有 Output 功能的類型。

這個例子中、app.StringWriter 是一個由消費者(app 包)定義的、最小化的接口。printer.ConsolePrinter 碰巧實現了這個接口(隱式地)、所以它們可以很好地協同工作、同時保持低耦合

簡潔的例子

type Speaker interface {Speak() string
}type Dog struct{}func (d Dog) Speak() string {return "Woof!"
}func makeSpeak(s Speaker) {fmt.Println(s.Speak())
}func main() {var d DogmakeSpeak(d) // ? Dog 隱式實現了 Speaker 接口
}

你并沒有顯式說 “Dog implements Speaker”
但是只要方法對上了、它就能用了

如果用 Java 實現和 Go 中“隱式接口”相同功能的代碼、需要顯式聲明接口和實現類的關系。
Java 是顯式接口實現語言
代碼如下:

public interface Speaker {String speak();
}
public class Dog implements Speaker {@Overridepublic String speak() {return "Woof!";}
}
public class Main {public static void main(String[] args) {Speaker dog = new Dog();System.out.println(dog.speak());}
}

在這里插入圖片描述

所以Java 版本不能省略 implements 關鍵字和方法重寫、這是 Java 類型系統設計的結果。
而這正是 Go 接口設計被稱為“duck typing”風格
(只要像鴨子、就認為是鴨子)的核心體現

類型斷言 vs 類型轉換
隱式接口經常和類型斷言一起使用

var x interface{} = Dog{}
dog, ok := x.(Dog)

常見面試題

  • Go 接口的實現機制是怎樣的?
  • 什么是隱式接口?Go 為什么不需要顯式 implements?
  • 如何判斷一個類型是否實現了某個接口?
  • 接口值的底層結構(接口值是如何存儲實際類型和值的)?
  • 空接口(interface{})和類型斷言的使用?
  • 使用接口是否會引入性能開銷?

出一道代碼題

type Speaker interface {Speak() string
}type Cat struct{}func (c Cat) Meow() string {return "Meow!"
}func main() {var s Speaker = Cat{}fmt.Println(s.Speak())
}

?編譯錯誤

解釋: Cat 沒有實現 Speaker 接口的方法 Speak()、所以不能賦值給接口類型 Speaker。方法名必須完全匹配

改正后的為:

type Speaker interface {Speak() string
}type Cat struct{}?將這里改正就行了
func (c Cat) Speak() string {return "Meow!"
}func main() {var s Speaker = Cat{}fmt.Println(s.Speak())
}
  • 隱式實現:不需要顯式寫 implements、只要方法簽名對上即可
  • 類型賦值:var s Speaker = Cat{} 成立是因為 Cat 實現了接口的方法

空接口 interface{} 有什么作用?請舉一個應用場景

?參考答案:
空接口可以表示任意類型。常用于:

  • 接收任意類型的參數(如 fmt.Println)
  • 實現通用容器(如 map[string]interface{})
  • 在 JSON 解碼時接收未知結構的數據

在這里插入圖片描述

在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/82569.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/82569.shtml
英文地址,請注明出處:http://en.pswp.cn/web/82569.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Async-profiler 內存采樣機制解析:從原理到實現

引言 在 Java 性能調優的工具箱中,async-profiler 是一款備受青睞的低開銷采樣分析器。它不僅能分析 CPU 熱點,還能精確追蹤內存分配情況。本文將深入探討 async-profiler 實現內存采樣的多種機制,結合代碼示例解析其工作原理。 為什么需要內…

Android 顏色百分比對照

本文就是簡單寫個demo,打印下顏色百分比的數值.方便以后使用. 1: 獲取透明色 具體的代碼如下: /*** 獲取透明色* param percent* param red* param green* param blue* return*/public static int getTransparentColor(int percent, int red, int green, int blue) {int alp…

MPLS-EVPN筆記詳述

目錄 EVPN簡介: EVPN路由: 基本四種EVPN路由 擴展: EVPN工作流程: 1.啟動階段: 2.流量轉發: 路由次序整理: 總結: EVPN基本術語: EVPN表項: EVPN支持的多種服務模式: 簡介: 1.Port Based: 簡介: 配置實現: 2.VLAN Based: 簡介: 配置實現: 3.VLAN Bundle: 簡…

SpringBoot自定義線程池詳細教程

文章目錄 1. 線程池基礎概念1.1 什么是線程池1.2 Java線程池核心參數1.3 線程池執行流程 2. SpringBoot中的線程池2.1 SpringBoot默認線程池2.2 SpringBoot異步任務基礎 3. 自定義線程池配置3.1 配置文件方式3.2 Java配置方式3.3 線程池工廠配置 4. 異步任務實際應用4.1 業務服…

智能快遞地址解析接口如何用PHP調用?

一、什么是智能快遞地址解析接口 隨著互聯網技術的普及和電子商務的迅猛發展,網購已成為現代人日常生活的重要組成部分。然而,在這個便捷的背后,一個看似不起眼卻影響深遠的問題正悄然浮現——用戶填寫的快遞地址格式混亂、信息不全甚至錯漏…

概率分布,支撐AI算法的數學基石

概率分布,是現代人工智能(AI)算法不可或缺的數學語言。它不僅描述了數據中的不確定性,更揭示了機器學習模型背后的本質運作機制。本文將帶你深入了解概率分布的數學本質,以及它在監督學習、深度學習、生成模型等核心AI領域的關鍵作用,揭秘概率論如何成為AI理論和實踐的強…

2025年Splunk的替代方案:更智能的安全選擇

在安全信息和事件管理(SIEM)領域,2025年的競爭愈發激烈。Splunk憑借其強大的功能和穩定性長期占據市場主導地位,但其高昂的成本、復雜性和擴展性挑戰促使許多企業轉向其他解決方案。無論是初創公司、快速發展的中型企業&#xff0…

(10)Fiddler抓包-Fiddler如何設置捕獲Firefox瀏覽器的Https會話

1.簡介 經過上一篇對Fiddler的配置后,絕大多數的Https的會話,我們可以成功捕獲抓取到,但是有些版本的Firefox瀏覽器仍然是捕獲不到其的Https會話,需要我們更進一步的配置才能捕獲到會話進行抓包。 2.環境 1.環境是Windows 10版…

simulink mask的使用技巧

1.mask界面布局 1.1如何調整控件的位置和控件大小? 反正2020a是調不了, 找了好久,只能是調布局,例如你要調成下面這樣: 第一個控件的iTem location屬性選擇New row 后面跟著的幾個和第一個同一行的空間屬性選擇Cu…

Go中MAP底層原理分析

MAP底層原理分析 參考 https://golang.design/go-questions/map/principalmap | Golang 中文學習文檔 先來看一下map結構體,(runtime.hmap結構體就是代表著 go 中的map,與切片一樣map的內部實現也是結構體) type hmap struct {/…

#開發環境篇:postMan可以正常調通,但是瀏覽器里面一直報403

本地header代理下面內容即可 headers: { // 添加必要的請求頭 ‘Host’: ‘服務端域名’, ‘Origin’: https://服務端域名, ‘Referer’: https://服務端域名 }, devServer: {// 本地開發代理API地址proxy: {^/file: {target: https://服務端域名,changeOrigin: true, // 是否…

【論文閱讀 | PR 2024 |ICAFusion:迭代交叉注意力引導的多光譜目標檢測特征融合】

論文閱讀 | PR 2024 |ICAFusion:迭代交叉注意力引導的多光譜目標檢測特征融合 1.摘要&&引言2.方法2.1 架構2.2 雙模態特征融合(DMFF)2.2.1 跨模態特征增強(CFE)2.2.2 空間特征壓縮(SFS)…

效率、便捷、安全:智慧充電樁一站式解決方案如何重塑新能源充電體驗?

在新能源浪潮席卷全球的背景下,電動汽車的普及對充電基礎設施提出了更高要求。傳統充電模式因效率低、操作繁瑣、安全隱患等問題,難以滿足用戶需求。智慧充電樁一站式解決方案應運而生,通過技術創新將效率、便捷與安全融為一體,徹…

杰發科技AC7840——Timer修改重裝載值

需要在運行過程中修改定時器的中斷時間 int main(void) {SystemClock_Config(); /*時鐘初始化*/GPIO_LedInit(); /*GPIO初始化*/TIMER_Init(); /*定時器初始化*/InitDebug(); …

https和http有什么區別-http各個版本有什么區別

http和 https的區別 HTTP(超文本傳輸協議)和 HTTPS(安全超文本傳輸協議)是兩種用于在網絡上傳輸數據的協議,它們的主要區別在于安全性: HTTP(Hypertext Transfer Protocol)&#x…

低秩矩陣、奇異值矩陣和正交矩陣

低秩矩陣 低秩矩陣(Low-rank Matrix)是指秩(rank)遠小于其行數和列數的矩陣,即 r a n k ( M ) r ? min ? ( m , n ) rank(M) r \ll \min(m,n) rank(M)r?min(m,n)。其核心特點是信息冗余性,可通過少量…

對抗性提示:大型語言模型的安全性測試

隨著大語言模型(LLM)在虛擬助手、企業平臺等現實場景中的深度應用,其智能化與響應速度不斷提升。然而能力增長的同時,風險也在加劇。對抗性提示已成為AI安全領域的核心挑戰,它揭示了即使最先進的模型也可能被操縱生成有…

SSM 框架核心知識詳解(Spring + SpringMVC + MyBatis)

🌱 第一部分:Spring 核心原理與使用 1. 什么是 Spring Spring 是一個開源的 Java 企業級開發框架,旨在簡化 Java 企業應用程序開發。它核心思想是控制反轉(IoC)和面向切面編程(AOP)&#xff0…

基于 Alpine 定制單功能用途(kiosk)電腦

前言 故事回到 7 年前, 在網上沖浪的時候發現了一篇介紹使用 Ubuntu 打造 kiosk 單功能用途電腦的文章, 挺好玩的, 就翻譯了一下并比葫蘆畫瓢先后用了 CentOS 7, ArchLinux 進行了實現. 歷史文章: 翻譯 - 使用Ubutnu14.04和Chrome打造單功能用途電腦(大屏展示電腦) 使用CentOS…

【機器學習及深度學習】機器學習模型的誤差:偏差、方差及噪聲

機器學習模型的誤差分析 V1.0機器學習模型的衡量準則概念引入機器學習模型誤差分析誤差出現的原因及消除 V1.0 機器學習模型的衡量準則 衡量機器學習模型的好壞可以考慮以下幾個方面: 偏差(Bias): 在充分訓練的情況下&#xff0…