Go语言中的常见的几个坑

TonoT · 2020-10-12 18:26:26 · 1494 次点击 · 预计阅读时间 7 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2020-10-12 18:26:26 的文章,其中的信息可能已经有所发展或是发生改变。

记录一下日常中遇到的几个坑,加深一下印象。

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


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1494 次点击  
加入收藏 微博
4 回复  |  直到 2020-10-14 16:18:07
avtion
avtion · #1 · 5年之前

关于第5点:http手动关闭

并不是不关闭会发生内存泄漏,而是http.Respone需要被完整读取后才能让TCP连接被复用,具体可以阅读以下两篇文章,关键字:HTTP线头阻塞(实际上是TCP的问题)

TonoT
TonoT · #2 · 5年之前
avtionavtion #1 回复

> 关于第5点:http手动关闭 并不是`不关闭会发生内存泄漏`,而是`http.Respone`需要被`完整`读取后才能让TCP连接被复用,具体可以阅读以下两篇文章,关键字:`HTTP线头阻塞`(实际上是`TCP`的问题) - [Go HTTP 重用底层 TCP 连接需要注意的关键点](https://gocn.vip/topics/10626) - [Go中的HTTP请求之——HTTP1.1请求流程分析](https://mp.weixin.qq.com/s/6WYhwaRrjv6W6NZCNw2CeA)

谢谢大佬,这边的原因没有细看,现在了解一下

TonoT
TonoT · #3 · 5年之前
avtionavtion #1 回复

> 关于第5点:http手动关闭 并不是`不关闭会发生内存泄漏`,而是`http.Respone`需要被`完整`读取后才能让TCP连接被复用,具体可以阅读以下两篇文章,关键字:`HTTP线头阻塞`(实际上是`TCP`的问题) - [Go HTTP 重用底层 TCP 连接需要注意的关键点](https://gocn.vip/topics/10626) - [Go中的HTTP请求之——HTTP1.1请求流程分析](https://mp.weixin.qq.com/s/6WYhwaRrjv6W6NZCNw2CeA)

这两天看了下源码并且实验了一下,得出以下结论:

  1. resp.body.close() 释放io资源,防止内存泄漏
  2. body全部读取完毕(io.Copy(os.Stdout, resp.Body)),实现tcp的复用

resp.body本质上是一个io流,它在系统中占用了部分的内存资源,go语言的gc并不会对其进行释放,所以需要我们手动释放。

tcp的多路复用

// Before looping back to the top of this function and peeking on
// the bufio.Reader, wait for the caller goroutine to finish
// reading the response body. (or for cancellation or death)
select {
case bodyEOF := <-waitForBodyRead:
   pc.t.setReqCanceler(rc.req, nil) // before pc might return to idle pool
   alive = alive &&
      bodyEOF &&
      !pc.sawEOF &&
      pc.wroteRequest() &&
      tryPutIdleConn(trace)    //tcp复用
   if bodyEOF {
      eofc <- struct{}{}
   }
case <-rc.req.Cancel:
   alive = false
   pc.t.CancelRequest(rc.req)
case <-rc.req.Context().Done():
   alive = false
   pc.t.cancelRequest(rc.req, rc.req.Context().Err())
case <-pc.closech:
   alive = false
}

当bodyEOF为true是才会走tcp复用(alive等先不管),那么bodyEOF什么时候才会为true

fn: func(err error) error {
   isEOF := err == io.EOF     //io流被读取完
   waitForBodyRead <- isEOF
   if isEOF {
      <-eofc // see comment above eofc declaration
   } else if err != nil {
      if cerr := pc.canceled(); cerr != nil {
         return cerr
      }
   }
   return err
},

因此需要对body读取完毕。

TonoT
TonoT · #4 · 5年之前

以下为实例展示:

func main() {
   for i := 0; i < 2; i++ {
      _, err := http.Get("https://www.baidu.com")
      if err != nil {
         panic(err)
      }
      //io.Copy(ioutil.Discard, resp.Body) 
      //resp.Body.Close()   
   }
}

func main() {
   for i := 0; i < 2; i++ {
      resp, err := http.Get("https://www.baidu.com")
      if err != nil {
         panic(err)
      }
      io.Copy(ioutil.Discard, resp.Body)
      //resp.Body.Close()
   }
}

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传