Golang编程经验



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@ 是比较复杂的, 注意以下几点:

  1. http1.1@H_301_9@ 默认支持keepalive, 但是不同浏览器对keepalive都有个超时时间, 比如firefox:默认超时时间115秒, 不同浏览器不一样;

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

  3. 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()

@H_301_9@ @H_301_9@

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@ @H_301_9@

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

@H_301_9@ @H_301_9@

如何全局捕获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@ @H_301_9@

具体方法就是调用@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)
			}
		}()

@H_301_9@ @H_301_9@

思路是启动一个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
		}
	}
}

@H_301_9@ @H_301_9@

多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

http://golang.org/ref/mem

@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@ @H_301_9@

使用锁有个主意的地方是避免死锁,比如循环加锁。@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
}

@H_301_9@ @H_301_9@ @H_301_9@

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]
	}

	...
}

@H_301_9@ @H_301_9@ @H_301_9@

性能监控:

		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)
			}
		}()

@H_301_9@ @H_301_9@

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

@H_301_9@ @H_301_9@

如何进行程序调试:

对于调试,每个人理解不一样,@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)
    }
 
}

@H_301_9@ @H_301_9@ @H_301_9@

进程管理:

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

@H_301_9@

代码热更新:

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

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

  2. 热更新代码.目前网上有多种第三方库, 实现方法大同小异。先编译代码(这一步可以使用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.

@H_301_9@ @H_301_9@

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

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

@H_301_9@ @H_301_9@

与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开发的, 已经有相关的书出版, 对系统运维,云计算感兴趣的可以了解。

相关文章

程序目录结构 简单实现,用户登录后返回一个jwt的token,下次请求带上token请求用户信息接口并返回信息...
本篇博客的主要内容是用go写一个简单的Proof-of-Work共识机制,不涉及到网络通信环节,只是一个本地的简...
简介 默克尔树(MerkleTree)是一种典型的二叉树结构,其主要特点为: 最下面的叶节点包含存储数据或其...
接下来学习并发编程, 并发编程是go语言最有特色的地方, go对并发编程是原生支持. goroutine是go中最近本...
先普及一下, 什么是广度优先搜索 广度优先搜索类似于树的层次遍历。从图中的某一顶点出发,遍历每一个顶...
第一天: 接口的定义和实现 第二天: 一. go语言是面向接口编程. 在学习继承的时候说过, go语言只有封装,...