類型
- 引用類型特指slice、map、channel這三種預(yù)定義類型。
- 內(nèi)置函數(shù)new按指定類型長度分配零值內(nèi)存,返回指針,并不關(guān)心類型內(nèi)部構(gòu)造和初始化方式。而引用類型則必須使用make函數(shù)創(chuàng)建,編譯器會將make轉(zhuǎn)換為目標(biāo)類型專用的創(chuàng)建函數(shù)(或指令),以確保完成全部內(nèi)存分配和相關(guān)的初始化。(除new/make外,還可以使用初始化表達式,編譯器生成的指令基本相同)
- 具有相同聲明的未命名類型被視作同一類型:
a). 具有相同基類型的指針。
b). 具有相同元素類型和長度的數(shù)組(array)。
c). 具有相同元素類型的切片(slice)。
d). 具有相同鍵值類型的字典(map)。
e). 具有相同數(shù)據(jù)類型及操作方向的通道(channel)。
f). 具有相同字段序列(字段名、字段類型、標(biāo)簽,以及字段順序)的結(jié)構(gòu)體(struct)。
g). 具有相同簽名(參數(shù)和返回值列表,不包括參數(shù)名)的函數(shù)(func)。
h). 具有相同方法集(方法名、方法簽名,不包括順序)的接口(interface)。 - 未命名類型轉(zhuǎn)換規(guī)則:
a). 所屬類型相同。
b). 基礎(chǔ)類型相同,且其中一個是未命名類型。
c). 數(shù)據(jù)類型相同,將雙向通道賦值給單向通道,且其中一個為未命名類型。
d). 將默認值nil賦值給切片、字典、通道、指針、函數(shù)或接口。
e). 對象實現(xiàn)了目標(biāo)接口。
表達式
- 指針類型支持相等運算符,但不能做加減法運算和類型和轉(zhuǎn)換??梢酝ㄟ^unsafe.Pointer將指針轉(zhuǎn)化為uintptr后進行加減法運算,但可能會造成非法訪問。
- Pointer類似C語言中的void*萬能指針,可用來轉(zhuǎn)換指針類型。他能安全持有對象或?qū)ο蟪蓡T,但uintptr不行。后者僅僅是一種特殊的整形,并不引用對象,無法阻止垃圾回收器回收對象內(nèi)存。
- for ... range 會賦值底層對象,如數(shù)組,則會復(fù)制底層數(shù)組。可以改用切片作為range的對象,減少復(fù)制整個數(shù)組的開銷。相關(guān)的數(shù)據(jù)類型中,字符串、切片本身基本結(jié)構(gòu)是個很小的結(jié)構(gòu)體,而字典、通道本身是指針的封裝,復(fù)制成本都很小,無須專門的優(yōu)化。
- 如果range的對象是一個函數(shù),那么該函數(shù)也只被調(diào)用一次。
for i := range data() {
}
- 切片用來代替數(shù)組傳參可避免復(fù)制開銷。并非所有時候都適合用切片代替數(shù)組,因為切片底層數(shù)組可能會在堆上分配內(nèi)存,而小數(shù)組在棧上的拷貝消耗也未必就比make代價大。
- 新建切片對象依舊指向原底層數(shù)組,也就是說修改對所有關(guān)聯(lián)切片可見。
- 從表面上看,指針參數(shù)的性能要更好一些,但實際上得具體分析。被復(fù)制的指針會延長目標(biāo)對象生命周期,可能還會導(dǎo)致它分配到堆上,那么其性能消耗就得加上堆內(nèi)存分配和垃圾回收的成本。
函數(shù)
- Go中函數(shù)特點:
a). 無須前置聲明
b). 不支持命名嵌套定義(nested)
c). 不支持同名函數(shù)重載(overload)
d). 不支持默認參數(shù)
e). 支持不定長參數(shù)
f). 支持多返回值
g). 支持命名返回值
h). 支持匿名函數(shù)和閉包 - 函數(shù)只能判斷其是否為nil,不支持其它操作
- 變參本質(zhì)上就是一個切片。只能接收一到多個類型參數(shù),且必須放在列表尾部。
- 將匿名函數(shù)賦值給變量,與為普通函數(shù)提供名字標(biāo)識符有著根本的區(qū)別,當(dāng)然,編譯器會為匿名函數(shù)生成一個“隨機”符號名。
- 閉包是函數(shù)和引用環(huán)境的組合體。本質(zhì)上返回的是一個funcval結(jié)構(gòu)。
138 type funcval struct {
139 fn uintptr
140 // variable-size, fn-specific data here
141 }
- 正因為閉包通過指針引用環(huán)境變量,那么可能會導(dǎo)致其生命周期延長,設(shè)置被分配到堆內(nèi)存。還有延遲求值的特性。
func test() []func () {
var s []func()
for i:=0; i < 2 ; i++ {
s = append ( s , func() {
println(&i , i )
})
}
return s
}
func main() {
// 這里的test只會被調(diào)用一次
for _, f := range test() {
f()
}
}
// 輸出結(jié)果
0xc420070000 2
0xc420070000 2
// 解決辦法
for i:=0 ; i < 2 ; i++ {
x := i
s = append (s , func() {
println(&x , x )
})
}
- return 語句不是ret匯編指令,它會先更新返回值。return和panic語句都會終止當(dāng)前函數(shù)流程,引發(fā)延遲調(diào)用。
- 千萬記住,延遲調(diào)用在函數(shù)結(jié)束時才被執(zhí)行。不合理的使用方式會浪費資源,甚至造成邏輯錯誤。如對一個日志文件的close使用defer可能導(dǎo)致文件不能及時關(guān)閉,資源不能釋放。延遲調(diào)用的性能和直接手工調(diào)用效率相差4倍~5倍。Go 1.5 version
- 實現(xiàn)接口的方法集的receiver必須不是pointer reciver,賦值給接口的實例必須不是一個pointer實例。
- 在延遲調(diào)用中再次panic,不會影響后續(xù)延遲調(diào)用執(zhí)行。而recover之后panic,可能被再次捕獲,另外,recover必須在延遲調(diào)用函數(shù)中執(zhí)行才能正常工作。
- 在正式代碼中,我們不能忽略error返回值,應(yīng)嚴格檢查,否則可能會導(dǎo)致錯誤的邏輯狀態(tài)。調(diào)用多返回值函數(shù)時,除error外,其它返回值同樣需要關(guān)注,如os.File.Read方法,它同時會返回剩余內(nèi)容和EOF
- 大量的error處理的解決思路:
- 使用專門的檢查函數(shù)處理錯誤邏輯,簡化檢查代碼
- 在不影響邏輯的情況下,使用defer延后處理錯誤狀態(tài)(err退化賦值)
- 在不中斷邏輯的情況下,將錯誤作為內(nèi)部狀態(tài)保存,等最終“提交”時再處理。
- 除非是不可恢復(fù)性、導(dǎo)致系統(tǒng)無法工作的錯誤,否則不建議使用panic
數(shù)據(jù)
- 動態(tài)構(gòu)建字符串容易造成性能問題,通常推薦使用strings.Join函數(shù),它會統(tǒng)計所有參數(shù)長度,并一次性完成分配操作。
字符串buffer可以用類似于vector.reserve(),也能完成相似的工作,并且性能相當(dāng)
var b bytes.Buffer
b.Grow(1000)
b.WriteString("hello world")
對于數(shù)量較小的字符串格式化拼接,可以使用fmt.Sprintf、text/template
字符串操作通常在堆上分配內(nèi)存,這會對Web等高并發(fā)應(yīng)用會造成較大影響,會有大量字符串要做垃圾回收。建議使用[]byte緩存池,或在棧上自行拼裝等方式來實現(xiàn)zero-garbage。
- 內(nèi)置函數(shù)len和cap都返回第一緯度的長度
- 數(shù)組傳參數(shù)時候,為了減少內(nèi)存拷貝,可以用指針接收或者切片
func main() {
var a []int
b := []int {}
println(a == nil , b == nil)
}
上述兩種方式定義的區(qū)別在與,a僅僅定義了一個[]int類型的變量,并未執(zhí)行初始化操作,而b則用初始化表達式完成了全部創(chuàng)建過程。
自然的,a為nil,b不為nil。
另外,a==nil僅僅表示a是一個未初始化的切片對象,切片本身依然會分配所需內(nèi)存??梢灾苯訉η衅鰏lice[:]操作,同樣返回nil
- 并非所有時候都適合用切片代替數(shù)組,因為切片地城數(shù)組可能會在棧上分配內(nèi)存。而且小數(shù)組在棧上拷貝的消耗也未必就比make代價大。
- slice在append時候,如果超出當(dāng)前slice的cap限制,則會重新分配內(nèi)存
新分配的數(shù)組長度是原cap的2倍,并非原數(shù)組的2倍(并非總是2倍,對于較大的切片,會嘗試擴容1/4,以節(jié)約內(nèi)存) - 向nil切片追加數(shù)據(jù)時,會為其分配底層數(shù)組內(nèi)存
- 正因為可能會重新分配內(nèi)存,所以需要留足空間,防止重新分配內(nèi)存的情況
- 如果切片長時間引用大數(shù)組中很小的片段,那么建議獨立建立切片,復(fù)制出所需要數(shù)據(jù),以便原數(shù)組內(nèi)存可以被GC隨時回收。
- 字典不能被cap,并被設(shè)置為no addressable,所以當(dāng)需要更新map的key-value時候,應(yīng)當(dāng)先讀取值存變量中,修改value之后,在重新賦值:
type user struct {
name string
age byte
}
func main() {
m := map[int]user {
1: {"Tom",19},
}
u := m[1]
u.age += 1
m[1] = u
}
但如果內(nèi)部存儲的是指針類型,則可以直接修改:
m2 := map[int]*user {
1 : &user { "wind" , 20 },
}
m2[1].age++
- 不能對nil字典做寫操作,但可以讀。
- 內(nèi)容為空的字典,與nil是不同的:
var m1 map[string]int // nil 字典
m2 := map[string]int{} // 內(nèi)容為空的字典
println( m1 == nil , n2 == nil )
// true false
- 字典和切片對象本身就是指針封裝,傳參數(shù)時,無需要再去地址
最好預(yù)先分配好足夠的空間,減小map擴張時候,內(nèi)存分配和重新hash造成的運行時開銷。 - 只有在所有的結(jié)構(gòu)字段都支持相等操作時候,才能對結(jié)構(gòu)進行相等比較。
- 空結(jié)構(gòu)(struct{})沒有字段結(jié)構(gòu)類型,無論是單個struct{}變量,或者struct{}數(shù)組,長度都為0。盡管沒分配數(shù)組內(nèi)存,但依然可以操作元素,對應(yīng)切片的len和cap屬性也正常。這類“長度”為0的對象通常都指向runtime.zerobase的變量。
- 空結(jié)構(gòu)可作為通道元素類型,用于事件通知。
- 未命名類型沒有名字標(biāo)識,無法作為匿名字段,接口指針和多級指針都不能作為匿名字段。
- 不能將基礎(chǔ)類型和其指針類型同時嵌入,因為兩者隱式名字相同。
- tag并不是注釋,而是對字段進行描述元數(shù)據(jù)。盡管其不屬于數(shù)據(jù)成員,但確實類型的組成部分(在運行時,可以用反射獲取標(biāo)簽信息。被作為格式校驗,數(shù)據(jù)庫關(guān)系映射等)
方法
- 不能用多級指針調(diào)用方法,指針類型的receiver必須是合法指針(包括nil都可以),或者能獲取實例地址
type X struct{}
func (x *X) test() {
println("hi!",x)
}
func main() {
var a *X
a.text() // 相當(dāng)于 test(nil)
}
X{}.test() // 錯誤 cannot take the address of X literal
- 如何選擇方法的reveiver類型?
- 要修改實例狀態(tài),用*T
- 無須修改狀態(tài)的小對象或固定值,建議用T
- 大對象建議用*T,以減少復(fù)制成本
- 引用類型、字符串、函數(shù)等指針包裝對象,直接用T
- 若包含Mutex等同步字段,用*T,避免因為復(fù)制造成鎖操作無效
- 其它無法確定的情況,都用*T
- 方法會有同名遮蔽問題,利用這種特性,可以實現(xiàn)類似覆蓋(override)操作。(name hiding)
- 類型集的判別:
- 類型T方法集包含所有receiver T方法
- 類型*T方法集包含所有receiver T + *T方法
- 匿名嵌入S,T方法集包含所有receiver S方法
- 匿名嵌入S,T方法集包含所有receiver S+S方法
- 匿名嵌入S或S,T方法集包含所有receiver S+*S方法
- 方法集僅影響接口實現(xiàn)和方法表達式轉(zhuǎn)換,與通過實例或者實例指針調(diào)用方法無關(guān)。實例并不使用方法集,而是直接調(diào)用(通過隱士字段名)
- 面向?qū)ο蟮娜筇卣鳌胺庋b”、“繼承”和“多態(tài)”,Go僅實現(xiàn)了部分特征,它更傾向于“組合優(yōu)于繼承”這種思想。將模塊分解成相互獨立的更小但愿,分別處理不同方面的需求,最后以匿名嵌入方式組合到一起,共同實現(xiàn)對外接口。
- Method Expression 和 Method Value的區(qū)別:
- 通過類型引用的method expression 會被還原為普通函數(shù)樣式,receiver是第一參數(shù),調(diào)用時須顯式傳遞。類型可以是T或者*T,只要目標(biāo)方法存在于該類型方法集中即可
type N int
func (n N) test() {
fmt.Printf("test.n:%p,%d\n" , &n , n )
}
func main() {
var n N = 25
fmt.Printf("main.n: %p,%d\n" , &n , n)
f1 := N.test // func (n N)
f1(n) //
f2 := (*N).test // func(n *N)
f2(&n) // 按方法集中的簽名傳遞正確類型的參數(shù)
}
- method value,參數(shù)簽名不會改變,依舊按照正常方式調(diào)用。但當(dāng)method value 被賦值給變量或作為參數(shù)傳遞時,會立即計算并復(fù)制該方法執(zhí)行鎖需要的receiver對象,與其綁定,以便在稍后執(zhí)行時,能隱式傳入receiver參數(shù)。
type N int
func (n N) test() {
fmt.Printf("test.n: %p, %v\n" , &n ,n)
}
func main() {
var n N = 100
p := &n
n++
f1 := n.test // 因為test方法的receiver是N類型
// 因此復(fù)制n , 等于101
n++
f2 := p.test // 復(fù)制p指向的值 等于102
n++
fmt.Prinf("main.n: %p,%v\n" , p , n )
f1()
f2()
}
type N int
func (n N) test() {
fmt.Printf("test.n: %p, %v\n" , &n ,n)
}
func main() {
var n N = 100
p := &n
n++
f1 := n.test // 因為test方法的receiver是N類型
// 因此復(fù)制n , 等于101
n++
f2 := p.test // 復(fù)制p指向的值 等于102
n++
fmt.Prinf("main.n: %p,%v\n" , p , n )
f1()
f2()
}
// main.n: 0xc42007c008,103
// test.n: 0xc42007c020,101
// test.n: 0xc42007c030,102
- 編譯器會為method value生成一個包裝函數(shù),實現(xiàn)間接調(diào)用。至于receiver復(fù)制,和閉包的實現(xiàn)方法基本相同,打包成funcval,經(jīng)由DX寄存器傳遞。
- 當(dāng)method value作為參數(shù)時,會復(fù)制含receiver在內(nèi)的整個method value,當(dāng)目標(biāo)方法的receiver是指針類型,那么被復(fù)制的僅是指針。
接口
- 接口除了類型以來,有助于減少用戶可視方法,屏蔽內(nèi)部結(jié)構(gòu)和實現(xiàn)細節(jié)。但接口實現(xiàn)機制會有運行期開銷。對于相同包,或者不會頻繁變化的內(nèi)部模塊之間,并不需要抽象出接口來強行分離。接口最常見的使用場景,是對包外提供訪問,或預(yù)留擴展空間。
- 從內(nèi)部實現(xiàn)來看,接口自身也是一種結(jié)構(gòu)類型,只是編譯器會對其作出很多限制。
type iface struct {
tab *itab
data unsafe.Pointer
}
- 接口不能有字段
- 不能定義自己的方法
- 只能聲明方法,不能實現(xiàn)
- 可嵌入其它接口類型
- 編譯器根據(jù)方法集判斷是否實現(xiàn)了接口。接口變量的默認值是nil,如果實現(xiàn)接口的類型支持,可以做相等運算。
- 嵌入其它接口類型,相當(dāng)于將其聲明的方法集導(dǎo)入。這就要求不能有同名方法,因為不支持重載。還有,不能嵌入自身或者循環(huán)嵌入,那會導(dǎo)致遞歸錯誤。
- 超級接口變量可以隱士轉(zhuǎn)換為子集,反過來不行。
- 接口使用一個名為itab的結(jié)構(gòu)存儲運行期所需的相關(guān)類型信息。
type iface struct {
tab *itab // 類型信息
data unsafe.Pointer // 實際對象指針
}
type itab struct {
inter *interfacetype // 接口類型
_type *_type // 實際對象類型
fun [1]uintptr // 實際對象方法地址
}
- 相關(guān)類型信息里保存了接口和實際對象的元數(shù)據(jù)。同時,itab還用fun數(shù)組(不定長結(jié)構(gòu))保存了實際方法地址,從而實現(xiàn)在運行期對目標(biāo)方法的動態(tài)調(diào)用。
除此之外,接口還有一個重要特征:將對象復(fù)制給接口變量時,會復(fù)制該對象。