IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

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

    bigwhite发表于 2015-01-23 10:51:40
    love 0

    这两周来业余时间都在用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")

    执行结果:





    Hello, tonybai

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

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

    © 2015, bigwhite. 版权所有.



沪ICP备19023445号-2号
友情链接