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:
-
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.
官方相關說明:
多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開發的, 已經有相關的書出版, 對系統運維,雲端計算感興趣的可以瞭解。