Golang编程经验总结

benlightning · · 2552 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

如何选择web框架:
首先Golang语言开发web项目不一定非要框架,本身已经提供了Web开发需要的一切必要技术。当然如果想要ruby里面Rail那种高层次全栈式的MVC框架, Golang里面暂时没有,但是不是所有人都喜欢这种复杂的框架。Golang里面一些应用层面的技术需要自己去组装,比如session,cache, log等等. 可选择的web框架有martini, goji等,都是轻量级的。

 

Golang的web项目中的keepalive

关于keepalive, 是比较复杂的, 注意以下几点:

  1. http1.1 默认支持keepalive, 但是不同浏览器对keepalive都有个超时时间, 比如firefox:

    默认超时时间115秒, 不同浏览器不一样;

  2. Nginx默认超时时间75秒;

  3. golang默认超时时间是无限的, 要控制golang中的keepalive可以设置读写超时, 举例如下:

1
2
3
4
5
6
7
8
server := &http.Server{
    Addr:           ":9999",
    Handler:        framework,
    ReadTimeout:    32 * time.Second,
    WriteTimeout:   32 * time.Second,
    MaxHeaderBytes: 1 << 20,
}
server.ListenAndServe()




github.com/go-sql-driver/mysql使用主意事项:

这是使用率极高的一个库, 在用它进行事务处理的情况下, 要注意一个问题, 由于它内部使用了连接池, 使用事务的时候如果没有Rollback或者Commit, 这个取出的连接就不会放回到池子里面, 导致的后果就是连接数过多, 所以使用事务的时候要注意正确地使用。

 

github.com/garyburd/redigo/redis使用注意事项:

这也是一个使用率极高的库, 同样需要注意,它是支持连接池的, 所以最好使用连接池, 正确的用法是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func initRedis(host string) *redis.Pool {
    return &redis.Pool{
        MaxIdle: 64,   
        IdleTimeout: 60 * time.Second,
        TestOnBorrow: func(c redis.Conn, t time.Time) error {
            _, err := c.Do("PING")
 
            return err
        },
        Dial: func() (redis.Conn, error) {
            c, err := redis.Dial("tcp", host)
            if err != nil {
                return nil, err
            }
 
            _, err = c.Do("SELECT", config.RedisDb)
 
            return c, err
        },
    }
}



另外使用的时候也要把连接放回到池子里面, 否则也会导致连接数居高不下。用完之后调用rd.Close(), 这个Close并不是真的关闭连接,而是放回到池子里面。



如何全局捕获panic级别错误:

 

1
2
3
4
5
defer func() {
    if err := recover(); err != nil {
        lib.Log4e("Panic error", err)
    }
}()


1. 需要注意的是捕获到pannic之后, 程序的执行点不会回到触发pannic的地方,需要程序再次执行, 一些框架支持这一点,比如martini里面有c.Next()。

 

2. 如果程序main里启动了多个goroutine, 每个goroutine里面都应该捕获pannic级别错误, 否则某个goroutine触发panic级别错误之后,整个程序退出, 这是非常不合理的。

 

最容易出错的地方:

使用指针,但是没有判断指针是否为nil, Golang中array, struct是值语义, slice,map, chanel是引用传递。

 

如何获取程序执行栈:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
defer func() {
    if err := recover(); err != nil {
        var st = func(all bool) string {
            // Reserve 1K buffer at first
            buf := make([]byte, 512)
 
            for {
                size := runtime.Stack(buf, all)
                // The size of the buffer may be not enough to hold the stacktrace,
                // so double the buffer size
                if size == len(buf) {
                    buf = make([]byte, len(buf)<<1)
                    continue
                }
                break
            }
 
            return string(buf)
        }
        lib.Log4e("panic:" + toString(err) + "\nstack:" + st(false))
    }
}()



具体方法就是调用 runtime.Stack。

 

如何执行异步任务:

比如用户提交email, 给用户发邮件, 发邮件的步骤是比较耗时的, 这个场景适合可以使用异步任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
result := global.ResponseResult{ErrorCode: 0, ErrorMsg: "GetInviteCode success!"}
render.JSON(200, &result)
go func() {
    type data struct {
        Url string
    }
    name := "beta_test"
    subject := "We would like to invite you to the private beta of Screenshot."
    url := config.HttpProto + r.Host + "/user/register/" + *uniqid
    html := ParseMailTpl(&name, &beta_test_mail_content, data{url})
    e := this.SendMail(mail, subject, html.String())
    if e != nil {
        lib.Log4w("GetInviteCode, SendMail faild", mail, uniqid, e)
    else {
        lib.Log4w("GetInviteCode, SendMail success", mail, uniqid)
    }
}()



思路是启动一个goroutine执行异步的操作, 当前goroutine继续向下执行。特别需要注意的是新启动的个goroutine如果对全局变量有读写操作的话,需要注意避免发生竞态条件, 可能需要加锁。

 

如何使用定时器:

通常情况下, 写一些定时任务需要用到crontab, 在Golang里面是不需要的, 提供了非常好用的定时器。举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func Init() {
    ticker := time.NewTicker(30 * time.Minute)
    for {
        select {
        case c := <-global.TaskCmdChannel:
            switch *c {
            case "a":
                //todo
            }
        case c := <-global.TaskImageMessageChannel:
            m := new(model.TaskModel)
            m.Init()
            m.CreateImageMessage(c)
            m = nil
        case <-ticker.C:
            m := new(model.TaskModel)
            m.Init()
            m.CleanUserExpiredSessionKey()
            m = nil
        }
    }
}



多goroutine执行如果避免发生竞态条件:

Data races are among the most common and hardest to debug types of bugs in concurrent systems. A data race occurs when two goroutines access the same variable concurrently and at least one of the accesses is a write. See the The Go Memory Model for details.

官方相关说明:

http://blog.golang.org/race-detector

http://golang.org/ref/mem

 

多goroutine执行,访问全局的变量,比如map,可能会发生竞态条件, 如何检查呢?首先在编译的时候指定 -race参数,指定这个参数之后,编译出来的程序体积大一倍以上, 另外cpu,内存消耗比较高,适合测试环境, 但是发生竞态条件的时候会panic,有详细的错误信息。go内置的数据结构array,slice, map都不是线程安全的。

 

没有设置runtime.GOMAXPROCS会有竞态条件的问题吗?

答案是没有, 因为没有设置runtime.GOMAXPROCS的情况下, 所有的goroutine都是在一个原生的系统thread里面执行, 自然不会有竞态条件。

 

如何充分利用CPU多核:

runtime.GOMAXPROCS(runtime.NumCPU() * 2)
以上是根据经验得出的比较合理的设置。

 

解决并发情况下的竞态条件的方法:

1.  channel, 但是channel并不能解决所有的情况,channel的底层实现里面也有用到锁, 某些情况下channel还不一定有锁高效, 另外channel是Golang里面最强大也最难掌握的一个东西, 如果发生阻塞不好调试。

2. 加锁, 需要注意高并发情况下,锁竞争也是影响性能的一个重要因素, 使用读写锁,在很多情况下更高效, 举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var mu sync.RWMutex
 
    
 
 
 
    mu.RLock()
    defer mu.RUnlock()
    conns := h.all_connections[img_id]
 
    for _, c := range conns {
        if c == nil /*|| c.uid == uid */ {
            continue
        }
 
        select {
        case c.send <- []byte(message):
        default:
            h.conn_unregister(c)
        }
    }



使用锁有个主意的地方是避免死锁,比如循环加锁。 
3. 原子操作(CAS), Golang的atomic包对原子操作提供支持,Golang里面锁的实现也是用的原子操作。

 

获取程序绝对路径:

   Golang编译出来之后是独立的可执行程序, 不过很多时候需要读取配置,由于执行目录有时候不在程序所在目录,路径的问题经常让人头疼,正确获取绝对路径非常重要, 方法如下:

1
2
3
4
5
6
7
func GetCurrPath() string {
    file, _ := exec.LookPath(os.Args[0])
    path, _ := filepath.Abs(file)
    index := strings.LastIndex(path, string(os.PathSeparator))
    ret := path[:index]
    return ret
}




Golang函数默认参数:

   大家都知道Golang是一门简洁的语言, 不支持函数默认参数. 这个特性有些情况下确实是有用的,如果不支持,往往需要重写函数,或者多写一个函数。其实这个问题非常好解决, 举例如下:

1
2
3
4
5
6
7
8
9
10
func (this *ImageModel) GetImageListCount(project_id int64,  paramter_optional ...int) int {
    var t int
 
    expire_time := 600
    if len(paramter_optional) > 0 {
        expire_time = paramter_optional[0]
    }
 
    ...
}




性能监控:

1
2
3
4
5
6
7
8
9
10
11
go func() {
    profServeMux := http.NewServeMux()
    profServeMux.HandleFunc("/debug/pprof/", pprof.Index)
    profServeMux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
    profServeMux.HandleFunc("/debug/pprof/profile", pprof.Profile)
    profServeMux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
    err := http.ListenAndServe(":7789", profServeMux)
    if err != nil {
        panic(err)
    }
}()



接下来就可以使用go tool pprof分析。



如何进行程序调试:

   对于调试,每个人理解不一样, 如果要调试程序功能, 重新编译即可, Golang的编译速度极快。如果在开发的时候调试程序逻辑, 一般用log即可, Golang里面最好用的log库是log4go, 支持log级别。如果要进行断点调试, GoEclipse之类的是支持的, 依赖Mingw和GDB, 我个人不习惯这种调试方法。

 

守护进程(daemon)

下面给出完整的真正可用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package main
  
import (
    "fmt"
    "log"
    "os"
    "runtime"
    "syscall"
    "time"
)
  
func daemon(nochdir, noclose int) int {
    var ret, ret2 uintptr
    var err syscall.Errno
  
    darwin := runtime.GOOS == "darwin"
  
    // already a daemon
    if syscall.Getppid() == 1 {
        return 0
    }
  
    // fork off the parent process
    ret, ret2, err = syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)
    if err != 0 {
        return -1
    }
  
    // failure
    if ret2 < 0 {
        os.Exit(-1)
    }
  
    // handle exception for darwin
    if darwin && ret2 == 1 {
        ret = 0
    }
  
    // if we got a good PID, then we call exit the parent process.
    if ret > 0 {
        os.Exit(0)
    }
  
    /* Change the file mode mask */
    _ = syscall.Umask(0)
  
    // create a new SID for the child process
    s_ret, s_errno := syscall.Setsid()
    if s_errno != nil {
        log.Printf("Error: syscall.Setsid errno: %d", s_errno)
    }
    if s_ret < 0 {
        return -1
    }
  
    if nochdir == 0 {
        os.Chdir("/")
    }
  
    if noclose == 0 {
        f, e := os.OpenFile("/dev/null", os.O_RDWR, 0)
        if e == nil {
            fd := f.Fd()
            syscall.Dup2(int(fd), int(os.Stdin.Fd()))
            syscall.Dup2(int(fd), int(os.Stdout.Fd()))
            syscall.Dup2(int(fd), int(os.Stderr.Fd()))
        }
    }
  
    return 0
}
  
func main() {
    daemon(0, 1)
    for {
        fmt.Println("hello")
        time.Sleep(1 * time.Second)
    }
  
}




进程管理:

个人比较喜欢用supervisord来进行进程管理,支持进程自动重启,supervisord是一个python开发的工具, 用pip安装即可。

 

代码热更新:

代码热更新一直是解释型语言比较擅长的,Golang里面不是做不到,只是稍微麻烦一些, 就看必要性有多大。如果是线上在线人数很多, 业务非常重要的场景, 还是有必要, 一般情况下没有必要。

  1. 更新配置.
    因为配置文件一般是个json或者ini格式的文件,是不需要编译的, 在线更新配置还是相对比较容易的, 思路就是使用信号, 比如SIGUSER2, 程序在信号处理函数中重新加载配置即可。

  2. 热更新代码.
    目前网上有多种第三方库, 实现方法大同小异。先编译代码(这一步可以使用fsnotify做到监控代码变化,自动编译),关键是下一步graceful restart进程,实现方法可参考:http://grisha.org/blog/2014/06/03/graceful-restart-in-golang/   也是创建子进程,杀死父进程的方法。

条件编译:

条件编译时一个非常有用的特性,一般一个项目编译出一个可执行文件,但是有些情况需要编译成多个可执行文件,执行不同的逻辑,这比通过命令行参数执行不同的逻辑更清晰.比如这样一个场景,一个web项目,是常驻进程的, 但是有时候需要执行一些程序步骤初始化数据库,导入数据,执行一个特定的一次性的任务等。假如项目中有一个main.go, 里面定义了一个main函数,同目录下有一个task.go函数,里面也定义了一个main函数,正常情况下这是无法编译通过的, 会提示“main redeclared”。解决办法是使用go build 的-tags参数。步骤如下(以windows为例说明):

1.在main.go头部加上// +build main

2. 在task.go头部加上// +build task

3. 编译住程序:go build -tags 'main'

4. 编译task:go build -tags 'task' -o task.exe

 

官方说明:

Build Constraints

 

A build constraint is a line comment beginning with the directive  +build that lists the conditions under which a file should be included in the package. Constraints may appear in any kind of source file (not just Go), but they must appear near the top of the file, preceded only by blank lines and other line comments.

 

To distinguish build constraints from package documentation, a series of build constraints must be followed by a blank line.



如果将项目有关资源文件打包进主程序:

使用go generate命令,参考godoc的实现。



与C/C++ 交互

1. Cgo,Cgo支持Golang和C/C++混编, 在Golang里面使用pthread,libuv之类的都不难,github上也有相关开源代码;

2.Swig, 很多库都用Swig实现了Golang的绑定,Swig也可以反向回调Golang代码。

3. syscall包, 该包让你以Golang的方式进行系统编程,不需要再使用C/C++, syscall提供了很多系统接口,比如epoll,原始socket套接字编程接口等。

 

其他:

    近几年最热门的技术之一Docker是用Golang开发的, 已经有相关的书出版, 对系统运维,云计算感兴趣的可以了解。

来自:http://blog.csdn.net/yxw2014/article/details/43451625


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

本文来自:博客园

感谢作者:benlightning

查看原文:Golang编程经验总结

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

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