1. 程式人生 > 實用技巧 >Go語言中的常見的幾個坑

Go語言中的常見的幾個坑

目錄

記錄一下日常中遇到的幾個坑,加深一下印象。

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

方法是有了,但是到底該怎麼用呢?下面說一下我個人的看法:

  1. map是執行緒不安全,如何保證在資料遷移的時候保證線性安全,加鎖,讀寫鎖sync.RWMutex
  2. 什麼時候遷移,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。正確的解決方案可以有下面幾個:

  1. 放一個信箱,收到的郵件都放裡面,只取第一個;

    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
    
  2. 知道總共有幾份郵件,收件人在門口都等著全部收完(直接扔了就行)

    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
    
  3. 還有一種想法是收到第一份郵件後直接通知其他沒有必要再送了,不過這個感覺目前實現不了(協程裡需要不斷請求是否有人成功了),有大佬可以幫忙不。

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 裡多次提到過了提過,注意一下就行