相較於其他程式語言,Go的型態系統顯得豐富,提供了基本型態、陣列、結構等內建型態,以及slice、map、函式、channel等參考型態,介面基本上分類在參考型態,然而,似乎又沒那麼單純,在變數方面,指標與參考之間的關係,似乎也有點令人迷惑。

傳遞數值還是指標?

Go在一些特性上,不難令人聯想到C。

例如,Go的基本型態如布林、數字與字串,以及自定義型態的結構在指定的行為與C類似,都是進行數值的複製。

陣列方面,C不允許直接將陣列指定給另一個陣列,然而Go卻可以這麼做──因為此時會逐一將陣列中的元素,依序複製至另一個陣列對應索引處,在陣列作為引數或者傳回值時,也是逐一進行複製,在函式間傳遞陣列若有開銷上的考量時,就可以使用指標。

指標對C開發者而言,並不陌生,它代表記憶體位址,而這個位址就是資料實際儲存起始之處。

對於基本型態來說,Go與C在指標上的行為類似,然而基於安全考量,Go並不允許對指標進行運算,而是可以使用&取得存放數值的位址,指定給某個指標型態的變數,位址會複製給該變數──如果想存取指標位址處的數值,可以使用*。由於操作時是同一位址的資料,因此在想要避免陣列於函式間傳遞時的開銷時,使用指標是解決的方式之一。

然而,由於傳遞的是指標,若改變指標位址處的資料,持有相同位址的指標變數,在存取時就會發現相對應的改變,例如,底下程式執行後,arr[0]的結果會是10:

arr := [3]int{1, 2, 3}
ptr := &arr
ptr[0] = 10

在這邊prt[0] = 10是個語法糖,Go編譯器會展開為(*ptr)[0] = 10,因此,實際是在存取指標;除了陣列之外,結構在指定時也是逐一複製欄位,若有開銷上的考量,也是使用指標,然而必須注意到共用時,數值是預期中或者是意外遭到修改的問題。

為複製而生的參考

由於有複製上的開銷及共用上的問題,資料必須為了傳遞傳值或者是傳遞指標而煩惱。

接下來,Go語言告訴你,若只想處理陣列中某片區域,或者以更高階的觀點來看待一片資料,而不是從固定長度的陣列觀點,那麼,可以使用slice,而且它能夠更好地處理共用的問題。例如底下程式執行後,s1[0]的結果會是10:

s1 := []int{1, 2, 3}
s2 := s1
s2[0] = 10

這個程式片段與方才的陣列示範滿像的,s1與s2是指標嗎?不是!Go語言中有Java/Python等語言參考型態的概念,從高階觀點來看,在這邊會說s1與s2參考到同一個slice實例,而不是s1與s2是否擁有相同位址──實際上也不會是,若試著用&s1、&s2取址,得到的會是不同的結果。

技術上來說,slice的參考是包含了三個欄位的資料結構,而各欄位分別儲存了底層陣列位址所用的指標、slice的長度與容量,其中,s1位置儲存的是slice這三個欄位的資料,在s2 := s1時,s2的位置也是有三個欄位,結果是將s1位置三個欄位的數值,複製給s2中的對應欄位。

就上例而言,s1、s2的位置是不同的,然而,s1、s2儲存底層陣列位址用的指標欄位,儲存了相同的位址,s2[0] = 10時,操作的就是相同位址上的陣列,因而透過s1取索引0,才會反映出相同的變化,然而像這類細節,都是Go語言在背後處理掉了。

處理掉的細節不只有這些,對slice進行append時,若slice容量的容量不足,底層會建立足夠容量的新陣列,將原slice底層陣列的元素逐一複製,然後將新元素附加上去,例如,若將上例的第二行,換為s2 := append(s1, 4),在執行過後,s1[0]的結果會是1,因為底層操作的並不是同一陣列了。

所以,這裡與操作陣列的指標考量不同,在使用slice、map、func、channel這類參考型態時,考量的部分,其實是對資料結構打算採用什麼樣的高階操作,將底層細節抽象化,而不僅僅是開銷或共用的問題。

接收者的行為

Go語言可以為結構來定義方法。雖然就術語來說,我們在方法上,必須定義接收者(Receiver),然而,實際上,只是將函式上接受操作對象的參數,移至方法名稱之前,因而在設計方法時,經常會發生的疑問是,如果是結構,接收者要定義為傳遞數值,還是指標呢?

知道嗎?實際上,接收者也可以是參考,換言之,並不是只有結構,才能定義方法。

更具體地說,可透過type、基於某參考型態來定義一個新的型態,或說令其成為一個定義的型態(a defined type),例如type IntSlice []int,如此一來,就可以使用IntSlice來定義接收者,若真的必要,基於int定義一個Int後,也可以使用Int來定義接受者。而Go語言標準程式庫內部,就有不少實現是透過此方式,亦即基於基本型態來定義更高階的行為。

也是說,在考慮方法時,除了考量傳遞時的開銷、共享資料與否或者是對資料結構採用什麼樣的高階操作之外,更進一步地,要考量接收者會有什麼行為。

關於這點,或許從介面定義上,可以看出個端倪。你是否注意過,介面定義行為時,是不包含接收者的型態的!那麼,若有個Savings定義了Deposit與Withdraw行為,var savings Savings所儲存的,是數值、指標,還是參考呢?

技術上來說,savings是個參考,目前指向nil,後續可以指向擁有Deposit與Withdraw行為的接收者,無論接收者是個值或者是指標,甚至是個參考,如果你的接收者是定義為(account *Account),var savings Savings = &Account{"Foo"}時,不能說savings是指標,只能說接收者account擁有Deposit與Withdraw行為,而接收者account的型態是指標,儲存了Account結構實例的位址。

類似地,如果接收者是定義為(account Account),var savings Savings = Account{"Foo"},只能說,接收者account擁有Deposit與Withdraw行為,而接收者account的型態是Account。

數值的本質是什麼?

Go語言具有C語言低階的指標存取特性,亦具有Java/Python等語言高階的參考特性,Go同時具備了數值傳遞上的彈性,以及抽象操作上的方便性,然而要能得心應手,就必須同時掌握C、Java/Python等語言對應型態之特性,不要只採取簡單的答案。

例如,面對new與make的差別時,答案並非只是簡單的「make只能用於slice、map、channel」,否則,類似底下兩行差異性的判別上,就會令人感到困擾:

p := new([]int)
v := make([]int, 100)

若只是採取簡單的答案,選擇上只會更加令人困惑。除了掌握語法上對應之型態特性外,更重要的,或許就如《Go in action》中談到的:「不要只關注某個方法如何處理數值,要關切數值的本質是什麼!」。

作者簡介


Advertisement

更多 iThome相關內容