Go學(xué)習(xí)筆記筆記

類型

  1. 引用類型特指slice、map、channel這三種預(yù)定義類型。
  2. 內(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外,還可以使用初始化表達式,編譯器生成的指令基本相同)
  3. 具有相同聲明的未命名類型被視作同一類型:
    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)。
  4. 未命名類型轉(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ù)

  1. Go中函數(shù)特點:
    a). 無須前置聲明
    b). 不支持命名嵌套定義(nested)
    c). 不支持同名函數(shù)重載(overload)
    d). 不支持默認參數(shù)
    e). 支持不定長參數(shù)
    f). 支持多返回值
    g). 支持命名返回值
    h). 支持匿名函數(shù)和閉包
  2. 函數(shù)只能判斷其是否為nil,不支持其它操作
  3. 變參本質(zhì)上就是一個切片。只能接收一到多個類型參數(shù),且必須放在列表尾部。
  4. 將匿名函數(shù)賦值給變量,與為普通函數(shù)提供名字標(biāo)識符有著根本的區(qū)別,當(dāng)然,編譯器會為匿名函數(shù)生成一個“隨機”符號名。
  5. 閉包是函數(shù)和引用環(huán)境的組合體。本質(zhì)上返回的是一個funcval結(jié)構(gòu)。
138 type funcval struct {
139     fn uintptr
140     // variable-size, fn-specific data here
141 }
  1. 正因為閉包通過指針引用環(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 )
  })
}
  1. return 語句不是ret匯編指令,它會先更新返回值。return和panic語句都會終止當(dāng)前函數(shù)流程,引發(fā)延遲調(diào)用。
  2. 千萬記住,延遲調(diào)用在函數(shù)結(jié)束時才被執(zhí)行。不合理的使用方式會浪費資源,甚至造成邏輯錯誤。如對一個日志文件的close使用defer可能導(dǎo)致文件不能及時關(guān)閉,資源不能釋放。延遲調(diào)用的性能和直接手工調(diào)用效率相差4倍~5倍。Go 1.5 version
  3. 實現(xiàn)接口的方法集的receiver必須不是pointer reciver,賦值給接口的實例必須不是一個pointer實例。
  4. 在延遲調(diào)用中再次panic,不會影響后續(xù)延遲調(diào)用執(zhí)行。而recover之后panic,可能被再次捕獲,另外,recover必須在延遲調(diào)用函數(shù)中執(zhí)行才能正常工作。
  5. 在正式代碼中,我們不能忽略error返回值,應(yīng)嚴格檢查,否則可能會導(dǎo)致錯誤的邏輯狀態(tài)。調(diào)用多返回值函數(shù)時,除error外,其它返回值同樣需要關(guān)注,如os.File.Read方法,它同時會返回剩余內(nèi)容和EOF
  6. 大量的error處理的解決思路:
    • 使用專門的檢查函數(shù)處理錯誤邏輯,簡化檢查代碼
    • 在不影響邏輯的情況下,使用defer延后處理錯誤狀態(tài)(err退化賦值)
    • 在不中斷邏輯的情況下,將錯誤作為內(nèi)部狀態(tài)保存,等最終“提交”時再處理。
  7. 除非是不可恢復(fù)性、導(dǎo)致系統(tǒng)無法工作的錯誤,否則不建議使用panic

數(shù)據(jù)

  1. 動態(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。

  1. 內(nèi)置函數(shù)len和cap都返回第一緯度的長度
  2. 數(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

  1. 并非所有時候都適合用切片代替數(shù)組,因為切片地城數(shù)組可能會在棧上分配內(nèi)存。而且小數(shù)組在棧上拷貝的消耗也未必就比make代價大。
  2. slice在append時候,如果超出當(dāng)前slice的cap限制,則會重新分配內(nèi)存
    新分配的數(shù)組長度是原cap的2倍,并非原數(shù)組的2倍(并非總是2倍,對于較大的切片,會嘗試擴容1/4,以節(jié)約內(nèi)存)
  3. 向nil切片追加數(shù)據(jù)時,會為其分配底層數(shù)組內(nèi)存
  4. 正因為可能會重新分配內(nèi)存,所以需要留足空間,防止重新分配內(nèi)存的情況
  5. 如果切片長時間引用大數(shù)組中很小的片段,那么建議獨立建立切片,復(fù)制出所需要數(shù)據(jù),以便原數(shù)組內(nèi)存可以被GC隨時回收。
  6. 字典不能被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++
  1. 不能對nil字典做寫操作,但可以讀。
  2. 內(nèi)容為空的字典,與nil是不同的:
    var m1 map[string]int       // nil 字典
    m2 := map[string]int{}     // 內(nèi)容為空的字典
  
    println( m1 == nil , n2 == nil )
    // true false
  1. 字典和切片對象本身就是指針封裝,傳參數(shù)時,無需要再去地址
    最好預(yù)先分配好足夠的空間,減小map擴張時候,內(nèi)存分配和重新hash造成的運行時開銷。
  2. 只有在所有的結(jié)構(gòu)字段都支持相等操作時候,才能對結(jié)構(gòu)進行相等比較。
  3. 空結(jié)構(gòu)(struct{})沒有字段結(jié)構(gòu)類型,無論是單個struct{}變量,或者struct{}數(shù)組,長度都為0。盡管沒分配數(shù)組內(nèi)存,但依然可以操作元素,對應(yīng)切片的len和cap屬性也正常。這類“長度”為0的對象通常都指向runtime.zerobase的變量。
  4. 空結(jié)構(gòu)可作為通道元素類型,用于事件通知。
  5. 未命名類型沒有名字標(biāo)識,無法作為匿名字段,接口指針和多級指針都不能作為匿名字段。
  6. 不能將基礎(chǔ)類型和其指針類型同時嵌入,因為兩者隱式名字相同。
  7. tag并不是注釋,而是對字段進行描述元數(shù)據(jù)。盡管其不屬于數(shù)據(jù)成員,但確實類型的組成部分(在運行時,可以用反射獲取標(biāo)簽信息。被作為格式校驗,數(shù)據(jù)庫關(guān)系映射等)

方法

  1. 不能用多級指針調(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
  1. 如何選擇方法的reveiver類型?
    • 要修改實例狀態(tài),用*T
    • 無須修改狀態(tài)的小對象或固定值,建議用T
    • 大對象建議用*T,以減少復(fù)制成本
    • 引用類型、字符串、函數(shù)等指針包裝對象,直接用T
    • 若包含Mutex等同步字段,用*T,避免因為復(fù)制造成鎖操作無效
    • 其它無法確定的情況,都用*T
  2. 方法會有同名遮蔽問題,利用這種特性,可以實現(xiàn)類似覆蓋(override)操作。(name hiding)
  3. 類型集的判別:
    • 類型T方法集包含所有receiver T方法
    • 類型*T方法集包含所有receiver T + *T方法
    • 匿名嵌入S,T方法集包含所有receiver S方法
    • 匿名嵌入S,T方法集包含所有receiver S+S方法
    • 匿名嵌入S或S,T方法集包含所有receiver S+*S方法
  4. 方法集僅影響接口實現(xiàn)和方法表達式轉(zhuǎn)換,與通過實例或者實例指針調(diào)用方法無關(guān)。實例并不使用方法集,而是直接調(diào)用(通過隱士字段名)
  5. 面向?qū)ο蟮娜筇卣鳌胺庋b”、“繼承”和“多態(tài)”,Go僅實現(xiàn)了部分特征,它更傾向于“組合優(yōu)于繼承”這種思想。將模塊分解成相互獨立的更小但愿,分別處理不同方面的需求,最后以匿名嵌入方式組合到一起,共同實現(xiàn)對外接口。
  6. Method Expression 和 Method Value的區(qū)別:
  7. 通過類型引用的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ù)
    }
  1. 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          
  1. 編譯器會為method value生成一個包裝函數(shù),實現(xiàn)間接調(diào)用。至于receiver復(fù)制,和閉包的實現(xiàn)方法基本相同,打包成funcval,經(jīng)由DX寄存器傳遞。
  2. 當(dāng)method value作為參數(shù)時,會復(fù)制含receiver在內(nèi)的整個method value,當(dāng)目標(biāo)方法的receiver是指針類型,那么被復(fù)制的僅是指針。

接口

  1. 接口除了類型以來,有助于減少用戶可視方法,屏蔽內(nèi)部結(jié)構(gòu)和實現(xiàn)細節(jié)。但接口實現(xiàn)機制會有運行期開銷。對于相同包,或者不會頻繁變化的內(nèi)部模塊之間,并不需要抽象出接口來強行分離。接口最常見的使用場景,是對包外提供訪問,或預(yù)留擴展空間。
  2. 從內(nèi)部實現(xiàn)來看,接口自身也是一種結(jié)構(gòu)類型,只是編譯器會對其作出很多限制。
    type iface struct {
        tab *itab
        data unsafe.Pointer
    }
  • 接口不能有字段
  • 不能定義自己的方法
  • 只能聲明方法,不能實現(xiàn)
  • 可嵌入其它接口類型
  1. 編譯器根據(jù)方法集判斷是否實現(xiàn)了接口。接口變量的默認值是nil,如果實現(xiàn)接口的類型支持,可以做相等運算。
  2. 嵌入其它接口類型,相當(dāng)于將其聲明的方法集導(dǎo)入。這就要求不能有同名方法,因為不支持重載。還有,不能嵌入自身或者循環(huán)嵌入,那會導(dǎo)致遞歸錯誤。
  3. 超級接口變量可以隱士轉(zhuǎn)換為子集,反過來不行。
  4. 接口使用一個名為itab的結(jié)構(gòu)存儲運行期所需的相關(guān)類型信息。
   type iface struct {
       tab  *itab       // 類型信息
       data unsafe.Pointer     // 實際對象指針
   }
   type itab struct {
       inter   *interfacetype      // 接口類型
       _type   *_type                 // 實際對象類型
       fun     [1]uintptr             // 實際對象方法地址
   }
  1. 相關(guān)類型信息里保存了接口和實際對象的元數(shù)據(jù)。同時,itab還用fun數(shù)組(不定長結(jié)構(gòu))保存了實際方法地址,從而實現(xiàn)在運行期對目標(biāo)方法的動態(tài)調(diào)用。
    除此之外,接口還有一個重要特征:將對象復(fù)制給接口變量時,會復(fù)制該對象。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容