标签 Web 下的文章

近期遇到的3个Golang代码问题

这两周来业余时间都在用Golang写代码,现在处于这样一个状态:除了脚本,就是Golang了。反正能用golang实现的,都用golang写。

Golang语言相对成熟了,但真正写起来,还是要注意一些“坑”的,下面是这周遇到的三个问题,这里分享出来,希望能对遇到同样问题的童鞋有所帮助。

一、误用定时器,狂占CPU

golang中有一个通过channel实现timeout或tick timer的非常idiomatic的方法,代码如下:

func worker(start chan bool) {
        for {
                timeout := time.After(30 * time.Second)
                 select {
                         // … do some stuff
                         case <- timeout:
                                 return
                 }
        }
}

func worker(start chan bool) {
        for {
                heartbeat := time.Tick(30 * time.Second)
                 select {
                         // … do some stuff
                         case <- heartbeat:
                                 return
                 }
        }
}

没错,就像上面这两个例子,如果你单独执行它们,你不会发现任何问题,但是当你将这样的代码放到一个7 * 24小时的Service中,并且timeout间隔或heartbeat间隔为更短时间,比如1s时,问题就出现了。

我的程序最初就是用上面的代码实现了一个timewheel,通过放置在一个单独goroutine中的定时器检测timewheel是否有到期的 timer。程序跑在后台运行的很好,直到有一天晚上我无意中执行了一下top,我发现这个service居然站用了40%多的CPU负荷。最初我怀疑是 不是代码中有死循环,但仔细巡查一遍代码后,没有发现死循环的痕迹,算法逻辑也没问题。

于是重启了一下这个service,发现cpu占用降了下来。出去去了趟卫生间,回来继续用top观察,不好,这个service占用了1%的CPU,再 过一会升到2%,观察一段时间后,发现这个service对cpu的占用率随着时间的推移而增加。gdb attach了相应的进程号,stack多是go runtime的调度。

再次回到代码,发现可能存在问题的只有这里的tick。我的tick间隔是1s。这样每1s都会创建一个runtime timer,而通过runtime的源码来看,这些timer都扔给了runtime调度(一个heap)。时间长了,就会有超多的timer需要 runtime调度,不耗CPU才怪。

于是做了如下修改:

func worker(start chan bool) {
        heartbeat := time.Tick(1 * time.Second)
         for {
               
                 select {
                         // … do some stuff
                         case <- heartbeat:
                                 return
                 }
        }
}

重新编译执行service,观察了一天,cpu再也没有升高过。

二、小心list.List的Delete逻辑

其实这是一个在哪种语言中都会遇到的初级问题,这里只是给大家提个醒罢了。不多说了,上代码:

从一个list.List中删除一个element,一般逻辑是:

l := list.New()
… …
for e := l.Front(); e != nil; e = e.Next() {
        if e.Value.(int) == someValue {
                l.Remove(e)
                return or break
        }
}

但是如果list里有重复元素,且代码要遍历整个list删除某个值为somevalue的元素呢?上面的一般方法是由逻辑缺陷的,例子:

func foo(i int) {
        l := list.New()
        for i := 0; i < 9; i++ {
                l.PushBack(i)
        }
        l.PushBack(6)

        for e := l.Front(); e != nil; e = e.Next() {
                fmt.Print(e.Value.(int))
        }

        for e := l.Front(); e != nil; e = e.Next() {
                if e.Value.(int) == i {
                        l.Remove(e)
                }
        }

        fmt.Printf("\n")
        for e := l.Front(); e != nil; e = e.Next() {
                fmt.Print(e.Value.(int))
        }
        fmt.Printf("\n")
}

func main() {
        foo(6)
}

该程序试图删除list中的所有值为6的element,但执行结果却是:

go run testlist.go
0123456786
012345786

list中尾部的那个6没有被删除,程序似乎在删除完第一个6之后就不再继续循环了。事实也是这样:

当l.Remove(e)执行后,e.Next()被置为了nil,这样循环条件不再满足,循环终止。

为此,对于这样的程序,下面的方法才是正确的:

func main() {
        bar(6)
}

func bar(i int) {
        l := list.New()
        for i := 0; i < 9; i++ {
                l.PushBack(i)
        }
        l.PushBack(6)

        for e := l.Front(); e != nil; e = e.Next() {
                fmt.Print(e.Value.(int))
        }

        var next *list.Element
        for e := l.Front(); e != nil; {
                if e.Value.(int) == i {
                        next = e.Next()
                        l.Remove(e)
                        e = next
                } else {
                        e = e.Next()
                }
        }

        fmt.Printf("\n")
        for e := l.Front(); e != nil; e = e.Next() {
                fmt.Print(e.Value.(int))
        }
        fmt.Printf("\n")
}

执行结果:
$ go run testlist.go
0123456786
01234578

三、要给template起个正确的名字

编写一个Web程序,需要用到html/template。

… …
t := template.New("My Reporter")
t, err = t.ParseFiles("views/report.html")
if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
}

t.Execute(w, UserInfo{xx: XX})

结果一执行却crash了:

[martini] PANIC: runtime error: invalid memory address or nil pointer dereference
/usr/local/go/src/runtime/panic.go:387 (0×16418)
/usr/local/go/src/runtime/panic.go:42 (0x1573e)
/usr/local/go/src/runtime/sigpanic_unix.go:26 (0x1bb50)
/usr/local/go/src/html/template/template.go:59 (0x7ed64)
/usr/local/go/src/html/template/template.go:75 (0x7ef0d)
/Users/tony/Test/GoToolsProjects/src/git.oschina.net/bigwhite/web/app.go:104 (0x2db0)
    reportHandler: t.Execute(w, UserInfo{xx: XXX})

问题在t.Execute这行,单独把template代码摘出来放在一个测试代码中:

//testtmpl.go
type UserInfo struct {
        Name string
}

func main() {
        t := template.New("My Reporter")
        t, err := t.ParseFiles("views/report.html")
        if err != nil {
                fmt.Println("parse error")
                return
        }

        err = t.Execute(os.Stdout, UserInfo{Name: "tonybai"})
        if err != nil {
                fmt.Println("exec error", err)
        }
        return
}

执行结果:
go run testtmpl.go
exec error template: My Reporter: "My Reporter" is an incomplete or empty template; defined templates are: "report.html"

看起来似乎template对象与模板名字对不上导致的错误啊。修改一下:

t := template.New("report.html")

执行结果:

<html>
<head>
</head>
<body>
    Hello, tonybai
</body>
</html>

这回对了,看来template的名字在与ParseFiles一起使用时不是随意取的,务必要与模板文件名字相同。

ParseFiles支持解析多个文件,如果是传入多个文件该咋办?godoc说了,template名字与第一个文件名相同即可。

勇于面对

刚刚过去的这一周搞得我十分疲惫,起因是岳母生病了。

果果自出生以来一直是岳母照顾,这个五一岳母将果果带回老家待了一周,也许是太过操劳导致旧病复发(腰椎肩盘轻微突出),无法坚持照顾果果了。可这段时间 又恰逢我和我LP都很忙碌,但无奈身边没有亲戚,只能我请假待果果(LP那里集团领导检查,实在无法脱身),还要照顾生病的岳母。本以为病两三天就能好 转,但观察两天后仍不见好转,于是我只能将母亲大人请来照顾果果,好抽身上班。万没想到,我母亲刚来一天多,居然也生病了,估计是上火所致(母亲大人十分 易上火,尤其是出远门)。于是乎又将母亲送回家里,这一顿折腾啊,转眼间5天过去了,终于盼到了周末,老婆也休息了,疲惫的我也可以缓缓了。

躺在沙发上,闭目反思:像我和LP这样大学毕业后留在大城市的人有很多,父母亲属均不在身边,总会遇到各种困难,有些时候特别难,怎么办?只能勇于面对,依靠自己,不要抱怨,也不要退缩。

这段时间一直没有更新博客,都长草了,今天顺便借这里嘀咕嘀咕这段时间发生的一些事情。

1、《七周七语言》正式出版

大上周收到了《七周七语言》的样书(3本),甚是欣喜,不过样书没够分;又厚脸皮向出版社要了三本,还好出版社比较厚道,上周又寄来了三本,瞬间分光。 LP看到我翻译的样书,以一种甚为崇拜的眼光注视着我,让我很是不适应,心里想:这真的不算什么;下一个目标:自己写一本书^_^,还不知猴年马月可以实 现呢。

2、榜样

不知道是因为看了我的博客,还是无意中看到了网上的《七周七语言》译者中有了我的名字,几位同事都私下里向我表达了羡慕之意,更有同事借此机会表达了一直 以来都视我为榜样的心声。但我的确配不上“榜样”这个词,我觉得本人只是一个有目标、常实践、善于挤时间、有忍耐、爱思考、爱总结、养成了一些还不错的习 惯的普通人罢了,这些很多人都能做到,很多人也都是这样做的,比我做的好的多了去了。另外一个不得不承认的事实是:随着年龄的增长,家庭、工作压力都增加 好多,精力有限,要想保持原先的状态,甚至优于以前的状态,真得挺难的。

3、Web开发

以前一直在系统底层探索,对上层有些不屑。但最近了解和学习了一些Web开发方面的知识,感觉还不错。接触Web开发,这缘于之前有一些想法,想通过自己 并不熟悉的Web方式实现。至少目前对htmlcss的原理有了初步认识;开发采用了传统的LAMP;另外对于我这样的 beginner,Twitter开源的bootstrap是搭建Web UI的极好起点,上周已经完成了一稿UI Demo,自己感觉还不错;以前一直是做开源,用Google Code管理repository;这次是Private project,Github对Private repository是收费的,于是我用的是bitbucket,感觉也不错,也算是正式用上了git这个工具了。

4、PR值升1

上周发现我的这个博客的Google PR值由0升为1了,可能意味着更多朋友可以search到我的这个站点中的文章了。

5、 同学小聚

昨天一位高中同学结婚,我抽空也参加了婚礼。饭桌上和几位高中同学(还有远道从帝都赶来的)小聚了一下。有几点感悟:
  * 即使是同城,也是聚少离多。大家都处于事业和家庭双重高压力区间,时间精力十分有限啊;
  * 同学感情非同一般。以前在念书时,老师就说过同学的感情那不是同事和其他可比的,当时没有感觉。但工作后每次小聚都能深刻体会到这一点;
  * 我们这批80后大多都已结婚生子(饭桌上的一位同学刚当爸爸还不到5天,可喜可贺啊)。见面的话题已经逐渐向下一代的培养转移,甚至还谈到了生二胎,不知道是不是我们的心态已经变老了^_^。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats