Go語言中的常見的幾個坑
- 2020 年 10 月 12 日
- 筆記
記錄一下日常中遇到的幾個坑,加深一下印象。
1、for range
這個是比較常見的問題了,我自己也整理一下:
func main() {
l := []int{1,2,3}
fmt.Printf("%p \n", &l)
for _, v := range l {
fmt.Printf("%p : %d \n", &v,v)
}
}
輸出結果
0xc000092080
0xc00018a008 : 1
0xc00018a008 : 2
0xc00018a008 : 3
這邊基本可以看出來了,v是一個臨時分配出來的的記憶體,賦值為當前遍歷的值。因此就可能會導致兩個問題
- 對其本身沒有操作
- 引用的是同一個變數地址
func main() {
l := []int{1, 2, 3}
for _, v := range l {
v+=1
}
fmt.Println(l)
}
//[1 2 3]
func main() {
m := make(map[string]*student)
stus := []student{
{Name: "a"},
{Name: "b"},
{Name: "c"},
}
for _, stu := range stus {
m[stu.Name] = &stu
}
fmt.Println(m)
}
//map[a:0xc000012060 b:0xc000012060 c:0xc000012060]
如果怕用錯的話建議使用index,不要用value:
for i, _ := range list {
list[i]//TODO
}
2、defer與閉包
先來看一下兩組程式碼和答案:
未使用閉包
func main() {
for i := 0; i < 5; i++ {
defer fmt.Printf("%d %p ",i,&i)
}
}
//4 0xc00009a008 3 0xc00009a008 2 0xc00009a008 1 0xc00009a008 0 0xc00009a008
使用閉包
func main() {
for i := 0; i < 5; i++ {
defer func() {
fmt.Printf("%d %p ", i, &i)
}()
}
}
//5 0xc000096018 5 0xc000096018 5 0xc000096018 5 0xc000096018 5 0xc000096018
defer
是一個延時調用關鍵字,會在當前函數執行結束前才被執行,後面的函數先會被編譯,到了快結束前才會被輸出,而不是結束前再進行編譯。下面寫了一些程式碼便於理解:
func main() {
fmt.Println(time.Now().Second())
defer fmt.Println(time.Now().Second())
time.Sleep(time.Second)
}
//19
//19
func main() {
fmt.Println(time.Now().Second())
defer func() {
fmt.Println(time.Now().Second())
}()
time.Sleep(time.Second)
}
//22
//23
從上面程式碼可以看出,defer是及時編譯的,因此在沒有閉包的情況下,時間是相同的,但是在加了閉包之後,遇到defer之後會對匿名函數進行編譯(不會進行函數內的操作),然後打入一個棧里,到了最後才會執行函數內的操作,所以輸出不同。根據這個程式碼再看一下上面的問題。第一個沒有閉包會直接對i進行取值放入棧裡面,最後輸出,因此可以得到想要的結果。但是當有了閉包之後,函數體里的方法不會立即執行,這個i所表現的只是一個記憶體地址,在最後輸出時都指向了同一個地址,因此它的值是相同的。
了解原因之後,解決方法也就很簡單,既然原因是因為傳入參數的地址相同了,那使它不同就行了:
func main() {
for i := 0; i < 5; i++ {
//j:=i
defer func(j int) {
fmt.Printf("%d %p ", j, &j)
}(i)
}
}
//4 0xc000018330 3 0xc000018340 2 0xc000018350 1 0xc000018360 0 0xc000018370
這兩種寫法一樣,都是將當前的值賦值給一個新的對象(相當於指向了新的地址),不過給閉包函數加參數會顯得更加優雅一點。
3、map記憶體溢出
這個問題在個人開發時幾乎不會考慮,當服務數據量很大時才需要注意一下,上一遍文章也專門寫了一下關於go裡面的map的相關內容,具體問題是由於map的刪除並不是真正的釋放記憶體空間,比如一個map裡面有1w個k-v,然後其中5k個不需要被刪除了,接著往裡面繼續添加1k個鍵值對,此時map所佔的記憶體大小很有可能仍為11k個鍵值對的大小,這將會導致所佔用的記憶體會越來越大,造成記憶體溢出。方法就是將原本map中有用的值重新加入到新的map中:
oldMap := make(map[int]int, 10000)
newMap := make(map[int]int, len(oldMap))
for k, v := range oldMap {
newMap[k] = v
}
oldMap = newMap
方法是有了,但是到底該怎麼用呢?下面說一下我個人的看法:
- map是執行緒不安全,如何保證在數據遷移的時候保證線性安全,加鎖,讀寫鎖sync.RWMutex
- 什麼時候遷移,set的時候是不合適的,固定的時間間隔?不太好。因為是刪除導致的記憶體問題,那麼就在delete中進行遷移,添加計數記錄已刪除個數,比如當刪除數目達到10000或者達到某個比例時進行
4、協程泄漏
協程泄漏是我同事開發時遇到的一個問題,這邊我也記錄一下。
什麼是協程泄漏,大體的意思是主程式已經跑完了,但是主程式中開的go協程沒有結束。如何知道協程是否發生了泄漏,最簡單的方法是runtime.NumGoroutine()得到結果是否與你的期望值一樣,如果大了就是發生了泄漏。
哪些問題會導致協程泄漏?
1、死循環
func main() {
defer func() {
time.Sleep(time.Second)
fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
}()
go func() {
select {
}
}()
}
//the number of goroutines: 2
2、鎖(chan的就是鎖+隊列的實現)
func queryAll(n int) int {
ch := make(chan int)
for i := 0; i < n; i++ {
go func(i int) {
time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
ch <- i
}(i)
}
s := <-ch
return s
}
func main() {
queryAll(3)
time.Sleep(time.Second) //查看一段時間後的協程數
fmt.Printf("the number of goroutines: %d", runtime.NumGoroutine())
}
//the number of goroutines: 3
死循環好理解,conrountinue一直在運行,沒有退出。
對於通道舉例說明:海陸空三路一起送一份郵件,只需要第一個送到的,main主協程為收件人,收件人開著門在門口等著收郵件,在收到第一個人的郵件時,門沒關就直接進屋研究去了(主協程結束),後面兩位過一會也到了,但是發現門沒關,認為家裡有人就一直在等著(協程堵塞,資源泄漏)。那麼這時候該怎麼辦?如何close了這個門,那後面兩個人到了發現門是關著的,這麼緊急的郵件居然關門了(並不知道有人已經送到了)就會認為可能出問題了,panic。正確的解決方案可以有下面幾個:
-
放一個信箱,收到的郵件都放裡面,只取第一個;
func queryAll(n int) int { ch := make(chan int, n) for i := 0; i < n; i++ { go func(i int) { time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond) ch <- i }(i) } s := <-ch return s } func main() { queryAll(3) time.Sleep(time.Second) fmt.Printf("the number of goroutines: %d", runtime.NumGoroutine()) } //the number of goroutines: 1
-
知道總共有幾份郵件,收件人在門口都等著全部收完(直接扔了就行)
func queryAll(n int) int { ch := make(chan int) totla:=0 for i := 0; i < n; i++ { go func(i int) { time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond) ch <- i }(i) } s := <-ch for range ch{ totla++ if totla==n-1{ close(ch) } } return s } func main() { queryAll(3) time.Sleep(time.Second) fmt.Printf("the number of goroutines: %d", runtime.NumGoroutine()) } //the number of goroutines: 1
-
還有一種想法是收到第一份郵件後直接通知其他沒有必要再送了,不過這個感覺目前實現不了(協程里需要不斷請求是否有人成功了),有大佬可以幫忙不。
5、http手動關閉
這個算是比較簡單的錯誤了,不關閉的話會發生記憶體泄漏,具體原因沒有了解,個人理解可以將response.body認為一個網路型的os file,和你讀取本地文件效果一樣,數據被寫到快取去了,不關閉的話將會佔用資源。
// An error is returned if there were too many redirects or if there
// was an HTTP protocol error. A non-2xx response doesn't cause an
// error. Any returned error will be of type *url.Error. The url.Error
// value's Timeout method will report true if request timed out or was
// canceled.
//
// When err is nil, resp always contains a non-nil resp.Body.
// Caller should close resp.Body when done reading from it.
//
// Get is a wrapper around DefaultClient.Get.
//
// To make a request with custom headers, use NewRequest and
// DefaultClient.Do.
func Get(url string) (resp *Response, err error) {
return DefaultClient.Get(url)
}
Caller should close resp.Body when done reading from it. 這一句話 go/src/net/http/client.go 里多次提到過了提過,注意一下就行