Go 語言是一種靜態類型的編程語言,所以在編譯器進行編譯的時候,就要知道每個值的類型,這樣編譯器就知道要為這個值分配多少內存,並且知道這段分配的內存表示什么。
提前知道值的類型的好處有很多,比如編譯器可以合理的使用這些值,可以進一步優化代碼,提高執行的效率,減少 bug 等等。
基本類型
基本類型是 Go 語言自帶的類型,比如 數值、浮點、字符串、布爾、數組 及 錯誤 類型,他們本質上是原始類型,也就是不可改變的,所以對他們進行操作,一般都會返回一個新創建的值,所以把這些值傳遞給函數時,其實傳遞的是一個值的副本。
funcmain() { name := "張三" fmt.Println(modify(name)) fmt.Println(name) } funcmodify(sstring)string{ s = s + s return s }
張三張三 張三
以上是一個操作字符串的例子,通過打印的結果,可以看到,本來 name
的值並沒有被改變,也就是說,我們傳遞的時一個副本,並且返回一個新創建的字符串。
基本類型因為是拷貝的值,並且在對他進行操作的時候,生成的也是新創建的值,所以這些類型在多線程里是安全的,我們不用擔心一個線程的修改影響了另外一個線程的數據。
引用類型
引用類型和原始的基本類型恰恰相反,它的修改可以影響到任何引用到它的變量。在 Go 語言中,引用類型有 切片(slice)、字典(map)、接口(interface)、函數(func) 以及 通道(chan) 。
引用類型之所以可以引用,是因為我們創建引用類型的變量,其實是一個標頭值,標頭值里包含一個指針,指向底層的數據結構,當我們在函數中傳遞引用類型時,其實傳遞的是這個標頭值的副本,它所指向的底層結構並沒有被復制傳遞,這也是引用類型傳遞高效的原因。
本質上,我們可以理解函數的傳遞都是值傳遞,只不過引用類型傳遞的是一個指向底層數據的指針,所以我們在操作的時候,可以修改共享的底層數據的值,進而影響到所有引用到這個共享底層數據的變量。
funcmain() { ages := map[string]int{"張三": 20} fmt.Println(ages) modify(ages) fmt.Println(ages) } funcmodify(mmap[string]int) { m["張三"] = 10 }
這是一個很明顯的修改引用類型的例子,函數 modify
的修改,會影響到原來變量 ages
的值。
結構類型
結構類型是用來描述一組值的,比如一個人有身高、體重、名字和年齡等,本質上是一種聚合型的數據類型。
type person struct { age int name string }
要定義一個結構體的類型,通過 type
關鍵字和類型 struct
進行聲明,以上我們就定義了一個結構體類型 person
,它有 age
, name
這兩個字段數據。
結構體類型定義好之后,就可以進行使用了,我們可以用過 var
關鍵字聲明一個結構體類型的變量。
var p person
這種聲明的方式,會對結構體 person
里的數據類型默認初始化,也就是使用它們類型的零值,如果要創建一個結構體變量並初始化其為零值時,這種 var
方式最常用。
如果我們需要指定非零值,就可以使用我們字面量方式了。
jim := person{10, "Jim"}
示例這種我們就為其指定了值,注意這個值的順序很重要,必須和結構體里聲明字段的順序一致,當然我們也可以不按順序,但是這時候我們必須為字段指定值。
jim := person{name: "Jim", age: 10}
使用冒號 :
分開字段名和字段值即可,這樣我們就不用嚴格的按照定義的順序了。
除了基本的原始類型外,結構體內的值也可以是引用類型,或者自己定義的其他類型。具體選擇類型,要根據實際情況,比如是否允許修改值本身,如果允許的話,可以選擇引用類型,如果不允許的話,則需要使用基本類型。
函數傳參是值傳遞,所以對於結構體來說也不例外,結構體傳遞的是其本身以及里面的值的拷貝。
funcmain() { jim := person{10, "Jim"} fmt.Println(jim) modify(jim) fmt.Println(jim) } funcmodify(p person) { p.age = p.age + 10 } type person struct { age int name string }
以上示例的輸出是一樣的,所以我們可以驗證傳遞的是值的副本。如果上面的例子我們要修改 age
的值可以通過傳遞結構體的指針,我們稍微改動下例子
funcmain() { jim := person{10, "Jim"} fmt.Println(jim) modify(&jim) fmt.Println(jim) } funcmodify(p *person) { p.age = p.age + 10 } type person struct { age int name string }
這個例子的輸出是
{10 Jim} {20 Jim}
非常明顯的, age
的值已經被改變。如果結構體里有引用類型的值,比如 map
,那么我們即使傳遞的是結構體的值副本,如果修改這個 map
的話,原結構的對應的 map
值也會被修改,這里不再寫例子,大家可以驗證下。
自定義類型
Go 語言支持我們自定義類型,比如剛剛上面的結構體類型,就是我們自定義的類型,這也是比較常用的自定義類型的方法。
另外一個自定義類型的方法是基於一個已有的類型,就是基於一個現有的類型創造新的類型,這種也是使用 type
關鍵字。
type Duration int64
我們在使用 time
這個包的時候,對於類型 time.Duration
應該非常熟悉,它其實就是基於 int64
這個基本類型創建的新類型,來表示時間的間隔。
但是這里我們注意,雖然 Duration
是基於 int64
創建,覺得他們其實一樣,比如都可以使用數字賦值。
type Duration int64 var i Duration = 100 var j int64 = 100
但是本質上,他們並不是同一種類型,所以對於Go這種強類型語言,他們是不能相互賦值的。
type Duration int64 var dur Duration dur = int64(100) fmt.Println(dur)
上面的例子,在編譯的時候,會報類型轉換的異常錯誤。
cannot use int64(100) (type int64) as type Duration in assignment
Go 的編譯器不會像 Java 的那樣,幫我們做隱式的類型轉換。
有時候,大家會迷茫,已經有了 int64
這些類型了,可以表示,還要基於他們創建新的類型做什么?其實這就是 Go 靈活的地方,我們可以使用自定義的類型做很多事情,比如添加方法,比如可以更明確的表示業務的含義等等。