1. 程式人生 > >go 學習筆記之詳細說一說封裝是怎麼回事

go 學習筆記之詳細說一說封裝是怎麼回事

關注公眾號[雪之夢技術驛站]檢視上篇文章 猜猜看go是不是面嚮物件語言?能不能面向物件程式設計?

雖然在上篇文章中,我們通過嘗試性學習探索了 Go 語言中關於面向物件的相關概念,更確切的說是關於封裝的基本概念以及相關實現.

但那還遠遠不夠,不能滿足於一條路,而是應該儘可能地多走幾條路,只有這樣才能為以後可能遇到的問題積攢下來經驗,所以這一節我們將繼續探索封裝.

何為探索性學習

通過現有知識加上思想規則指導不斷猜想假設逐步驗證的學習過程是探索性學習,這樣既有利於我們思考又能加深我們對新知識的理解,何樂而不為?

學習 Go 語言的過程越發覺得吃力,倒不是因為語法晦澀難懂而是因為語法習慣背後蘊藏的思維習慣差異性太大!

Go 語言相對於其他主流的程式語言來說是一種新語言,不僅體現在語法層面更重要的是實現思路的差異性.

尤其是對於已有其他程式設計經驗的開發者而言,這種體會更加深刻,原本可能覺得理所應當的事情到了 Go 語言這裡基本上都變了模樣,很大程度上都換了一種思路去實現,這其實是一件好事,不同的思維碰撞才能促進思考進步,一成不變的話,談何創新發展?

在這裡不得不感謝強大的 IDE 開發工具,沒有它我們就不能及時發現錯誤,正是這種快速試錯的體驗才給我們足夠的反饋,運用已有的程式設計經驗逐步接近 Go 語言程式設計的真相.

上篇文章中已經確定主線方向,基本上弄清楚了面向物件中的封裝概念以及實現,為了不遺漏任何可能重要的知識點,本文將繼續開放性探索,力爭講解清楚封裝的知識點.

如果這種學習的過程用走迷宮來比喻的話,一條道走到黑這種策略就是演算法理論中的深度優先演算法.如果邊走邊看,四處觀望周圍的風景就是廣度優先演算法.

所以,聰明的你肯定已經猜到了,上文采用的正是深度優先演算法而本文則採用廣度優先演算法繼續探索封裝物件之旅!

定義結構體

結構體的定義方式只有一種,或者不存在簡化形式嗎?

個人覺得不會不存在簡化形式,當結構體存在多個欄位,標準定義方式是合理使用的,但要是欄位只有一個,仍然以標準形式定義結構體未免有種殺雞焉用牛刀的感覺.

type MyDynamicArray struct {
    ptr *[10]int
    len int
    cap int
}

所謂的結構體只不過是實現封裝的一種手段,當封裝的物件只有一個欄位時,這個欄位也就不存在欄位名或者說這個唯一的欄位名應該就可以由編譯器自動定義,因此欄位名可以省略.

欄位型別肯定是不可或缺的,這麼想的話,對於封裝只有一個欄位的物件來說,只需要考慮的是這個唯一欄位的型別.

基於上述原因,個人覺得是這種猜想是合情合理的,但是按照已有的知識能否實現呢?

簡單起見,暫時先以上篇文章中關於動態陣列的結構體宣告為例作為測試案例.

type MyDynamicArray struct {
    ptr *[10]int
    len int
    cap int
}

如果一定要從三個欄位中選擇一個欄位,那隻能是保留內部陣列,排除其餘欄位了,同時最終結果上可能實現不了動態陣列的功能,語義上會有所欠缺,那就不論語義,只談技術!

由於只保留內部陣列,動態陣列就變成下面這樣.失去了動態陣列的語義,命名上也做了改變,姑且稱之為 MyArray 吧!

type MyArray struct {
    arr [10]int
}

很明顯,現在仍然是結構體的標準語法形式,請隨我一起思考一下如何簡化這種形式?

因為這種簡化形式的內部欄位只有一個,所以欄位名必須省略而欄位型別可能不同,因此應該在簡化形式中只保留宣告內部欄位型別的部分.

type MyArray struct {
    [10]int
}

由於多個欄位時才需要換行分隔,一個欄位自然是不需要換行的,因此大括號也是沒必要存在的,這也是符合 Go 設計中儘可能精簡的情況下保證語義清晰的原則.

當然如果你問我是否真的有這個原則的話,我的回答是可能有也可能沒有.

因為我也不知道,只是近期學習 Go 語言的一種感覺,處處體現了這麼一種哲學思想,也不用較真,只是個人看法.

type MyArray struct [10]int

現在這種形式應該可以算是隻有一種欄位的結構體的簡化形式,struct 語義上指明瞭 MyArray 是結構體,緊隨後面的 [10]int 語義上表示結構體的型別,整體上就是說 MyArray 結構體的型別是 [10]int .

現在讓我們在編輯器中測試一下,看一看 Go 的編譯會不會報錯,能否驗證我們的猜測呢?

很遺憾,IDE 編輯器告訴我們 [10]int 不合法,必須是型別或型別指標!

[10]int 確實是我們需要的型別啊,既然報錯也就是說Go 編譯器不支援這種簡化形式!

個人猜測可能是 struct 關鍵字不支援這種簡化形式,那就去掉這個關鍵字好了!

沒想到真的可以!

至少現在看來 Go 編譯器是支援簡化形式的,至於這種支援的形式和我們預期實現的語義是否一致,暫時還不好說,繼續做實驗探索吧!

通過宣告變數後直接列印,初步證明了我們這種簡化形式是可以正常工作的,輸出結果也是我們定義的內部陣列!

接下來看一看能不能對這個所謂的內部陣列進行操作呢?

這種簡化形式只有一個欄位,只指明瞭欄位的型別,沒有欄位名,因而訪問該欄位應該直接通過結構體變數訪問,不知道這種猜測是否正確,依舊做實驗來證明.

type MyArray [10]int

func TestMyArray(t *testing.T) {
    var myArr MyArray

    // [0 0 0 0 0 0 0 0 0 0]
    t.Log(myArr)

    myArr[0] = 1
    myArr[9] = 9

    // [1 0 0 0 0 0 0 0 0 9]
    t.Log(myArr)
}

這一次猜想也得到了驗證,Go 編譯器就是通過結構體變數直接操作內部欄位,看來我們離真相更進一步!

先別急著高興,將唯一的欄位換成其他型別,多測試幾遍看看是否依然正常?

type MyBool bool

func TestMyBool(t *testing.T) {
    var myBool MyBool

    // false
    t.Log(myBool)

    myBool = true

    // true
    t.Log(myBool)
}

一番測試後並沒有報錯,很有可能這是 Go 所支援的結構體簡化形式,也和我們的預期一致.

關於結構體屬性的語法規則暫時沒有其他探索的新角度,接下來開始探索結構體的方法.

探索的過程中要儘可能的設身處地思考 Go 語言應該如何設計才能方便使用者,儘可能地把自己想象成 Go 語言的設計者!

結構體的簡化形式下可能並不支援方法,如果真的是這樣的話,這樣做也有一定道理.

首先就語法層面分析,為什麼單欄位的結構體不支援方法?

還記得我們想要簡化單欄位結構體遇到的報錯提示嗎?

type MyArray struct [10]int

如果直接將單欄位型別放到 struct 關鍵字後面,Go 編譯器就會報錯,當我們省略 struct 關鍵字時上述報錯自然就消失了.

Go 編譯器的角度上來講,struct 是系統關鍵字,告訴編譯器只要遇到這個關鍵字就解析成結構體語法,現在沒有遇到 sruct 關鍵字也就意味著不是結構體語法.

這裡關鍵字和結構體是一一對應關係,也就是充分必要條件,由關鍵字可以推測到結構體,由結構體也可以推測到關鍵字.

再回來看一看,我們的單欄位結構體是怎麼定義的呢?

type MyArray [10]int

因為沒有關鍵字 struct ,所以編譯器推斷 MyArray 不是結構體,既然不是結構體,也不能用結構體的接收者函式去定義方法.

func (myBool *MyBool) IsTrue() bool{
    return myBool
}

所以這種方法就會報錯,由此可見 ,Go 語言如果真的不支援單欄位結構體方法也有理可循.

然後我們再從語義的角度上解釋一下為什麼不支援方法?

回到探索的初衷,當正在定義的結構體有多個欄位時,應該按照標準寫法為每個欄位指定欄位的名稱和型別.

假如該欄位有且只有一個時,再按照標準寫法定義當然可以,但也應該提供更加簡化的寫法.

只有一個欄位的結構體,欄位名稱是沒有意義的也是不應該出現的,因為完全可以用結構體變數所代替,此時這個結構體唯一有存在價值的就是欄位的型別了!

欄位型別包括內建型別和使用者自定義結構體型別,不論哪種型別,這種簡化形式的結構體的語義上完全可以由該結構體的欄位型別所決定,所以簡化形式的結構體還需要方法嗎?

自然是不需要的!

欄位型別可以由欄位型別自己定義的,也能確保職責清晰,彼此分離!

綜上,個人覺得即便 Go 真的不支援單欄位結構體的方法,背後的設計還是有章可循的,有理可依的!

上文中定義動態陣列時,內部使用的陣列是靜態陣列,現在為了方便繼續探索方法,應該提供過載方法使其支援動態陣列.

func NewMyDynamicArray() *MyDynamicArray {
    var myDynamicArray MyDynamicArray

    myDynamicArray.len = 0
    myDynamicArray.cap = 10
    var arr [10]int
    myDynamicArray.ptr = &arr

    return &myDynamicArray
}

內部陣列 arr 是靜態陣列,應該提供可以讓外部呼叫者初始化指定陣列的介面,按照已知的面向物件中關於方法的定義來過載方法.

初次嘗試方法的過載就遇到了問題,報錯提示該方法已宣告,所以說 Go 可能並不支援方法過載,這樣就有點麻煩了.

想要實現類似的功能要麼通過定義不同的方法名,要麼定義一個非常大的函式,接收最全的引數,再根據呼叫者引數進行對應的邏輯處理.

用慣了方法的過載,突然發現這種特性在 Go 語言中無法實現,頓時有點沮喪,和其他主流的面嚮物件語言差異性也太大了吧!

不支援建構函式,不支援方法過載,原來以為理所應當的特性並不理所應當.

還是先冷靜下來想一想,Go 為什麼不支援方法過載呢?難不成和建構函式那樣,怕是濫用乾脆禁用的邏輯?

因為我不是設計者,無法體會也不想猜測原因,但可以肯定的是,Go 語言是一門全新的語言,有著獨特的設計思路,不與眾人同!

吐槽時間結束,既然上了賊船就得一條道走到黑,不支援方法過載就換個函式名或者按引數名區分.

天啊擼,剛剛解決方法過載問題又冒出陣列初始化不能是變數只能是常量表達式?

簡直不可思議!

既然陣列初始化長度只是常量表達式,也就無法接收外部傳遞的容量 cap,沒有了容量只能接收長度 len ,而初始化內部陣列長度又沒辦法確定了,兩個變數都無法對外暴露!

一切又回到原點,想要實現動態陣列的功能只能靠具體的方法中去動態擴容和縮容,不能初始化指定長度了.

這樣的話,關於方法也是一條死路,停止探索.

宣告結構體

結構體定義基本已經探索完畢,除了發現一種單欄位結構體的簡化形式外,暫時沒有新的發現.

再次回到使用者的角度上,宣告結構體有沒有其他方式呢?

var myDynamicArray MyDynamicArray
    
t.Log(myDynamicArray)

這是變數的宣告方式,除了這種形式,還記得在學習 Go 的變數時曾經介紹過宣告並初始化變數方式,是否也適用於結構體變數呢?

var myDynamicArray = MyDynamicArray{
        
}

t.Log(myDynamicArray)

編譯器沒有報錯,證明這種字面量形式也是適用的,不過空資料結構沒有太大的意義,怎麼能初始化對應的結構呢?

和多欄位結構體最為相似的資料結構莫過於對映 map 了!

回憶一下 map 如何進行字面量初始化的吧!

var m = map[string]string{
    "id":   "1006",
    "name": "雪之夢技術驛站",
}

t.Log(m)

模仿這種結構看看能不能對結構體也這麼初始化,果然就沒有那麼順利!

我還沒定義,你就不行了?

IDE 編輯器提示欄位名稱無效,結構體明明就有 len 欄位啊,除非是沒有正確識別!

"len"len 是不一樣的吧?

那就去掉雙引號 "" 直接使用欄位名進行定義看看.

var myDynamicArray = MyDynamicArray{
    len: 10,
}

t.Log(myDynamicArray)

此時報錯消失了,成功解鎖一種新的隱藏技能.

var myDynamicArray = MyDynamicArray{
    ptr: &[10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
    len: 10,
    cap: 10,
}

t.Log(myDynamicArray)

除了這種指定欄位名稱注入方式,能不能不指定欄位名稱而是按照欄位順序依次初始化?

藉助編輯器可以看到確實是按照順序注入的,這樣的話,其實有點意思了,明明不支援建構函式,採用字面量例項化時卻看起來像建構函式的無參,有引數和全參形式?

可以預想到的是,這種全參注入的方式一定是嚴格按照定義順序相匹配的,當引數不全時可能按位插入也可能不支援,真相如何,一試便知!

事實上並不支援這種引數不全的形式,因此個人覺得要麼無參要麼全參要麼指定初始化欄位這三種語義上還是比較清楚的.

除了字面量的方式,Go 是否支援建立 slicemap 時所使用的 make 函式呢?

看樣子,make 函式並不支援建立結構體,至於為什麼不支援,原因就不清楚了,也是個人的一個疑惑點.

既然 make 可以建立 slice ,map 這種內建型別,語義上就是用來建立型別的變數,而結構體也是一種型別,唯一的差別可能就是結構體大多是自定義型別而不是內建型別.

如果我來設計的話,可能會一統天下,因為語義上一致的功能只使用相同的關鍵字.

回到面向物件的傳統程式設計規範上,一般例項化物件用的是關鍵字 new,而 new 並不是 Go 中的關鍵字.

Go 語言中的函式是一等公民,正如剛才說的 make 也不是關鍵字,同樣是函式.

即便對於同一個目標,Go 也是有著自己的獨到見解!

new 不是以關鍵字形式出現而是以函式的身份登場,初步推測應該也具備例項化物件的能力吧?

難道 new 函式不能例項化物件?為什麼報錯說賦值錯誤,難不成姿勢不對?

嚇得我趕緊看一下 new 的文件註釋.


// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type

根據註釋說明,果然是使用姿勢不對,並不像其他的面嚮物件語言那樣可以重複賦值,Go 不支援這種形式,還是老老實實初始化宣告吧!

var myDynamicArray2 = new(MyDynamicArray)
    
t.Log(myDynamicArray2)  

既然存在著兩種方式來例項化物件,那麼總要看一下有什麼區別.

func TestNewMyDynamicArray(t *testing.T) {
    var myDynamicArray = MyDynamicArray{
        ptr: &[10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
        len: 10,
        cap: 10,
    }
    myDynamicArray = MyDynamicArray{
        &[10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
        10,
        10,
    }
    t.Log(myDynamicArray)
    t.Logf("%[1]T %[1]v", myDynamicArray)

    var myDynamicArray2 = new(MyDynamicArray)
    myDynamicArray2.ptr = &[10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    myDynamicArray2.len = 10
    myDynamicArray2.cap = 10

    t.Log(myDynamicArray2)

    t.Logf("%[1]T %[1]v", myDynamicArray2)
}

這裡簡單解釋下 t.Logf("%[1]T %[1]v", myDynamicArray) 語句是什麼意思?

%[1]T 其實是 %T 的變體,%[1]v 也是 %v 的變體,仔細觀察的話就會發現佔位符剛好都是同一個變數,這裡也就是第一個引數,所以就用 [1] 替代了,再次體現了 Go 語言設計的簡潔性.

下面再舉一個簡單的例子加深印象,看仔細了哦!

test := "snowdreams1006"

// string snowdreams1006
t.Logf("%T %v", test, test)
t.Logf("%[1]T %[1]v", test)

%T 是列印變數的型別,應該是型別 type 的縮寫,v 應該是值 value 的縮寫.

解釋清楚了測試程式碼的含義,再回頭看看測試結果,發現採用字面量方式得到的變數型別和 new 函式得到的變數型別明顯不同!

具體表現為 _struct.MyDynamicArray {0xc0000560f0 10 10} 是結構體型別,而 *_struct.MyDynamicArray &{0xc000056190 10 10} 是結構體型別的指標型別.

這種差異也是可以預期的差異,也是符合語義的差異.

字面量例項化的物件是值物件,而 new 例項化物件開闢了記憶體,返回的是例項物件到引用,正如其他程式語言的 new 關鍵字一樣,不是嗎?

既然說到了值物件和引用物件,再說一遍老生常談的問題,函式或者說方法傳遞時應該傳遞哪一種型別?

值傳遞還是引用傳遞

接下來的示例和動態陣列並沒有什麼關係,簡單起見,新開一個結構體叫做 Employee,順便回顧一下目前學到的封裝知識.

type Employee struct {
    Id   string
    Name string
    Age  int
}

func TestCreateEmployee(t *testing.T) {
    e := Employee{
        "0",
        "Bob",
        20,
    }
    t.Logf("%[1]T %[1]v", e)

    e1 := Employee{
        Name: "Mike",
        Age:  30,
    }
    t.Logf("%[1]T %[1]v", e1)

    e2 := new(Employee)
    e2.Id = "2"
    e2.Name = "Rose"
    e2.Age = 18
    t.Logf("%[1]T %[1]v", e2)
}

首先測試引用傳遞,這也是結構體常用的傳遞方式,行為表現上和其他的主流程式語言表現一致,方法內的修改會影響呼叫者的引數.

func (e *Employee) toStringPointer() string {
    fmt.Printf("Name address is %x\n", unsafe.Pointer(&e.Name))

    return fmt.Sprintf("ID:%s-Name:%s-Age:%d", e.Id, e.Name, e.Age)
}

func TestToStringPointer(t *testing.T) {
    e := &Employee{"0", "Bob", 20}

    fmt.Printf("Name address is %x\n", unsafe.Pointer(&e.Name))

    t.Log(e.toStringPointer())
}

unsafe.Pointer(&e.Name) 是檢視變數的記憶體地址,可以看出來呼叫前後的地址是同一個.

func (e Employee) toStringValue() string {
    fmt.Printf("Name address is %x\n", unsafe.Pointer(&e.Name))

    return fmt.Sprintf("ID:%s-Name:%s-Age:%d", e.Id, e.Name, e.Age)
}

func TestToStringValue(t *testing.T) {
    e := Employee{"0", "Bob", 20}

    fmt.Printf("Name address is %x\n", unsafe.Pointer(&e.Name))

    t.Log(e.toStringValue())
}

呼叫者傳送的記憶體地址和接收者接收的記憶體地址不一樣,符合期望,值傳遞都是拷貝變數進行傳遞的嘛!

值型別還是引用型別的區分無需贅述,接下來請關注一個神奇的事情,方法的接收者是值型別,方法的呼叫者是不是一定要傳遞值型別呢?

func (e Employee) toString() string {
    fmt.Printf("Name address is %x\n", unsafe.Pointer(&e.Name))

    return fmt.Sprintf("ID:%s-Name:%s-Age:%d", e.Id, e.Name, e.Age)
}

方法的呼叫者分別傳遞值型別和引用型別,兩者均能正常工作,是不是很神奇,好像和方法的定義沒什麼關係一樣!

func TestToString(t *testing.T) {
    e := Employee{"0", "Bob", 20}

    fmt.Printf("Name address is %x\n", unsafe.Pointer(&e.Name))

    t.Log(e.toString())
    t.Log((&e).toString())
}

雖然方法的接收者要求的是值型別,呼叫者傳遞的是值型別還是引用型別均可!

僅僅更改了方法接收者的型別,呼叫者不用做任何更改,依然可以正常執行!

這樣就很神奇了,方法的接受者不論是值型別還是指標型別,呼叫者既可以是值型別也可以是指標型別,為什麼?

同樣的,基於語義進行分析,方法的設計者和呼叫者之間可以說是鬆耦合的,設計者的更改對於呼叫者來說沒有太大影響,這也就意味著以後設計者覺得用值型別接收引數不好,完全可以直接更改為指標型別而不用通知呼叫者調整邏輯!

這其實要歸功於 Go 語言到設計者很好的處理了值型別和指標型別的呼叫方式,不論是值型別還是引用型別,一律使用點操作符 . 呼叫方法,並不像有的語言指標型別是 ->* 字首才能呼叫指標型別的方法.

有所為有所不為,可能正是看到了這兩種呼叫方式帶來的差異性,Go 全部統一成點操作符了!

雖然形式上兩種呼叫方式是一樣的,但是設計方法或者函式時到底應該是值型別還是指標型別呢?

這裡有三點建議可供參考:

  • 如果接收者需要更改呼叫者的值,只能使用指標型別
  • 如果引數本身非常大,拷貝引數比較佔用記憶體,只能用指標型別
  • 如果引數本身具有狀態,拷貝引數可能會影響物件的狀態,只能用指標型別
  • 如果是內建型別或者比較小的結構體,完全可以忽略拷貝問題,推薦用值型別.

當然,實際情況可能還和業務相關,具體用什麼型別還要自行判斷,萬一選用不當也不用擔心,更改一下引數型別就好了也不會影響呼叫者的程式碼邏輯.

封裝後如何訪問

封裝問題基本上講解清楚了,一般來說,封裝之後的結構體不僅是我們自己使用還有可能提供給外界使用,與此同時要保證外界不能隨意修改我們的封裝邏輯,這一部分就涉及到訪問的控制權限了.

Go 語言的訪問級別有兩種,一種是公開的另一種就是私有的,由於沒有繼承特性,也不涉及子類和父類之間訪問許可權繼承問題,頓時覺得沒有繼承也不是什麼壞事嘛,少了很多易錯的概念!

雖然現在理解起來很簡單,具體實際使用上是否便利還不好判斷.

關於可見性的命名規範如下:

  • 名稱一般使用大駝峰命名法即 CamelCase
  • 首字母大寫表示公開的 public ,小寫表示私有的 private .
  • 上述規則不僅適用於方法,包括結構體,變數和常量等幾乎是 Go 語言的全部.

那麼問題了,這裡的 publicprivate 是針對誰來說?

Go 語言中的基本結構是包 package,這裡的包和目錄有區別,並不像 Java 語言那樣包和目錄嚴格相關聯的,這一點對於 Java 小夥伴來說需要特別注意.

包是相關程式碼的集合,這些程式碼可能存放於不同的目錄檔案中,就是通過包 package 的宣告告訴 Go編譯器說:我們是一個家族整體.

如果不同的檔案目錄可以宣告在同一個包中,這樣相當於允許家族外遷,只保留姓氏就好.

還是用程式碼說話吧,散落在各地的小夥伴能不能有共同的姓氏!

package main

import (
    "fmt"
    "github.com/snowdreams1006/learn-go/oop/pack"
)

func main() {
    var l = new(pack.Lang)
    l.SetName("Go")
    l.SetWebsite("https://golang.google.cn/")

    fmt.Println(l.ToString())
}

pack.go 原始碼檔案和 pack_test 測試檔案都位於相同的目錄 pack 下且包的宣告也相同都是 pack.

這種情況相當於一家氏族位於一個村落中一起生活,和其他語言到表現一致.

現在試一下這個氏族的一部分人能不能搬到其他村落居住呢?

難不成跨域地域有點大,不支援定義方法嗎?那移動一下使其離 pack 目錄近一點試試看!

還是不行,不能新建子目錄,那麼和原來在一個目錄下呢?

只有這樣是可以被標識位結構體的方法的,如果不是方法,完全可以任意存放,這一點就不再演示了,小夥伴可自行測試一下喲!

package main

import (
    "fmt"
    "github.com/snowdreams1006/learn-go/oop/pack"
)

func main() {
    var l = new(pack.Lang)
    l.SetName("Go")
    l.SetWebsite("https://golang.google.cn/")

    fmt.Println(l.ToString())

    l.PrintLangName()
}

"github.com/snowdreams1006/learn-go/oop/pack" 是當前檔案中匯入依賴包路徑,因此呼叫者能否正常訪問到我們封裝的結構體.

在當前結構體中的屬性被我們設定成了小寫字母開頭,所以不在同一包是無法訪問該屬性的.

封裝後如何擴充套件

設計者封裝好物件供其他人使用,難免會有疏忽不足之處,此時使用者就需要擴充套件已存在的結構體了.

如果是面向物件的設計思路,最簡單的實現方式可能就是繼承了,重寫擴充套件什麼的都不在話下,可是 Go 並不這麼認為,不支援繼承!

所以剩下的方法就是組合了,這也是學習面向物件時的前人總結的一種經驗: 多用組合少用繼承!

現在想一想,Go 語言不但貫徹了這一思想,更是嚴格執行了,因為 Go 直接取消了繼承特性.

type MyLang struct {
    l *Lang
}

func (ml *MyLang) Print() {
    if ml == nil || ml.l == nil {
        return
    }

    fmt.Println(ml.l.ToString())
}

func TestMyLangPrint(t *testing.T) {
    var l = new(Lang)
    l.SetName("Go")
    l.SetWebsite("https://golang.google.cn/")

    var ml = MyLang{l}

    ml.Print()
}

通過自定義結構體內部屬性是 Lang 型別,進而擴充套件原來 Lang 不具備的方法或者重寫原來的方法.

如果我們的自定義結構體剛好只有這麼一個屬性,完全可以使用簡化形式,說到這裡其實有必要特別說明一下,專業叫法稱之為別名.

type Lan Lang

func (l *Lan) PrintWebsite(){
    fmt.Println(l.website)
}

func TestLanPrintWebsite(t *testing.T) {
    var la = new(Lan)
    la.name = "GoLang"
    la.website = "https://golang.google.cn/"

    la.PrintWebsite()
}

作為設計者和使用者都已經考慮到了,封裝的基本知識也要告一段落了,由於 Go 不支援繼承,也沒必要演示相關程式碼,唯一剩下的只有介面了.

雖然 Go 同樣是不支援多型,但是 Go 提供的介面確實與眾不同,別有一番滋味在心頭,下一節將開始探索介面.

關於封裝的覆盤

  • 定義結構體欄位
type Lang struct {
    name    string
    website string
}

結構體有多個欄位時彼此直接換行,不用逗號也不用分號之類的,不要多此一舉.

  • 定義結構體方法
func (l *Lang) GetName() string {
    return l.name
}

原本是普通的函式,函式名前面加入指向當前結構體的引數時,函式不再是函式而是方法,同時當前結構體引數叫做接收者,類似於其他面嚮物件語言中的 thisself 關鍵字實現的效果.

  • 字面量宣告結構體
func TestInitLang(t *testing.T) {
    l := Lang{
        name:    "Go",
        website: "https://golang.google.cn/",
    }

    t.Log(l.ToString())
}

字面量宣告結構體除了這種類似於有參建構函式使用方式,還有無參和全參建構函式使用方式,這裡說的建構函式只是看起來像並不真的是建構函式.

  • new 宣告結構體
func TestPack(t *testing.T) {
    var l = new(Lang)
    l.SetName("Go")
    l.SetWebsite("https://golang.google.cn/")

    t.Log(l.ToString())
}

new 函式和其他主流的程式語言 new 關鍵字類似,用於宣告結構體,不同於字面量宣告方式,new 函式的輸出物件是指標型別.

  • 首字母大小寫控制訪問許可權

不論是變數名還是方法名,名稱首字母大寫表示公開的,小寫表示私有的.

  • 程式碼的基本組織單元是包

訪問控制權限也是針對程式碼包而言,一個目錄下只有一個程式碼包,包名和目錄名沒有必然聯絡.

  • 複合擴充套件已有型別
type MyLang struct {
    l *Lang
}

func (ml *MyLang) Print() {
    if ml == nil || ml.l == nil {
        return
    }

    fmt.Println(ml.l.ToString())
}

func TestMyLangPrint(t *testing.T) {
    var l = new(Lang)
    l.SetName("Go")
    l.SetWebsite("https://golang.google.cn/")

    var ml = MyLang{l}

    ml.Print()
}

自定義結構體內嵌其他結構體,通過複合而不是繼承的方式實現對已有型別的增強控制,也是一種推薦的程式設計規範.

  • 別名擴充套件已有型別
type Lan Lang

func (l *Lan) PrintWebsite() {
    fmt.Println(l.website)
}

別名可以看成單欄位結構體的簡化形式,可以用來擴充套件已存在的結構體型別,也支援方法等特性.

最後,非常感謝你的閱讀,鄙人知識淺薄,如有描述不當的地方,還請各位看官指出,你的每一次留言我都會認真回覆,你的轉發就是對我最大的鼓勵!

如果需要檢視相關原始碼,可以直接訪問 https://github.com/snowdreams1006/learn-go,同時也推薦關注公眾號與我交流.

相關推薦

go 學習筆記詳細封裝是怎麼回事

關注公眾號[雪之夢技術驛站]檢視上篇文章 猜猜看go是不是面嚮物件語言?能不能面向物件程式設計? 雖然在上篇文章中,我們通過嘗試性學習探索了 Go 語言中關於面向物件的相關概念,更確切的說是關於封裝的基本概念以及相關實現. 但那還遠遠不夠,不能滿足於一條路,而是應該儘可能地多走幾條路,只有這樣才能為以後可

《SAS編程與數據挖掘商業案例》學習筆記

ror otto -c ace mov 得到 replace 讀書筆記 集中 繼續讀書筆記,本文重點側重sas觀測值的操作方面, 主要包含:輸出觀測值、更新觀測值、刪除觀測值、停止輸出觀測值等

Android學習筆記詳細講解畫圓角圖片

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

kubernetes學習筆記:kubernetes dashboard認證及分級授權

第一章、部署dashboard 作為Kubernetes的Web使用者介面,使用者可以通過Dashboard在Kubernetes叢集中部署容器化的應用,對應用進行問題處理和管理,並對叢集本身進行管理。通過Dashboard,使用者可以檢視叢集中應用的執行情況,同時也能夠基於Dashboard建立或修

滲透測試學習筆記案例

0x00 前言很久沒有更新部落格了,主要是因為工作很忙,寫部落格也太耗時間了。但是突然發現,許久不寫很多東西都快生疏了。因而決定從今天起開始寫一些跟滲透測試相關的文章,也可以認為是學習筆記吧,留作日後的技術積累和參考吧。0x01 案列分析實驗環境:目標靶機:10.11.1.0

機器學習筆記——整合學習Boosting及AdaBoosting

   上一篇記述了Bagging的思維與應用 : https://blog.csdn.net/qq_35946969/article/details/85045432    本篇記錄Boosting的思想與應用:AdaBoosting、GDBT(

Java Web 學習筆記:RestEasy統一處理異常

JBoss RestEasy框架配置異常統一處理 前提 利用JBoss restEasy框架搭建的restful java web後臺應用 希望通過統一的方式對restful介面丟擲的異常進行

奇舞學院學習筆記CSS頁通

CSS概念與簡單選擇器 版本 CSS Level 1 CSS Level 2(CSS2.1規範) CSS Level 3 Color Module Level 3 Selectors Level 3 Media Queries Fonts Level

設計模式C++學習筆記(c/c++面試筆試題)

一、指標與引用有什麼區別? 1、指標會佔用記憶體,引用不佔用記憶體。 2、引用在定義時必須初始化。 3、沒有空的引用,但是有空的指標。 二、static關鍵的幾個作用 1、函式體內的static變數的作用範圍為該函式體,該變數記憶體只分配一次,因此其值在下次再呼叫該函式時

Go學習筆記高階資料型別

高階資料型別,僅僅是做個概念認識,等到其他相關知識的學習時,再著重分析。 1 function 將 function 作為資料型別的語言有很多,函數語言程式設計的核心理念。 function 是“第一等公民”,function 與其他資料型別一樣,處於平等地位,可以賦值給

CloudFoundry原始碼學習筆記warden ()

# warden 是一個有關資源隔離 和 資源管理 的框架 由三個 Gem [ em-warden-client, warden-client, warden-protocol ] 和 一個 Ruby 專案 [ warden ] 組成## warden-protocol/

Lucene學習筆記)簡介和向文件寫索引並讀取文件

什麼是lucene? lucene就是一個全文檢索的工具包。 Lucene的能幹什麼? 1.      獲取內容(Acquire Content) Lucene不提供爬蟲功能,如果需要獲取內容需要自己建立爬蟲應用。 Lucene只做索引和搜尋工作。 2.建立文件(Buil

go 學習筆記初識 go 語言

Go 是一種開源程式語言,可以輕鬆構建簡單,可靠,高效的軟體. 摘錄自 github: https://github.com/golang/go,其中官網(國外): https://golang.org 和官網(國內): https://golang.google.cn/ Go 是 Google 公司

go 學習筆記環境搭建

千里之行始於足下,開始 Go 語言學習之旅前,首先要搭建好本地開發環境,然後就可以放心大膽瞎折騰了. Go 的環境安裝和其他語言安

go 學習筆記工作空間

搭建好 Go 的基本環境後,現在可以正式開始 Go 語言的學習之旅,初學時建議在預設的 GOPATH 工作空間規範編寫程式碼,基本目錄結構大概是這個樣子. . |-- bin | `-- hello.exe |-- pkg | `-- windows_amd64 | `-- github.

go 學習筆記走進Goland編輯器

工欲善其事必先利其器,命令列工具雖然能夠在一定程度上滿足基本操作的需求,但實際工作中總不能一直使用命令列工具進行編碼操作吧? 學習 Go 語言同樣如此,為此需要尋找一個強大的 IDE 整合環境幫助我們快速開發,據我所知,市面上比較流行的可能有三個選擇: LiteIDE X : LiteIDE 是一款簡單,開

go 學習筆記有意思的變數和不安分的常量

首先希望學習 Go 語言的愛好者至少擁有其他語言的程式設計經驗,如果是完全零基礎的小白使用者,本教程可能並不適合閱讀或嘗試閱讀看看,系列筆記的目標是站在其他語言的角度學習新的語言,理解 Go 語言,進而寫出真正的 Go 程式. 程式語言中一般都有變數和常量的概念,對於學習新語言也是一樣,變數指的是不同程式語言

go 學習筆記值得特別關注的基礎語法有哪些

在上篇文章中,我們動手親自編寫了第一個 Go 語言版本的 Hello World,並且認識了 Go 語言中有意思的變數和不安分的常量. 相信通過上篇文章的斐波那契數列,你已經初步掌握了 Go 語言的變數和常量與其他主要的程式語言的異同,為了接下來更好的學習和掌握 Go 的基礎語法,下面先簡單回顧一下變數和常量

go 學習筆記陣列還是切片都沒什麼不一樣

上篇文章中詳細介紹了 Go 的基礎語言,指出了 Go 和其他主流的程式語言的差異性,比較側重於語法細節,相信只要稍加記憶就能輕鬆從已有的程式語言切換到 Go 語言的程式設計習慣中,儘管這種切換可能並不是特別順暢,但多加練習尤其是多多試錯,總是可以慢慢感受 Go 語言之美! 在學習 Go 的內建容器前,同樣的,

go 學習筆記go是不是面嚮物件語言是否支援面對物件程式設計?

面向物件程式設計風格深受廣大開發者喜歡,尤其是以 C++, Java 為典型代表的程式語言大行其道,十分流行! 有意思的是這兩中語言幾乎毫無意外都來源於 C 語言,卻不同於 C 的面向過程程式設計,這種面向物件的程式設計風格給開發者帶來了極大的便利性,解放了勞動,鬆耦合,高內聚也成為設計的標準,從而讓我們