go學習筆記(2):資料結構
Go語言不是一門面向物件的語言,沒有物件和繼承,也沒有面向物件的多型、重寫相關特性。
Go所擁有的是資料結構,它可以關聯方法。Go也支援簡單但高效的組合(Composition),請搜尋面向物件和組合。
雖然Go不支援面向物件,但Go通過定義資料結構的方式,也能實現與Class相似的功能。
一個簡單的例子,定義一個Animal資料結構:
type Animal struct {
name string
speak string
}
這就像是定義了一個class,有自己的屬性。
在稍後,將會介紹如何向這個資料結構中新增方法,就像為類定義方法一樣。不過現在,先簡單介紹下資料結構。
資料結構的定義和初始化
除了int、string等內建的資料型別,我們可以定義structure來自定義資料型別。
建立資料結構最簡單的方式:
bm_horse := Animal{
name:"baima",
speak:"neigh",
}
注意,上面最後一個逗號","不能省略,Go會報錯,這個逗號有助於我們去擴充套件這個結構,所以習慣後,這是一個很好的特性。
上面bm_horse := Animal{}
中,Animal就像是一個類,這個宣告和賦值的操作就像建立了一個Animal類的例項,也就是物件,其中物件名為bm_horse
,它是這個例項的唯一識別符號。這個物件具有屬性name和speak,它們是每個物件所擁有的key,且它們都有自己的值。從面向物件的角度上考慮,這其實很容易理解。
還可以根據Animal資料結構再建立另外一個例項:
hm_horse := Animal{
name:"heima",
speak:"neigh",
}
bm_horse
和hm_horse
都是Animal的例項,根據Animal資料結構建立而來,這兩個例項都擁有自己的資料結構。如下圖:
從另一種角度上看,bm_horse
這個名稱其實是這個資料結構的一個引用。再進一步考慮,其實面向物件的類和物件也是一種資料結構,每一個物件的名稱(即bm_horse
)都是對這種資料結構的引用。關於這一點,在後面介紹指標的時候將非常有助於理解。
以下是兩外兩種有效的資料結構定義方式:
// 定義空資料結構 bm_horse := Animal{} // 或者,先定義一部分,再賦值 bm_horse := Animal {name:"baima"} bm_horse.speak = "neigh"
此外,還可以省略資料結構中的key部分(也就是屬性的名稱)直接為資料結構中的屬性賦值,只不過這時賦的值必須和key的順序對應。
bm_horse := Animal{"baima","neigh"}
在資料結構的屬性數量較少的時候,這種賦值方式也是不錯的,但屬性數量多了,不建議如此賦值,因為很容易混亂。
訪問資料結構的屬性
要訪問一個數據結構中的屬性,如下:
package main
import ("fmt")
func main(){
type Animal struct {
name string
speak string
}
bm_horse := Animal{"baima","neigh"}
fmt.Println("name:",bm_horse.name)
fmt.Println("speak:",bm_horse.speak)
}
前面說過,Animal是一個數據結構的模板(就像類一樣),不是例項,bm_horse
才是具體的例項,有自己的資料結構,所以,要訪問自己資料結構中的資料,可以通過自己的名稱來訪問自己的屬性:
bm_horse.name
bm_horse.speak
指標
bm_horse := Animal{}
表示返回一個數據結構給bm_horse,bm_horse指向這個資料結構,也可以說bm_horse是這個資料結構的引用。
除此,還有另一種賦值方式,比較下兩種賦值方式:
bm_horse := Animal{"baima","neigh"}
ref_bm_horse := &Animal{"baima","neigh"}
這兩種賦值方式,有何不同?
:=
操作符都宣告左邊的變數,並賦值變數。賦值的內容基本神似:
- 第一種將整個資料結構賦值給變數
bm_horse
,bm_horse
從此變成Animal的例項; - 第二種使用了一個特殊符號
&
在資料結構前面,它表示返回這個資料結構的引用,也就是這個資料結構的地址,所以ref_bm_horse
也指向這個資料結構。
那bm_horse
和ref_bm_horse
都指向這個資料結構,有什麼區別?我打算用perl語言的語法來解釋它們的區別,因為C和Go的指標太過"晦澀"。
perl中的引用
在Perl中,一個hash結構使用%
符號來表示,例如:
%Animal = (
name => "baima",
speak => "neigh",
);
這裡的"Animal"表示的是這個hash結構的名稱,然後通過%+NAME
的方式來引用這個hash資料結構。其實hash結構的名稱"Animal"就是這個hash結構的一個引用,表示指向這個hash結構,只不過這個Animal
是建立hash結構是就指定好的已命名的引用。
perl中還支援顯式地建立一個引用。例如:
$ref_myhash = \%Animal;
%Animal
表示的是hash資料結構,加上\
表示這個資料結構的一個引用,這個引用指向這個hash資料結構。perl中的引用是一個變數,所以使用$ref_myhash
表示。
也就是說,hash結構的名稱Animal
和$ref_myhash
是完全等價的,都是hash結構的引用,也就是指向這個資料結構,也就是指標。所以,%Animal
能表示取hash結構的屬性,%$ref_myhash
也能表示取hash結構的屬性,這種從引用取回hash資料結構的方式稱為"解除引用"。
另外,$ref_myhash
是一個變數型別,而%Animal
是一個hash型別。
引用變數可以賦值給另一個引用變數,這樣兩個引用都將指向同一個資料結構:
$ref_myhash1 = $ref_myhash;
現在,$ref_myhash
、$ref_myhash1
和Animal
都指向同一個資料結構。
Go中的指標:引用
總結下上面perl相關的程式碼:
%Animal = (
name => "baima",
speak => "neigh",
);
$ref_myhash = \%Animal;
$ref_myhash1 = $ref_myhash;
%Animal
是hash結構,Animal
、$ref_myhash
、$ref_myhash1
都是這個hash結構的引用。
回到Go語言的資料結構:
bm_horse := Animal{}
hm_horse := &Animal{}
這裡的Animal{}
是一個數據結構,相當於perl中的hash資料結構:
(
name => "baima",
speak => "neigh",
)
bm_horse是資料結構的直接賦值物件,它直接表示資料結構,所以它等價於前面perl中的%Animal
。而hm_horse
是Animal{}
資料結構的引用,它等價於perl中的Animal
、$ref_myhash
、$ref_myhash1
。
之所以Go中的指標不好理解,就是因為資料結構bm_horse和引用hm_horse都沒有任何額外的標註,看上去都像是一種變數。但其實它們是兩種不同的資料型別:一種是資料結構,一種是引用。
Go中的星號"*"
星號有兩種用法:
x *int
表示變數x是一個引用,這個引用指向的目標資料是int型別。更通用的形式是x *TYPE
*x
表示x是一個引用,*x
表示解除這個引用,取回x所指向的資料結構,也就是說這是 一個數據結構,只不過這個資料結構可能是內建資料型別,也可能是自定義的資料結構
x *int
的x是一個指向int型別的引用,而&y
返回的也是一個引用,所以&y
的y如果是int型別的資料,&y
可以賦值給x *int
的x。
注意,x的資料型別是*int
,不是int,雖然x所指向的是資料型別是int。就像前面perl中的引用只是一個變數,而其指向的卻是一個hash資料結構一樣。
*x
代表的是資料結構自身,所以如果為其賦值(如*x = 2
),則新賦的值將直接儲存到x指向的資料中。
例如:
package main
import ("fmt")
func main(){
var a *int
c := 2
a = &c
d := *a
fmt.Println(*a) // 輸出2
fmt.Println(d) // 輸出2
}
var a *int
定義了一個指向int型別的資料結構的引用。a = &c
中,因為&c
返回的是一個引用,指向的是資料結構c,c是int型別的資料結構,將其賦值給a,所以a也指向c這個資料結構,也就是說*a
的值將等於2。所以d := *a
賦值後,d自身是一個int型別的資料結構,其值為2。
Go函式引數傳值
Go函式給引數傳遞值的時候是以複製的方式進行的。
因為複製傳值的方式,如果函式的引數是一個數據結構,將直接複製整個資料結構的副本傳遞給函式,這有兩個問題:
- 函式內部無法修改傳遞給函式的原始資料結構,它修改的只是原始資料結構拷貝後的副本
- 如果傳遞的原始資料結構很大,完整地複製出一個副本開銷並不小
例如,第一個問題:
package main
import ("fmt")
type Animal struct {
name string
weight int
}
func main(){
bm_horse := Animal{
name: "baima",
weight: 60,
}
add(bm_horse)
fmt.Println(bm_horse.weight)
}
func add(a Animal){
a.weight += 10
}
上面的輸出結果仍然為60。add函式用於修改Animal的例項資料結構中的weight屬性。當執行add(bm_horse)
的時候,bm_horse
傳遞給add()函式,但並不是直接傳遞給add()函式,而是複製一份bm_horse
的副本賦值給add函式的引數a,所以add()中修改的a.weight
的屬性是bm_horse
的副本,而不是直接修改的bm_horse,所以上面的輸出結果仍然為60。
為了修改bm_horse所在的資料結構的值,需要使用引用(指標)的方式傳值。
只需修改兩個地方即可:
package main
import ("fmt")
type Animal struct {
name string
weight int
}
func main(){
bm_horse := &Animal{
name: "baima",
weight: 60,
}
add(bm_horse)
fmt.Println(bm_horse.weight)
}
func add(a *Animal){
a.weight += 10
}
為了修改傳遞給函式引數的資料結構,這個引數必須是直接指向這個資料結構的。所以使用add(a *Animal)
,既然a是一個Animal資料結構的一個例項的引用,所以呼叫add()的時候,傳遞給add()中的引數必須是一個Animal資料結構的引用,所以bm_horse
的定義語句中使用&
符號。
當呼叫到add(bm_horse)
的時候,因為bm_horse
是一個引用,所以賦值給函式引數a時,複製的是這個資料結構的引用,使得add能直接修改其外部的資料結構屬性。
大多數時候,傳遞給函式的資料結構都是它們的引用,但極少數時候也有需求直接傳遞資料結構。
屬於資料結構的函式
可以為資料結構定義屬於自己的函式。
package main
import ("fmt")
type Animal struct {
name string
weight int
}
func (a *Animal) add() {
a.weight += 10
}
func main() {
bm_horse := &Animal{"baima",70}
bm_horse.add()
fmt.Println(bm_horse.weight) // 輸出80
}
上面的add()函式定義方式func (a *Animal) add(){}
,它所表示的就是定義於資料結構Animal上的函式,就像類的例項方法一樣,只要是屬於這個資料結構的例項,都能直接呼叫這個函式,正如bm_horse.add()
一樣。
構造器
面向物件中有構造器(也稱為構造方法),可以根據類構造出類的例項:物件。
Go雖然不支援面向物件,沒有構造器的概念,但也具有構造器的功能,畢竟構造器只是一個方法而已。只要一個函式能夠根據資料結構返回這個資料結構的一個例項物件,就可以稱之為"構造器"。
例如,以下是Animal資料結構的一個建構函式:
func newAnimal(n string,w int) *Animal {
return &Animal{
name: n,
weight: w,
}
}
以下返回的是非引用型別的資料結構:
func newAnimal(n string,w int) Animal {
return Animal{
name: n,
weigth: w,
}
}
一般上面的方法型別稱為工廠方法,就像工廠一樣根據模板不斷生成產品。但對於建立資料結構的例項來說,一般還是會採用內建的new()方式。
new函式
儘管Go沒有構造器,但Go還有一個內建的new()函式用於為一個數據結構分配記憶體。其中new(x)
等價於&x{}
,以下兩語句等價:
bm_horse := new(Animal)
bm_horse := &Animal{}
使用哪種方式取決於自己。但如果要進行初始化賦值,一般採用第二種方法,可讀性更強:
# 第一種方式
bm_horse := new(Animal)
bm_horse.name = "baima"
bm_horse.weight = 60
# 第二種方式
bm_horse := &Animal{
name: "baima",
weight: 60,
}
擴充套件資料結構的欄位
在前面出現的資料結構中的欄位資料型別都是簡簡單單的內建型別:string、int。但資料結構中的欄位可以更復雜,例如可以是map、array等,還可以是自定義的資料型別(資料結構)。
例如,將一個指向同類型資料結構的欄位新增到資料結構中:
type Animal struct {
name string
weight int
father *Animal
}
其中在此處的*Animal
所表示的資料結構例項很可能是其它的Animal例項物件。
上面定義了father,還可以定義son,sister等等。
例如:
bm_horse := &Animal{
name: "baima",
weight: 60,
father: &Animal{
name: "hongma",
weight: 80,
father: nil,
},
}
composition
Go語言支援Composition(組合),它表示的是在一個數據結構中巢狀另一個數據結構的行為。
package main
import (
"fmt"
)
type Animal struct {
name string
weight int
}
type Horse struct {
*Animal // 注意此行
speak string
}
func (a *Animal) hello() {
fmt.Println(a.name)
fmt.Println(a.weight)
//fmt.Println(a.speak)
}
func main() {
bm_horse := &Horse{
Animal: &Animal{ // 注意此行
name: "baima",
weight: 60,
},
speak: "neigh",
}
bm_horse.hello()
}
上面的Horse資料結構中包含了一行*Animal
,表示Animal的資料結構插入到Horse的結構中,這就像是一種面向物件的類繼承。注意,沒有給該欄位顯式命名,但可以隱式地訪問Horse組合結構中的欄位和函式。
另外,在構建Horse例項的時候,必須顯式為其指定欄位名(儘管資料結構中並沒有指定其名稱),且欄位的名稱必須和資料結構的名稱完全相同。
然後呼叫屬於Animal資料結構的hello方法,它只能訪問Animal中的屬性,所以無法訪問speak屬性。
很多人認為這種程式碼共享的方式比面向物件的繼承更加健壯。
Go中的過載overload
例如,將上面屬於Animal資料結構的hello函式過載為屬於Horse資料結構的hello函式:
package main
import (
"fmt"
)
type Animal struct {
name string
weight int
}
type Horse struct {
*Animal // 注意此行
speak string
}
func (h *Horse) hello() {
fmt.Println(h.name)
fmt.Println(h.weight)
fmt.Println(h.speak)
}
func main() {
bm_horse := &Horse{
Animal: &Animal{ // 注意此行
name: "baima",
weight: 60,
},
speak: "neigh",
}
bm_horse.hello()
}