Golang编程经验
如何选择web框架:
@H_301_9@ 首先Golang语言开发web项目不一定非要框架,本身已经提供了Web开发需要的一切必要技术。当然如果想要ruby里面Rail那种高层次全栈式的MVC框架,@H_301_9@ Golang里面暂时没有,但是不是所有人都喜欢这种复杂的框架。Golang里面一些应用层面的技术需要自己去组装,比如session,cache,log等等. 可选择的web框架有martini, goji等,都是轻量级的。
@H_301_9@
Golang的web项目中的keepalive
关于keepalive,@H_301_9@ 是比较复杂的, 注意以下几点:
-
http1.1@H_301_9@ 默认支持keepalive, 但是不同浏览器对keepalive都有个超时时间, 比如firefox:默认超时时间115秒, 不同浏览器不一样;
-
Nginx默认超时时间75秒;
- golang默认超时时间是无限的,@H_301_9@ 要控制golang中的keepalive可以设置读写超时, 举例如下:
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使用主意事项:
这是使用率极高的一个库,@H_301_9@ 在用它进行事务处理的情况下, 要注意一个问题, 由于它内部使用了连接池, 使用事务的时候如果没有Rollback或者Commit, 这个取出的连接就不会放回到池子里面, 导致的后果就是连接数过多, 所以使用事务的时候要注意正确地使用。
@H_301_9@
github.com/garyburd/redigo/redis使用注意事项:
这也是一个使用率极高的库,@H_301_9@ 同样需要注意,它是支持连接池的, 所以最好使用连接池, 正确的用法是这样的:
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 },} }
另外使用的时候也要把连接放回到池子里面,@H_301_9@ 否则也会导致连接数居高不下。用完之后调用rd.Close(), 这个Close并不是真的关闭连接,而是放回到池子里面。
如何全局捕获panic级别错误:
defer func() { if err := recover(); err != nil { lib.Log4e("Panic error",err) } }()
@H_301_9@ 1. 需要注意的是捕获到pannic之后, 程序的执行点不会回到触发pannic的地方,需要程序再次执行,@H_301_9@ 一些框架支持这一点,比如martini里面有c.Next()。
2.@H_301_9@ 如果程序main里启动了多个goroutine, 每个goroutine里面都应该捕获pannic级别错误, 否则某个goroutine触发panic级别错误之后,整个程序退出, 这是非常不合理的。
@H_301_9@
最容易出错的地方:
使用指针,但是没有判断指针是否为nil,@H_301_9@ Golang中array, struct是值语义, slice,map, chanel是引用传递。
@H_301_9@
如何获取程序执行栈:
@H_301_9@
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)) } }()
具体方法就是调用@H_301_9@ runtime.Stack。
@H_301_9@
如何执行异步任务:
比如用户提交email,@H_301_9@ 给用户发邮件, 发邮件的步骤是比较耗时的, 这个场景适合可以使用异步任务:
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",uniqid) } }()
思路是启动一个goroutine执行异步的操作,@H_301_9@ 当前goroutine继续向下执行。特别需要注意的是新启动的个goroutine如果对全局变量有读写操作的话,需要注意避免发生竞态条件, 可能需要加锁。
@H_301_9@
如何使用定时器:
通常情况下,@H_301_9@ 写一些定时任务需要用到crontab, 在Golang里面是不需要的, 提供了非常好用的定时器。举例如下:
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@H_301_9@ 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.@H_301_9@
官方相关说明:
http://blog.golang.org/race-detector
@H_301_9@
多goroutine执行,访问全局的变量,比如map,可能会发生竞态条件,@H_301_9@ 如何检查呢?首先在编译的时候指定 -race参数,指定这个参数之后,编译出来的程序体积大一倍以上, 另外cpu,内存消耗比较高,适合测试环境, 但是发生竞态条件的时候会panic,有详细的错误信息。go内置的数据结构array,slice,@H_301_9@ map都不是线程安全的。
@H_301_9@
没有设置runtime.GOMAXPROCS会有竞态条件的问题吗?
答案是没有,@H_301_9@ 因为没有设置runtime.GOMAXPROCS的情况下, 所有的goroutine都是在一个原生的系统thread里面执行, 自然不会有竞态条件。
@H_301_9@
如何充分利用cpu多核:
runtime.GOMAXPROCS(runtime.Numcpu()@H_301_9@ * 2)以上是根据经验得出的比较合理的设置。
@H_301_9@
1.@H_301_9@ channel, 但是channel并不能解决所有的情况,channel的底层实现里面也有用到锁, 某些情况下channel还不一定有锁高效, 另外channel是Golang里面最强大也最难掌握的一个东西, 如果发生阻塞不好调试。
2.@H_301_9@ 加锁, 需要注意高并发情况下,锁竞争也是影响性能的一个重要因素, 使用读写锁,在很多情况下更高效, 举例如下:
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) } }
使用锁有个主意的地方是避免死锁,比如循环加锁。@H_301_9@ 3. 原子操作(CAS),Golang的atomic包对原子操作提供支持,Golang里面锁的实现也是用的原子操作。
@H_301_9@
Golang编译出来之后是独立的可执行程序,@H_301_9@ 不过很多时候需要读取配置,由于执行目录有时候不在程序所在目录,路径的问题经常让人头疼,正确获取绝对路径非常重要, 方法如下:
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是一门简洁的语言,@H_301_9@ 不支持函数默认参数. 这个特性有些情况下确实是有用的,如果不支持,往往需要重写函数,或者多写一个函数。其实这个问题非常好解决, 举例如下:
@H_301_9@
@H_301_9@
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] } ... }
性能监控:
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@H_301_9@ tool pprof分析。
如何进行程序调试:
对于调试,每个人理解不一样,@H_301_9@ 如果要调试程序功能, 重新编译即可, Golang的编译速度极快。如果在开发的时候调试程序逻辑, 一般用log即可, Golang里面最好用的log库是log4go, 支持log级别。如果要进行断点调试, GoEclipse之类的是支持的, 依赖Mingw和GDB, 我个人不习惯这种调试方法。
@H_301_9@
守护进程(daemon)
下面给出完整的真正可用的例子:
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) 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开发的工具,@H_301_9@ 用pip安装即可。
@H_301_9@
代码热更新:
代码热更新一直是解释型语言比较擅长的,Golang里面不是做不到,只是稍微麻烦一些,@H_301_9@ 就看必要性有多大。如果是线上在线人数很多, 业务非常重要的场景, 还是有必要, 一般情况下没有必要。
- 更新配置.因为配置文件一般是个json或者ini格式的文件,是不需要编译的,@H_301_9@ 在线更新配置还是相对比较容易的, 思路就是使用信号, 比如SIGUSER2, 程序在信号处理函数中重新加载配置即可。
- 热更新代码.目前网上有多种第三方库, 实现方法大同小异。先编译代码(这一步可以使用fsnotify做到监控代码变化,自动编译),关键是下一步graceful@H_301_9@ restart进程,实现方法可参考:http://grisha.org/blog/2014/06/03/graceful-restart-in-golang/@H_301_9@ 也是创建子进程,杀死父进程的方法。
@H_301_9@
@H_301_9@
条件编译:
条件编译时一个非常有用的特性,一般一个项目编译出一个可执行文件,但是有些情况需要编译成多个可执行文件,执行不同的逻辑,这比通过命令行参数执行不同的逻辑更清晰.比如这样一个场景,一个web项目,是常驻进程的,@H_301_9@ 但是有时候需要执行一些程序步骤初始化数据库,导入数据,执行一个特定的一次性的任务等。假如项目中有一个main.go,里面定义了一个main函数,同目录下有一个task.go函数,里面也定义了一个main函数,正常情况下这是无法编译通过的, 会提示“main redeclared”。解决办法是使用go build 的-tags参数。步骤如下(以windows为例说明):
1.在main.go头部加上//@H_301_9@ +build main
2.@H_301_9@ 在task.go头部加上// +build task
3.@H_301_9@ 编译住程序:go build -tags 'main'
4.@H_301_9@ 编译task:go build -tags 'task' -o task.exe
@H_301_9@
官方说明:
Build@H_301_9@ Constraints
@H_301_9@
A@H_301_9@ 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@H_301_9@ file,preceded only by blank lines and other line comments.
@H_301_9@
To@H_301_9@ distinguish build constraints from package documentation,a series of build constraints must be followed by a blank line.
如果将项目有关资源文件打包进主程序:
使用go@H_301_9@ generate命令,参考godoc的实现。
与C/C++@H_301_9@ 交互
1.@H_301_9@ Cgo,Cgo支持Golang和C/C++混编, 在Golang里面使用pthread,libuv之类的都不难,github上也有相关开源代码;
2.Swig,@H_301_9@ 很多库都用Swig实现了Golang的绑定,Swig也可以反向回调Golang代码。
3.@H_301_9@ syscall包, 该包让你以Golang的方式进行系统编程,不需要再使用C/C++,@H_301_9@ syscall提供了很多系统接口,比如epoll,原始socket套接字编程接口等。
@H_301_9@
其他:
近几年最热门的技术之一Docker是用Golang开发的, 已经有相关的书出版, 对系统运维,云计算感兴趣的可以了解。