Go 面向對象,封裝、繼承、多態

Go 面向對象,封裝、繼承、多態

經典OO(Object-oriented 面向對象)的三大特性是封裝、繼承與多態,這里我們看看Go中是如何對應的。

1. 封裝

封裝就是把數據以及操作數據的方法“打包”到一個抽象數據類型中,這個類型封裝隱藏了實現的細節,所有數據僅能通過導出的方法來訪問和操作。這個抽象數據類型的實例被稱為對象。經典OO語言,如Java、C++等都是通過類(class)來表達封裝的概念,通過類的實例來映射對象的。熟悉Java的童鞋一定記得**《Java編程思想》**一書的第二章的標題:“一切都是對象”。在Java中所有屬性、方法都定義在一個個的class中。

Go語言沒有class,那么封裝的概念又是如何體現的呢?來自OO語言的初學者進入Go世界后,都喜歡“對號入座”,即Go中什么語法元素與class最接近!于是他們找到了struct類型。

Go中的struct類型中提供了對真實世界聚合抽象的能力,struct的定義中可以包含一組字段(field),如果從OO角度來看,你也可以將這些字段視為屬性,同時,我們也可以為struct類型定義方法(method),下面例子中我們定義了一個名為Point的struct類型,它擁有一個導出方法Length:

type Point struct {x, y float64
}func (p Point) Length() float64 {return math.Sqrt(p.x * p.x + p.y * p.y)
}

我們看到,從語法形式上來看,與經典OO聲明類的方法不同,Go方法聲明并不需要放在聲明struct類型的大括號中。Length方法與Point類型建立聯系的紐帶是一個被稱為receiver參數的語法元素。

那么,struct是否就是對應經典OO中的類呢? 是,也不是!從數據聚合抽象來看,似乎是這樣, struct類型可以擁有多個異構類型的、代表不同抽象能力的字段(比如整數類型int可以用來抽象一個真實世界物體的長度,string類型字段可以用來抽象真實世界物體的名字等)。

但從擁有方法的角度,不僅是struct類型,Go中除了內置類型的所有其他具名類型都可以擁有自己的方法,哪怕是一個底層類型為int的新類型MyInt:

type MyInt intfunc(a MyInt)Add(b int) MyInt {return a + MyInt(b)
}

2. 繼承

就像前面說的,Go設計者在Go誕生伊始就重新評估了對經典OO的語法概念的支持,最終放棄了對諸如類、對象以及類繼承層次體系的支持。也就是說:在Go中體現封裝概念的類型之間都是“路人”,沒有親爹和兒子的關系的“牽絆”

談到OO中的繼承,大家更多想到的是子類繼承了父類的屬性與方法實現。Go雖然沒有像Java extends關鍵字那樣的顯式繼承語法,但Go也另辟蹊徑地對“繼承”提供了支持。這種支持方式就是類型嵌入(type embedding),看一個例子:

package mainimport "fmt"type P struct {A intb string
}func (P) M1() {fmt.Println("P M1")
}func (P) M2() {fmt.Println("P M2")
}type Q struct {c [5]intD float64
}func (Q) M2() {fmt.Println("Q M2")
}
func (Q) M3() {fmt.Println("Q M3")
}func (Q) M4() {fmt.Println("Q M3")
}type T struct {PQE int
}// M2 重寫方法:在 T 中重寫 M2 方法,明確調用哪個嵌入結構體的 M2。
func (t T) M2() {t.P.M2() // 或者 t.Q.M2()
}func main() {var t Tt.M1()//需要顯式調用t.P.M2()t.Q.M2()// 或重寫方法t.M2()t.M3()t.M4()println(t.A, t.D, t.E)
}

我們看到類型T通過嵌入P、Q兩個類型,“繼承”了P、Q的導出方法(M1~M4)和導出字段(A、D)。

不過實際Go中的這種“繼承”機制并非經典OO中的繼承,其外圍類型(T)與嵌入的類型(P、Q)之間沒有任何“親緣”關系。P、Q的導出字段和導出方法只是被提升為T的字段和方法罷了,其本質是一種組合,是組合中的代理(delegate)模式的一種實現。T只是一個代理(delegate),對外它提供了它可以代理的所有方法,如例子中的M1~M4方法。當外界發起對T的M1方法的調用后,T將該調用委派給它內部的P實例來實際執行M1方法。

以經典OO理論話術去理解就是T與P、Q的關系不是is-a,而是has-a的關系

組合大于繼承

其實這種繼承更應該被稱為組合。Go 更愿意將模塊分成互相獨立的小單元,分別處理不同方面的需求,最后以匿名嵌入的方式組合到一起,共同實現對外接口。也就是組合大于繼承的思想。

組合沒有父子依賴,不會破壞封裝。且整體和局部松耦合,可任意增加來實現擴展。各單元持有單一職責,互不關聯,自由靈活組合,實現和維護更加簡單。

匿名嵌套

匿名嵌套在編譯時會根據嵌套類型生成包裝方法包裝方法實際是調用嵌套類型的原始方法

拓:匿名嵌套的多種玩法

  • struct 匿名嵌套 struct(上面已經展示過了)
  • interface 匿名嵌套 interface
  • struct 匿名嵌套 interface

interface 匿名嵌套 interface

接口可嵌入其他匿名接口,相當于將其聲明的方法集導入

當然,注意只有實現了兩個接口的全部的方法,才算實現大接口哈。

Go 標準庫中經典用法如下:

type Reader interface {Read(p []byte) (n int, err error)
}type Writer interface {Write(p []byte) (n int, err error)
}type ReadWriter interface {ReaderWriter
}

struct 匿名嵌套 interface

編譯器自動為 struct 的方法集加上 interface 的所有方法
(后面是我猜的)如我們通過 struct.M () 調用 interface 的 M 方法,編譯器實際為 struct 生成包裝方法 struct.interface.M()

注意:struct 中的 interface 要記得賦值哈,不然調用時會顯示 interface nil panic。

type I interface {M()
}type A struct {I
}type B struct {
}func (B) M() {print("B")
}func main() {var a A = A{I: B{}}a.M() // B// 當然 A 也是 I 接口類型var i I = A{I: B{}}i.M() // A
}

我們同時驗證以下匿名嵌套的同名覆蓋問題:

type I interface {M()
}type A struct {I
}type B struct {
}// 多加這個:驗證匿名方法同名覆蓋
func (A) M() {print("A")
}func (B) M() {print("B")
}func main() {var a A = A{I: B{}}a.M() // A
}

Go 標準庫中經典用法如下:

context 包中:

type valueCtx struct {Context  // 匿名接口key, val interface{}
}// 創建 valueCtx
func WithValue(parent Context, key, val interface{}) Context {return &valueCtx{parent, key, val}
}// 實際重寫了 Value() 接口,其他父 context 的方法依舊可以調用
func (c *valueCtx) Value(key interface{}) interface{} {if c.key == key {return c.val}return c.Context.Value(key)
}

3. 多態

經典OO中的多態是尤指運行時多態,指的是調用方法時,會根據調用方法的實際對象的類型來調用不同類型的方法實現。

下面是一個C++中典型多態的例子:

#include <iostream>class P {public:virtual void M() = 0;
};class C1: public P {public:void M();
};void C1::M() {std::cout << "c1.M()\n";
}class C2: public P {public:void M();
};void C2::M() {std::cout << "c2.M()\n";
}int main() {C1 c1;C2 c2;P *p = &c1;p->M(); // c1.M()p = &c2;p->M(); // c2.M()
}

這段代碼比較清晰,一個父類P和兩個子類C1和C2。父類P有一個虛擬成員函數M,兩個子類C1和C2分別重寫了M成員函數。在main中,我們聲明父類P的指針,然后將C1和C2的對象實例分別賦值給p并調用M成員函數,從結果來看,在運行時p實際調用的函數會根據其指向的對象實例的實際類型而分別調用C1和C2的M。

顯然,經典OO的多態實現依托的是類型的層次關系。那么對應沒有了類型層次體系的Go來說,它又是如何實現多態的呢?Go使用接口來解鎖多態

和經典OO語言相比,Go更強調行為聚合與一致性,而非數據。因此Go提供了對類似duck typing的支持,即基于行為集合的類型適配,但相較于ruby等動態語言,Go的靜態類型機制還可以保證應用duck typing時的類型安全。

Go的接口類型本質就是一組方法集合(行為集合),一個類型如果實現了某個接口類型中的所有方法,那么就可以作為動態類型賦值給接口類型。通過該接口類型變量的調用某一方法,實際調用的就是其動態類型的方法實現。看下面例子:

type MyInterface interface {M1()M2()M3()
}type P struct {
}func (P) M1() {}
func (P) M2() {}
func (P) M3() {}type Q int 
func (Q) M1() {}
func (Q) M2() {}
func (Q) M3() {}func main() {var p Pvar q Qvar i MyInterface = pi.M1() // P.M1i.M2() // P.M2i.M3() // P.M3i = qi.M1() // Q.M1i.M2() // Q.M2i.M3() // Q.M3
}

Go這種無需類型繼承層次體系、低耦合方式的多態實現,是不是用起來更輕量、更容易些呢!

Go 通過接口來實現多態。

Go 的接口類型本質就是一組方法集合 (行為集合),一個類型如果實現了某個接口類型中的所有方法,那么就可以作為動態類型賦值給接口類型(注意:定義一個接口變量,該變量本質是個 Struct 類型的變量哦)。

Go 的接口是特別重要的東西,通過學習 Go 接口的底層實現可以學到很多東西,例如動態語言的實現,方法動態派發實現等。

4. Gopher的“OO思維”

到這里,來自經典OO語言陣營的小伙伴們是不是已經找到了當初在入門Go語言時“感覺到別扭”的原因了呢!這種“別扭”就在于Go對于OO支持的方式與經典OO語言的差別:秉持著經典OO思維的小伙伴一上來就要建立的繼承層次體系,但Go沒有,也不需要。

要轉變為正宗的Gopher的OO思維其實也不難,那就是“prefer接口,prefer組合,將習慣了的is-a思維改為has-a思維”。

5. 小結

是時候給出一些結論性的觀點了:

  • Go支持OO,只是用的不是經典OO的語法和帶層次的類型體系;
  • Go支持OO,只是用起來需要換種思維;
  • 在Go中玩轉OO的思維方式是:“優先接口、優先組合”。

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

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

相關文章

無線網絡設備中AP和AC是什么?有什么區別?

無線網絡設備中AP和AC是什么&#xff1f;有什么區別&#xff1f; 一. 什么是AP&#xff1f;二. 什么是AC&#xff1f;三. AP與AC的關系 前言 肝文不易&#xff0c;點個免費的贊和關注&#xff0c;有錯誤的地方請指出&#xff0c;看個人主頁有驚喜。 作者&#xff1a;神的孩子都…

Android SDK

Windows純凈卸載Android SDK 1.關閉所有安卓相關的程序 Android StudioEmulators 如模擬器Command prompts using SDK 如appium服務 2.移除SDK相關目錄 # Delete your SDK directory F:\android_sdk\android-sdk-windows# Also check and remove if present: $env:LOCALAPP…

Android耗電優化全解析:從原理到實踐的深度治理指南

引言 在移動應用性能優化體系中&#xff0c;耗電優化是用戶體驗的核心指標之一。據Google官方統計&#xff0c;超過60%的用戶會因為應用耗電過快而選擇卸載應用。本文將從耗電統計原理、監控手段、治理策略三個維度展開&#xff0c;結合Android系統源碼與實際代碼示例&#xf…

QMK自定義4*4鍵盤固件創建教程:最新架構詳解

QMK自定義4*4鍵盤固件創建教程&#xff1a;最新架構詳解 前言 通過本教程&#xff0c;你將學習如何在QMK框架下創建自己的鍵盤固件。QMK是一個強大的開源鍵盤固件框架&#xff0c;廣泛用于DIY機械鍵盤的制作。本文將詳細介紹最新架構下所需創建的文件及其功能。 準備工作 在…

DAMA第10章深度解析:參考數據與主數據管理的核心要義與實踐指南

引言 在數字化轉型的浪潮中&#xff0c;數據已成為企業的核心資產。然而&#xff0c;數據孤島、冗余和不一致問題嚴重制約了數據價值的釋放。DAMA&#xff08;數據管理協會&#xff09;提出的參考數據&#xff08;Reference Data&#xff09;與主數據&#xff08;Master Data&…

力扣題解:2、兩數相加

個人認為&#xff0c;該題目可以看作合并兩個鏈表的變種題&#xff0c;本題與21題不同的是&#xff0c;再處理兩個結點時&#xff0c;對比的不是兩者的大小&#xff0c;而是兩者和是否大于10&#xff0c;加法計算中大于10要進位&#xff0c;所以我們需要聲明一個用來標記是否進…

深度學習部署包含哪些步驟?

深度學習部署包含哪些步驟&#xff1f; 階段說明示例工具模型導出把 .pt、.h5 等格式模型導出為通用格式&#xff08;如ONNX&#xff09;PyTorch, TensorFlow, ONNX推理優化減小模型體積、加速推理&#xff08;量化、剪枝&#xff09;TensorRT, ONNX Runtime系統集成將模型嵌入…

路由策略和策略路由的區別以及配置案例

區別 路由策略&#xff1a;路由策略是通過ACL等方式控制路由發布&#xff0c;讓對方學到適當路由條目&#xff0c;比如有20條路由&#xff0c;只想讓某個路由器學到10條&#xff0c;可以通過路由策略進行過濾。 策略路由&#xff1a;策略路由是通過定義策略和應用&#xff0c…

LeetCode 熱題 100 64. 最小路徑和

LeetCode 熱題 100 | 64. 最小路徑和 大家好&#xff0c;今天我們來解決一道經典的動態規劃問題——最小路徑和。這道題在 LeetCode 上被標記為中等難度&#xff0c;要求找到從網格的左上角到右下角的路徑&#xff0c;使得路徑上的數字總和為最小。 問題描述 給定一個包含非負…

JavaSE核心知識點02面向對象編程02-06(泛型)

&#x1f91f;致敬讀者 &#x1f7e9;感謝閱讀&#x1f7e6;笑口常開&#x1f7ea;生日快樂?早點睡覺 &#x1f4d8;博主相關 &#x1f7e7;博主信息&#x1f7e8;博客首頁&#x1f7eb;專欄推薦&#x1f7e5;活動信息 文章目錄 JavaSE核心知識點02面向對象編程02-06&#…

LVGL對象的盒子模型和樣式

文章目錄 &#x1f9f1; LVGL 對象盒子模型結構&#x1f50d; 組成部分說明&#x1f3ae; 示例代碼&#x1f4cc; 總結一句話 &#x1f9f1; 一、樣式的本質&#xff1a;lv_style_t 對象&#x1f3a8; 二、樣式應用的方式&#x1f9e9; 三、樣式屬性分類&#xff08;核心&#…

Github上如何準確地搜索開源項目

Github上如何準確地搜索開源項目&#xff1a; 因為尋找項目練手是最快速掌握技術的途徑&#xff0c;而Github上有最全最好的開源項目。 就像我的畢業設計“機器翻譯”就可以在Github上查找開源項目來參考。 以下搜索針對&#xff1a;項目名的關鍵詞&#xff0c;關注數限制&a…

正點原子IMX6U開發板移植Qt時出現亂碼

移植Qt時出現亂碼 1、前言2、問題3、總結 1、前言 記錄一下正點原子IMX6U開發板移植Qt時出現亂碼的解決方法&#xff0c;方便自己日后回顧&#xff0c;也可以給有需要的人提供幫助。 2、問題 用正點原子IMX6U開發板移植Qt時移植Qt后&#xff0c;sd卡里已經存儲了Qt的各種庫&…

python-django項目啟動尋找靜態頁面html順序

目錄結構 settings模塊 urls模塊 views模塊 1.settings文件下沒有DIR目錄,按照各app注冊順序尋找靜態頁面 啟動效果&#xff0c;直接返回注冊的app即app01下的templates文件夾下的html頁面 2.settings文件添加上DIR目錄 啟動效果&#xff0c;會優先去找項目下的templates文件…

MySQL索引詳解(上)(結構/分類/語法篇)

一、索引概述 索引本質是幫助MySQL高效獲取數據的排序數據結構&#xff08;類似書籍目錄&#xff09;&#xff0c;通過減少磁盤I/O次數提升查詢效率。其核心價值體現在大數據量場景下的快速定位能力&#xff0c;但同時帶來存儲和維護成本。 核心特點&#xff1a; 優點&#…

數據集-目標檢測系列- 煙霧 檢測數據集 smoke >> DataBall

數據集-目標檢測系列- 消防 濃煙 檢測數據集 smoke>> DataBall 數據集-目標檢測系列- 煙霧 檢測數據集 smoke &#xff1e;&#xff1e; DataBall * 相關項目 1&#xff09;數據集可視化項目&#xff1a;gitcode: https://gitcode.com/DataBall/DataBall-detections-10…

docker + K3S + Jenkins + Harbor自動化部署

最近公司在研究自動化部署的一套流程&#xff0c;下面記錄一下配置流程 需要提前準備好Jenkins Harbor Git(其他管理工具也可以) 我這里的打包編譯流程是Jenkins上配置打包任務-->自動到git目錄下找打包文件---->項目編譯后打鏡像包------>打完鏡像包將鏡像上傳到…

《用MATLAB玩轉游戲開發:從零開始打造你的數字樂園》基礎篇(2D圖形交互)-《打磚塊:向量反射與實時物理模擬》MATLAB教程

《用MATLAB玩轉游戲開發&#xff1a;從零開始打造你的數字樂園》基礎篇&#xff08;2D圖形交互&#xff09;-《打磚塊&#xff1a;向量反射與實時物理模擬》MATLAB教程 &#x1f3ae; 文章目錄 《用MATLAB玩轉游戲開發&#xff1a;從零開始打造你的數字樂園》基礎篇&#xff08…

Redisson 看門狗機制

何為看門狗 看門狗機制的主要作用是自動續期鎖&#xff0c;確保在節點完成任務之前&#xff0c;鎖不會過期。具體來說&#xff0c;當一個節點獲取到鎖后&#xff0c;看門狗會定期檢查該鎖的過期時間&#xff0c;并在必要時延長鎖的過期時間&#xff0c;確保節點可以順利完成任…

[架構之美]linux常見故障問題解決方案(十九)

[架構之美]linux下常見故障問題解決方案 一&#xff0c;文本文件忙 問題一&#xff1a;rootwh-VMware-Virtual-Platform:/home/hail# cp /root/containerd/bin/* /usr/bin/ cp: 無法創建普通文件 ‘/usr/bin/containerd’: 文本文件忙 在Linux系統中遇到“文本文件忙”錯誤時…