

Go语言中的常见的几个坑
source link: https://studygolang.com/articles/31108
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

记录一下日常中遇到的几个坑,加深一下印象。
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 里多次提到过了提过,注意一下就行
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK