Golang編程經驗總結
如何選擇web框架:
首先Golang語言開發web項目不一定非要框架,本身已經提供了Web開發需要的一切必要技術。當然如果想要ruby里面Rail那種高層次全棧式的MVC框架, Golang里面暫時沒有,但是不是所有人都喜歡這種復雜的框架。Golang里面一些應用層面的技術需要自己去組裝,比如session,cache, log等等. 可選擇的web框架有martini, goji等,都是輕量級的。
Golang的web項目中的keepalive
關于keepalive, 是比較復雜的, 注意以下幾點:
-
http1.1 默認支持keepalive, 但是不同瀏覽器對keepalive都有個超時時間, 比如firefox:
默認超時時間115秒, 不同瀏覽器不一樣; -
Nginx默認超時時間75秒;
-
golang默認超時時間是無限的, 要控制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使用主意事項:
這是使用率極高的一個庫, 在用它進行事務處理的情況下, 要注意一個問題, 由于它內部使用了連接池, 使用事務的時候如果沒有Rollback或者Commit, 這個取出的連接就不會放回到池子里面, 導致的后果就是連接數過多, 所以使用事務的時候要注意正確地使用。
github.com/garyburd/redigo/redis使用注意事項:
這也是一個使用率極高的庫, 同樣需要注意,它是支持連接池的, 所以最好使用連接池, 正確的用法是這樣的:
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級別錯誤:
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是引用傳遞。
如何獲取程序執行棧:
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, 給用戶發郵件, 發郵件的步驟是比較耗時的, 這個場景適合可以使用異步任務:
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里面是不需要的, 提供了非常好用的定時器。舉例如下:
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
多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. 加鎖, 需要注意高并發情況下,鎖競爭也是影響性能的一個重要因素, 使用讀寫鎖,在很多情況下更高效, 舉例如下:
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編譯出來之后是獨立的可執行程序, 不過很多時候需要讀取配置,由于執行目錄有時候不在程序所在目錄,路徑的問題經常讓人頭疼,正確獲取絕對路徑非常重要, 方法如下:
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是一門簡潔的語言, 不支持函數默認參數. 這個特性有些情況下確實是有用的,如果不支持,往往需要重寫函數,或者多寫一個函數。其實這個問題非常好解決, 舉例如下:
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 tool pprof分析。
如何進行程序調試:
對于調試,每個人理解不一樣, 如果要調試程序功能, 重新編譯即可, Golang的編譯速度極快。如果在開發的時候調試程序邏輯, 一般用log即可, Golang里面最好用的log庫是log4go, 支持log級別。如果要進行斷點調試, GoEclipse之類的是支持的, 依賴Mingw和GDB, 我個人不習慣這種調試方法。
守護進程(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, 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里面不是做不到,只是稍微麻煩一些, 就看必要性有多大。如果是線上在線人數很多, 業務非常重要的場景, 還是有必要, 一般情況下沒有必要。
-
更新配置.
因為配置文件一般是個json或者ini格式的文件,是不需要編譯的, 在線更新配置還是相對比較容易的, 思路就是使用信號, 比如SIGUSER2, 程序在信號處理函數中重新加載配置即可。 -
熱更新代碼.
目前網上有多種第三方庫, 實現方法大同小異。先編譯代碼(這一步可以使用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