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

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语言中的常见的几个坑的更多相关文章

  1. c语言中较常见的由内存分配引起的错误_内存越界_内存未初始化_内存太小_结构体隐含指针

    1.指针没有指向一块合法的内存 定义了指针变量,但是没有为指针分配内存,即指针没有指向一块合法的内浅显的例子就不举了,这里举几个比较隐蔽的例子. 1.1结构体成员指针未初始化 struct stude ...

  2. Python基础学习-Python中最常见括号()、[]、{}的区别

    Python中最常见括号的区别: 在Python语言中最常见的括号有三种,分别是:小括号().中括号[].花括号{}:其作用也不相同,分别用来代表不同的Python基本内置数据类型. Python中的 ...

  3. Python中最常见括号()、[]、{}的区别

    在Python语言中最常见的括号有三种,分别是:小括号().中括号[].花括号{}:其作用也不相同,分别用来代表不同的Python基本内置数据类型. Python中的小括号(): 代表tuple元祖数 ...

  4. C语言初学者代码中的常见错误与瑕疵(23)

    见:C语言初学者代码中的常见错误与瑕疵(23)

  5. 一个超复杂的间接递归——C语言初学者代码中的常见错误与瑕疵(6)

    问题: 问题出处见 C语言初学者代码中的常见错误与瑕疵(5) . 在该文的最后,曾提到完成的代码还有进一步改进的余地.本文完成了这个改进.所以本文讨论的并不是初学者代码中的常见错误与瑕疵,而是对我自己 ...

  6. C语言初学者代码中的常见错误与瑕疵(5)

    问题: 素数 在世博园某信息通信馆中,游客可利用手机等终端参与互动小游戏,与虚拟人物Kr. Kong 进行猜数比赛. 当屏幕出现一个整数X时,若你能比Kr. Kong更快的发出最接近它的素数答案,你将 ...

  7. C语言初学者代码中的常见错误与瑕疵(19)

    见:C语言初学者代码中的常见错误与瑕疵(19)

  8. C语言初学者代码中的常见错误与瑕疵(14)

    见:C语言初学者代码中的常见错误与瑕疵(14) 相关链接:http://www.anycodex.com/blog/?p=87

  9. 分数的加减法——C语言初学者代码中的常见错误与瑕疵(12)

    前文链接:分数的加减法——C语言初学者代码中的常见错误与瑕疵(11) 重构 题目的修正 我抛弃了原题中“其中a, b, c, d是一个0-9的整数”这样的前提条件,因为这种限制毫无必要.只假设a, b ...

随机推荐

  1. HDU - 1261-字串数 (排列组合+大数)

    一个A和两个B一共可以组成三种字符串:"ABB","BAB","BBA". 给定若干字母和它们相应的个数,计算一共可以组成多少个不同的字符串 ...

  2. 深入了解Netty【二】零拷贝

    引言 以下翻译自:Zero Copy I: User-Mode Perspective 零拷贝是什么? 为了更好地理解问题的解决方案,我们首先需要理解问题本身.让我们来看看什么是参与网络服务器的简单过 ...

  3. nginx的gzip压缩

    随着nginx的发展,越来越多的网站使用nginx,因此nginx的优化变得越来越重要,今天我们来看看nginx的gzip压缩到底是怎么压缩的呢? gzip(GNU-ZIP)是一种压缩技术.经过gzi ...

  4. 小程序开发-使用xpath解析网页html中的数据

    最新有个微信小程序的开发需求,需要从网页中提取一些元素信息,获取有效数据 1. 了解到微信小程序里面不能直接操作dom元素,所以我们需要使用一些其他的npm包 2. 经过查到各方面的文档,最新决定用x ...

  5. 用C、python手写redis客户端,兼容redis集群 (-MOVED和-ASK),快速搭建redis集群

    想没想过,自己写一个redis客户端,是不是很难呢? 其实,并不是特别难. 首先,要知道redis服务端用的通信协议,建议直接去官网看,博客啥的其实也是从官网摘抄的,或者从其他博客抄的(忽略). 协议 ...

  6. [LeetCode]105. 从前序与中序遍历序列构造二叉树(递归)、108. 将有序数组转换为二叉搜索树(递归、二分)

    题目 05. 从前序与中序遍历序列构造二叉树 根据一棵树的前序遍历与中序遍历构造二叉树. 注意: 你可以假设树中没有重复的元素. 题解 使用HashMap记录当前子树根节点在中序遍历中的位置,方便每次 ...

  7. [LeetCode]347. 前 K 个高频元素(堆)

    题目 给定一个非空的整数数组,返回其中出现频率前 k 高的元素. 示例 1: 输入: nums = [1,1,1,2,2,3], k = 2 输出: [1,2] 示例 2: 输入: nums = [1 ...

  8. Git切换分支开发

    入职第一家公司做开发的时候使用的项目版本管理工具是svn,公司内部搭建的服务器:在第二.第三家公司做开发的时候,使用的项目版本管理工具是Git,现在大多数公司使用的也是Git.刚进入公司的时候首先做的 ...

  9. JVM内存溢出与内存泄漏

    内存溢出与内存泄漏 内存溢出相对于内存泄漏来说,尽管更容易被理解,但是同样的,内存溢出也是引发程序崩溃的罪魁祸首之一. 由于GC一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成垃 ...

  10. springboot中图标的定制

    因为我用的版本是org.springframework.boot spring-boot-starter-parent 2.3.3.RELEASE 第一种方法: 配置一个application.yml ...