tft每日頭條

 > 圖文

 > golang都有哪些包

golang都有哪些包

圖文 更新时间:2024-12-16 16:27:29
前言

回想起來使用Go已三年有餘,有很多踩過的坑。Go是門活力四射的語言,語法簡單但表述能力強大且足夠高效,但是也有很多細微的點,這些點就是一些基本細節實現,如果能注意這些細節,我相信我們能夠對Go的理解能更深一些,寫的bug會少一些。

作為一名初學者我們時常寫對一些固定的寫法,不知道為什麼要這麼寫;我們時常寫了一些bug,不知道為什麼bug;我們時常知道可以這麼寫,但是不知道那樣寫是否可以;有時候我們很懶,懶得去測試是否可以,有時候我們很勤快,測試了并且知道答案,但是不求甚解;

再深一點?很多時候我們不求甚解,這天殺的産品經理又在催好像是個不錯的借口。慢慢地又覺得自己理解不夠深刻,所以總是閑暇的時候思考這些問題。我相信在二進制的世界裡,nothing is magic, 一定是有Why的,因為這是我們所創造的世界 ( AI算法除外 ) 。

希望這篇文章能夠幫到你,哪怕隻是一點點。

Common Sense in Go1. interface{} 後面是有{}的現象

go中其他的類型都是沒有{}的, 隻有interface{}有。

理解

go中其他的類型都是沒有{} 的 比如 map[int]int, 但是interface{}都是帶{}的,據說是為了讓你瞅瞅裡邊什麼也沒有。

2. 函數參數是值傳遞的(Passed by value)現象

函數的參數是值傳遞,且在調用的時立即執行值拷貝的。

理解

首先,函數調用是值傳遞的。

所以無論傳遞什麼參數都會被copy到函數的參數變量的内存地址中,堆或者棧上,具體是堆還是棧上涉及到逃逸問題,這裡不做過多分析。但是毫無疑問的是,在調用時立即對變量進行了Copy,以下例子中通過打印變量地址佐證。

funcmain(){ variint fmt.Printf("main:%p\n",&i) foo(i) } funcfoo(iint){ fmt.Printf("foo:%p\n",&i) } //輸出的變量地址不一樣 main:0xc0000a0008 foo:0xc0000a0018

所以對于複雜結構我們應該盡量的傳遞指針減少copy時的開銷。對于這裡有看到不同的觀點,主要是考慮到空指針問題,但是我仍然覺得應該使用指針。理由主要有以下幾點

  • 值傳遞會Copy對象,對于小内容對象,性能相差不大,但是在大結構下存在明顯的性能損耗
  • return 的時候可以直接 return nil, err,代碼精簡更加優雅
  • nil pointer panic 應該通過error handling來解決,不然即使沒有發生panic,也會執行錯誤的邏輯,引入更多的問題。

但是指針傳遞的同時也帶來變量逃逸,和GC壓力,也是一把雙刃劍,好在大部分情況下不需要特别的對GC進行調優。所以,在make it simple的理念下,在需要時再針對性調優是個不錯的選擇。

所以什麼時候我們應該傳遞值,什麼時候應該傳遞指針,這主要取決于copy開銷和是否需要在函數内部對變量值進行更改。我們可以用一個簡單的例子測試下兩者的性能差距:

funcpassedByValue(fooValue){ foo.C="1" } funcpassedByPointer(bar*Value){ bar.C="1" } //值傳遞 funcBenchmark_PassedByValue(b*testing.B){ varvalValue str:=bytes.Buffer{} //這裡為了構建一個大值進行傳遞,小值因為copy代價太小性能差距不明顯。 fori:=0;i<10000000;i { str.Write([]byte("=====")) } val.C=str.String() fori:=0;i<b.N;i { passedByValue(val) } } //指針傳遞 funcBenchmark_PassedByPointer(b*testing.B){ varval=new(Value) str:=bytes.Buffer{} fori:=0;i<10000000;i { str.Write([]byte("=====")) } val.C=str.String() fori:=0;i<b.N;i { passedByPointer(val) } } // Benchmark結果差距也很明顯,但是一般值的copy代價都比較小,差距不明顯。 goos:darwin goarch:amd64 pkg:demo/go Benchmark_PassedByValue-410000000000.676ns/op Benchmark_PassedByPointer-410000000000.383ns/op PASS

一般來說,基本類型我們都應該傳值,自定義類型中一般内容不可控,所以養成良好的習慣很關鍵。特别注意的是slice、map、ctx是引用值類型,所以copy時并沒有copy其中數據,所以一般也進行值傳遞,除非你要對其中更改其中的元素。但如果你需要更改其中的内容,還是建議更改完盡量返回回來一個新的,像内置的append函數一樣,通過返回新的地址來實現。這樣會更加清晰一些,寫代碼時自己盡量不要和自己過不去。

舉個栗子,以下代碼可能是一個bug:

funcmain(){ varids[]int appendSlice(ids) fmt.Println("main",len(ids)) } funcappendSlice(ids[]int){ fori:=0;i<4;i { ids=append(ids,i) } fmt.Println("appendSlice",len(ids)) } //輸出,因為appendSlice中的ids并不是main中的ids. appendSlice4 main0

其次,Copy發生在函數調用的時候。比如利用這個原理就可以使用以下代碼打印函數耗時。

funcdo(){ //因為defer語句執行的時候已經将函數參數轉儲,隻是函數體執行時機有所調整 deferfunc(ttime.Time){ fmt.Println("doCost:"time.Slice(t).Second()) }(time.Now()) //balabalabala }

3. for _, i := range ss, ss 中的元素是 copy 到 變量i 的現象

for range 的時候 slice 中的元素是copy給 變量i的,并且下次for循環,變量i會被直接覆蓋。并不是把 n号元素的地址給了i,i 是第n 号元素的 copy。

理解

Copy會産生兩個變量,i 是個臨時變量,下一次for循環就會被覆寫,而且因為是臨時值,所以以下代碼因為更改也不生效,也是非常常見的bug。

typeUserstruct{ Uidint } funcmain(){ users:=[]User{ {Uid:1},{Uid:2}, } foridx,i:=rangeusers{ i.Uid=2 fmt.Printf("i=%p,user_%d=%p\n",&i,idx,&users[idx]) } fmt.Println(users[0].Uid) } //輸出 //i的地址不變,并且不是元素的地址 i=0xc00008c008,user_0=0xc00008c010 i=0xc00008c008,user_1=0xc00008c018 1//原數組中的userid并沒有發生改變

要更改生效也很簡單,主要有兩種方案,一種是使用切片指針 []*User,這樣對于i的修改會被自動尋址到數字元素上。另一種是使用下标 主動尋址如 users[idx].Uid = 2 。至于[]T還是[]*T 的問題我們接下來再讨論。

這個問題看似簡單,如果将其使用go關鍵字并發将會發生巨大威力,造成血淋淋的事故。

其實用go的公司經常聽到這樣的事故:

  • 某公司發運營push全部發給了同一個uid
  • 某研發發運營消息發短信發給了同一個uid (如果通道商不限制,我相信用戶哭了,哄不好的那種)
  • 批量發優惠券,給同一個uid發了幾百張
  • ....

閉包問題一點都不新鮮,就是由于在go func裡邊使用for了循環的變量i了,然後因為函數體并沒在go的時候立即執行需要申請資源挂載然後由M進行運行需要一些時間,所以一般for循環執行一段時間之後go func才會執行,這時候 内部函數取到的值就得聽天命了。

經典bug複現

funcmain(){ for_,i:=range[]int{1,2,3}{ gofunc(){ println(i) }() } time.Sleep(1*time.Millisecond) } //隻會打印3,因為等到func執行的時候i已經變成3了 //所以把i當做匿名函數的參數傳進去或者在for中重新定義一個變量是個不錯的做法 3 3 3

所以,使用匿名函數的時候go func的時候要時刻注意循環變量的Scope, 該傳參傳參,該重新定義重新定義。好在 Goland 最新版本已經會提示i存在Scope問題了。但是好像沒幾個人會注意IDE警告,所以,習慣很重要,不要寫出IDE警告的代碼也是一個不錯的編程理念。

4. []T 還是 []*T現象

一般來說[]T 會比較高效一些,但是如果T比較大,在For循環時存在Copy開銷,個人覺得[]*T也是可以的。

5. []interface{}并不能接收[]T類型現象

很多時候我們都以為interface可以傳遞任意類型,凡事總有例外,他就不能接收 []T 類型, 如果你需要進行賦值,那你要将T轉成interface{}

理解

因為一個[]interface{}的空間是一定的,但是 []T 不是,因為占用空間不一緻,編譯器覺得有些代價,并沒有進行轉換.

6. Send on closed chan 會Panic,但是 Receive from closed chan 不會現象

往已經關閉的channel 再send數據會觸發runtime panic,但是receive從已經關閉的channel中消費不會觸發.

理解

很多人有誤區,認為chan關閉了就不能再操作了,但是send進chan的數據總歸要消費完的,不然就丢了,你品。

7. Goroutine 之間不能 Recover painc現象

goroutine沒有父子關系(創建應該不算父子吧),不能在一個go中 recover 另一個 go 的 panic

理解

GPM模型在go的調度時沒有上下級關系, 也沒有跨goroutine的異常捕獲機制。

8. error 是一個實現了Error()string 方法的任意類型.現象

error 被定義為 interface{ Error()string },隻要實現該方法的類型,其值都可以認為是error

9. 是否實現某個interface的的判斷是區别對待 *T 和 T 的現象

一個接口實現必須實現接口定義的全部方法,使用 指針類型的receiver 和 值類型的 receiver 是兩個不同的實現。

解釋

*張三不吃香菜,不等于張三不吃香菜。

typeUserinterface{ Eat(foodinterface{})(bool,error) } typeZhangSanstruct{ Namestring } //*ZhangSan實現了User接口 //但是ZhangSan沒有實現 func(*ZhangSan)Eat(foodinterface{})(bool,error){ iffood=="香菜"{ returnfalse,nil } returntrue,nil } funcuserEat(uUser,foodstring)(bool,error){ returnu.Eat(food) } funcmain(){ someone:=ZhangSan{Name:"張三"} //這裡someone是不能傳遞給userEat的 //因為 ZhangSan 這個結構沒有實現 User 接口, 隻能用&ZhangSan進行傳遞。 //userEat(someone,"花生") userEat(&someone,"花生") }

所以,實現接口時receiver類型要統一。

10. Reveiver 在函數調用時其實是作為函數第一參數傳遞給函數的現象

receiver 是可以為 nil 的

解釋

如果你細心看過panic的日志就會發現,打印日志的時候 receiver其實是作為函數第一參數傳遞的。所以,你可以在method中對receiver進行空值判斷,來防止panic的發生。

funcmain(){ varsomeone*ZhangSan _,_=someone.Eat("花生") } //如果在Eat中沒有對receiver進行空值判斷也可能引發空指針異常 goroutine1[running]: main.(*ZhangSan).Eat(0x0,0x10aafc0,0x10e9680,0x0,0x10a9ec0,0xc0000200b8) /Users/haoliu/demo/go/main.go:16 0x26 main.main() /Users/haoliu/demo/go/main.go:30 0x42

總結

以上就Go在日常使用過程中的基本點進行了一下總結,是golang日常使用過程中經常碰到的點。由于水平有限,如果存在某些表述不清楚的地方,可以一起讨論下。

作者:保護我方李元芳,授權發布

鍊接:https://juejin.im/post/6881267557346344974

來源:掘金 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

golang都有哪些包(原來是這麼回事)1

,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关圖文资讯推荐

热门圖文资讯推荐

网友关注

Copyright 2023-2024 - www.tftnews.com All Rights Reserved