Go語言中的常見的幾個坑

2020-10-13 go的慢慢學習路

1、for range

這個是比較常見的問題了,我自己也整理一下:

轉自:https://studygolang.com/articles/31108#reply2

參考:go語言中文文檔:www.topgoer.com

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//19func 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

  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

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

相關焦點

  • Go語言中for range的"坑"
    前言Go 中的for range組合可以和方便的實現對一個數組或切片進行遍歷,但是在某些情況下使用for range時很可能就會被"坑",下面用一段代碼來模擬下:func main() { arr1 := []int{1, 2, 3} arr2 := make([]*
  • 「Go 語言教程」Go語言函數說明
    我們知道在程序設計中,模塊設計是最小的設計單元,模塊設計可以對應面相對象設計中的類設計,也可以對應到函數(方法)設計。編程中很多時候函數作為模塊設計的最小單元。函數設計也有很多方法和規定,以及設計原則。那麼go語言的函數都是怎麼樣子的,都有些什麼原則和要求呢,那麼怎麼做好函數方法設計呢,就讓我們一起來學習學習。
  • 老王學習go語言——3.Go語言基礎 -第一個go程序
    慣例上書名《Go語言高並發與微服務實戰》第一部分 Go語言生平Go出身名門,是鼎鼎大名的google的親兒子,至於是不是唯一語言方面的兒子,這個我查了一下,Baidu沒告訴我,姑且認為他是google的獨生子吧,如果還有其他的,請各位提示孤陋寡聞的我。Go出生於2007年,09年由於表現優異,就開源了。
  • JAVA與GO語言哪個更容易學?
    一,GO語言的優劣勢Go開發中的痛點編譯慢,失控的依賴,個工程師只是用了一個語言裡面的一部分,程序難以維護(可讀性差、文檔不清晰等),更新的花費越來越長,交叉編譯困難Go語言的優勢學習曲線容易MGo語言語法簡單,包含了類C語法。
  • [Go 語言教程] Go 語言簡介
    Go 語言教程1 Go 語言介紹Go 即Golang,是Google公司2009年11月正式對外公開的一門程式語言。Go是靜態強類型語言,是區別於解析型語言的編譯型語言。解析型語言——原始碼是先翻譯為中間代碼,然後由解析器對代碼進行解釋執行。編譯型語言——原始碼編譯生成機器語言,然後由機器直接執行機器碼即可執行。
  • 「Go 語言教程」 Go語言結構
    1 目錄和源碼首先我門看目錄和源碼,從之前的Go 語言教程我們知道,Go語言有工程目錄,和GOPATH環境變量對應,工程目錄結構有bin 存放編譯後的可執行文件src 存放實現源碼,go get工具獲取的web上的模塊包都會放到這個目錄下,並有對應的目錄結構pkg 存放編譯後的庫文件(分不同平臺)Go語言的源碼文件格式為.go格式。
  • GO語言入門
    GO 命令源碼文件1)命令源碼文件定義:命令源碼文件是程序的運行入口,如果一個源碼文件聲明屬於main包,並且包含一個無參數聲明的main函數,那麼它就是命令源碼文件2)命令源碼文件接收參數的包:GO語言標準庫中有一個代碼包flag專門用於接收和解析程序參數A. flag.StringVar()flag.StringVar
  • go語言在使用中有哪些好處呢
    隨著網際網路的不斷發展,程式語言的種類也越來越多。每個程式語言都具有不同的優缺點,那麼在選擇使用程式語言的時候應該了解什麼內容呢?其實最重要的就是程式語言的優缺點。下面給大家介紹go語言的優點和好處,在使用語言時做出正確的選擇。
  • 「GCTT 出品」Go 語言中的 Monkey 補丁
    現在讓我們看看在 Go 語言中 函數的值是怎麼實現的。Go 語言中的函數值是如何工作的看看下面的代碼:packagemainimport ("fmt""unsafe")funca() int { return1 }funcmain() {f :=afmt.Printf("0x%x\n", *(*uintptr)(unsafe.Pointer(&f)))}funcval.go 由 GitHub 託管 查看源文件
  • Go語言的魅力
    給周末無事,想充電的朋友分享一下我最近迷戀上了Go語言的理由[偷笑],起初讓我了解它,是它那幾位殿堂級的創始人,後來去年B站原始碼洩露,幾乎整站全是go語言架構,讓我映像深刻,而近年國內很多巨頭阿里,頭條,小米,360,美團,螞蟻,騰訊等的開源項目可見,go就是一門未來具有絕對魅力的語言,我很多朋友公司裡,無歷史包袱的新項目都在優先考慮golang架構。
  • 在 Go 語言中管理 Concurrency 的三種方式
    相信大家踏入 Go 語言的世界,肯定是被強大的並發(Concurrency)所吸引,Go 語言用最簡單的關鍵字go就可以將任務丟到後臺處理,但是開發者怎麼有效率的控制並發,這是入門 Go 語言必學的技能,本章會介紹幾種方式來帶大家認識並發,而這三種方式分別對應到三個不同的名詞:WaitGroup,Channel,及 Context。下面用簡單的範例帶大家了解。
  • Go語言環境安裝
    技多不壓身,在學習過程中記錄下來,以備後續參考,希望對有同樣需求的小夥伴有所幫助。選擇一個print.go文件,通過window命令行工具運行go run命令,看看輸出效果。以上print.go測試了go語言中各類數據類型的列印結果輸出。
  • 我們為什麼要使用Go語言?
    在這些新語言中,包含D,Go,Rust和Vala語言,Go曾一度出現在TIOBE的排行榜上面。與其他新語言相比,Go的魅力明顯要大很多。Go的成熟特徵會得到許多開發者的欣賞,而不僅僅是因為其誇大其詞的曝光度。
  • 快速轉型golang(go語言)web開發 03-Go語言的特點
    穩定的二進位接口go自發布1.0時,就承諾向後兼容,所以即使用高版本的go去運行之前用低版本go寫的代碼,一樣可以運行。這也是go可以作為工業級語言的前提。穩定的語言特性自1.0版發布以來,go很少像其它語言那樣在語言層面添加新的語法(再也不用擔心學不動了有木有)實實在在的性能go的每次版本升級,都會帶來或多或少的性能提升以及bug修復(所以對於程式設計師而言,我們就很省心了,不需要像學java那樣還得學很多額外的性能調優知識)Go語言 vs 其它技術棧Go的開發速度跟python
  • Go-INI - 超讚的Go語言INI文件操作庫
    然而,日漸受到關注的 Go 語言,其官方庫並沒有對 INI 文件操作的庫,而進行 INI 文件的解析也並不是幾行就能完成的簡單工作。Go-INI,就為 Go 語言添加了對 INI 文件進行讀取、解析和寫入等操作,使得 Go 項目也能充分利用 INI 文件的便利性。
  • Go語言愛好者周刊:第 62 期—今天的題很細節
    lt;< len(s) / 128var b byte = 1 << len(s[:]) / 128func main() {  println(a, b)}這裡涉及到兩個知識點,在 Go 語言規範中都有明確的說明
  • go語言channel使用以及原理解析
    參考:go專家編程參考:go語言中文文檔 www.topgoer.com2. chan數據結構src/runtime/chan.go:hchan定義了channel的數據結構:type hchan struct { qcount
  • Go語言愛好者周刊:第 58 期—關於 context
    這裡記錄每周值得分享的 Go 語言相關內容,周日發布。本周刊開源(GitHub:polaris1119/golangweekly[1]),歡迎投稿,推薦或自薦文章/軟體/資源等,請提交 issue[2] 。鑑於大部分人可能沒法堅持把英文文章看完,因此,周刊中會儘可能推薦優質的中文文章。
  • 一分鐘系列:go語言運行時-垃圾回收
    垃圾回收能讓讓我們省去自己釋放內存的煩惱,go語言的運行時會幫我們自動去釋放不用了的內存。在介紹go垃圾回收原理之前,我們先了解下常見的垃圾回收方法。引用計數引用計數是對我們使用的每塊內存都做一個計數器,當計數器為0的時候,就釋放這塊內存。
  • 老王學習go語言——3.go語言基礎語法和關鍵字 gdb調試
    慣例書名放在最上面《go語言高並發與微服務實戰》開始基礎語法和關鍵字之前,上一篇遺留了幾個問題:go語言debug之Gdb Version,之前沒用過C++和C,所以對於Gdb不是很熟悉,也不想特別麻煩去搞,於是想偷個懶,利用網上的方法,去下載liteidex,準備利用裡面的gdb64.exe,但下載了最新版,裡面沒有gdb的可執行文件。