go项目的一些心得

作者: 董泽润 | 来源:发表于2016-03-16 20:44 被阅读6656次
spacex

最近小伙伴们刚完成广告系统,第二个直接服务于业务的项目。踩了一些坑,更收获了不少知识。总结出来与大家分享,没什么高大尚技术,都是周边的小技巧,加深对 go 语言的理解,适合新手,老鸟勿喷。

包管理

很多人都认为 go 的包管理不够友好,深有感觉。特别是在 github 上给别人提 patch, 我先 fork 到自已目录下面,如果原作者有引用自已路径下面的库,这就麻烦了。

另外一个是版本管理,每个人的 gopath 下面同样的库可能有不同版本,官方提供了一个 Godep 来控制版本,我看很多开源项目也在用。但是如果想管理除 go 以外的依赖呢?

我们使用相对路径的方式,将引用到的库集中放到 submodule 中,如下图:

包管理

我司将所有语言第三方库都放到 tinder 里面,包括多个组之间共用的 thrfit IDL 文件。Go 第三方库都放到 golang/lib 目录下面,共用的内部库放到 golang/src/common 下面,每次 install 编译程序时将 gopath 指定到当前项目下的相对目录。

更新20160629:现在依赖使用govender,第三方的IDL使用submodule

超时控制与请求跟踪

由于业务对时延要求高,给我们定 50ms 超时时间。大家肯定会想到用 Channel 和 timer 来做控制,但是我们还想跟踪请求在内部的一系列操作,否则 Debug 日志一大堆,无法定位。

此时想到了 golang.org/x/net/context 库,官方文档很详细,用于跨 API 调用很方便,vitess 中大量使用这个库。

// A Context carries a deadline, a cancelation signal, and other values across

// API boundaries.

// Context's methods may be called by multiple goroutines simultaneously.

type Context interface {

// Deadline returns the time when work done on behalf of this context

// should be canceled.  Deadline returns ok==false when no deadline is

// set.  Successive calls to Deadline return the same results.

Deadline() (deadline time.Time, ok bool)

// Done returns a channel that's closed when work done on behalf of this

// context should be canceled.  Done may return nil if this context can

// never be canceled.  Successive calls to Done return the same value.

// See http://blog.golang.org/pipelines for more examples of how to use

// a Done channel for cancelation.

Done() <-chan struct{}

// Value returns the value associated with this context for key, or nil

// if no value is associated with key.  Successive calls to Value with

// the same key returns the same result.

// Use context values only for request-scoped data that transits

// processes and API boundaries, not for passing optional parameters to

// functions.

// Packages that define a Context key should provide type-safe accessors

// // userKey is the key for user.User values in Contexts.  It is

// // unexported; clients use user.NewContext and user.FromContext

// // instead of using this key directly.

Value(key interface{}) interface{}

}

一个请求过来,每一次流转都要携带 context.Context, 并且首先检测是否超时,如果超时或是被取消,那么直接返回。另外 context.Context 会携带每次请求 ID,这是由业务传过来的字段,如果为空,内部会生成一个 uuid 来标识。

最终执行的代码逻辑

超时参数由业务传过来,根据 timeout 生成 context.Context,最终函数要么由 ctx.Done 超时返回,要么从 rr channel 中获取业务结果返回。 真实业务请求会开启一个匿名 goroutine, 传入的 context.Context 携带了 logid, 内部打日志都会先打印 logid.

cancel signal

在每个耗时请求(redis/mysql)的入口,都会先检测是否超时。

goroutine和panic

这块学艺不精,不像 actor 有父子关系,函数派生出来的 goroutine 如果panice 会挂掉整个程序,比如如下代码:

错误示例

最开始程序如上图,原以为会捕获到 do_something 产生的 panic, 还是太年轻啊。要将 recover 放置在 go func 入口。

缓存脏数据

我们会在 redis 缓存用户信息,过期时间 6 小时,如果没有再 fallback 到数据库,另外还有一个程序内置 lru cache. 

程序升级后,发现测试逻辑不对,uid 始终为0,fix 这个问题后,缓存这时就出现了脏数据。这时有两个办法,选择了第2个。

1. 使用 redis-port 批量清除无效缓存

2. 再次更新程序,内部修订错误数据

php thrift 超时问题

这个问题蛮头痛,网上也有人遇到过 thrift中的超时(timeout)坑。底层有三个超时时间 connect, send 和 recv,最初都设置的 100ms,线上每天大量超时报错,后来我们将 recv timeout 调到 1000ms 线上就安静了。

另外两个 connect, send 仍然是 100ms,我们更倾向于底层驱动的超时时间稍长一些,由业务层来控制超时 ( context 库)。

对象池

对象池是不同于连接池,两个概念的东西。连接池特指 redis/mysql 的长连接,常驻内存。而对象池是内部实例,使用对象池可以减少程序 GC 压力。目前常用的有两种  sync.Pool 和 channel 模拟的对象池。官方有对 sync.Pool 的详细说明,对象会在两个 GC 之间被回收释放,而 channel 则会常驻。

对象池

代码很简单也易懂,Get 时 channel 有数据就返回,没有直接 New。至于 channel 缓冲大小,要根据业务压力来定。

内部服务注册

在全局 map 注册服务,这也算是 go 程序标配了,最出名的就是官方 database 库注册 mysql driver 的代码

服务注册

实现在 driver.Driver 接口的服务,直接注册进来即可,使用时直接根据 name 找到 driver。

ServerOnRun

服务内部模块有大量初始化的需求,对于全局变量等直接扔到 init() 函数里即可,但是对于依赖外部服务 (mysql/redis/servervice),在程序启动时连接句柄都不存在,就不能扔到 init() 里。

一种做法就是在各个模块里写 init_xx()等方法,然后在 main() 启动初始化外部配置后,去调用,不过这样在 main 里维护就很麻烦。

所以要在全局定义 ServerOnRun, 每个无法由 init() 完成的初始化都在这里进行注册,最后由 main 遍历 ServerOnRun 来执行即可。

thrift字段变更问题

业务升级改动,时常会加字段,并且为了兼容现有代码,必须设为 optional。另外有时还遇到要将字段类型由 int 换成 string 的问题,比较麻烦,前期还是要设计好

json序列化

程序内部有大量的json序列化需求,官方的稍慢,采用比较流行的 ffjons 

相关文章

  • go项目的一些心得

    最近小伙伴们刚完成广告系统,第二个直接服务于业务的项目。踩了一些坑,更收获了不少知识。总结出来与大家分享,没什么高...

  • 应用编程基础课第五讲:Go项目实践

    今天我给大家介绍下我使用Go语言做项目的一些心得,实际项目由于涉及公司代码,所以我这里开发了一个通用的模版项目go...

  • go 基础

    安装和环境配置 自行百度解决 go项目的目录结构 go命令依赖一个重要的环境变量:$GOPATH一般的,一个Go项...

  • go语言学习-从基础到实战到源码分析

    收集的一些go语言学习资料,有go基础学习系列,go项目实战,go进阶-go源码分析,还有go的一些书籍,go的架...

  • 产品项目管理

    移动端产品项目管理一些心得,项目管理要根据实际情况调整,本文只是根据当前的工作环境的一些管理心得,希望给大家一点项...

  • De novo 基因组GO 注释小窗口

    关于GO 注释的心得体会 目前对于GO功能注释的思路有 以下常见的三种: 1、BLAST+InterProScan...

  • 设置后台守护进程运行beego应用

    先贴上项目的路径:/alidata/www/go_workspace/src/go_zhanj163/ 运行命令:...

  • R | GO富集结果描述太长怎么办?

    问题描述 近期在使用clusterProfiler的GO/KEGG富集结果进行绘图时,注意到一些条目的描述过于长,...

  • 做科研项目的一些心得

    面对一个任务 1,首先,心态上不要发怵,要把挑战当作是一个成长的过程。 2,当然是首先想办法将一个大的问题分解成一...

  • Go Environment

    GOPATH andPATH go项目「hello」的路径为 所有go项目的根路径都一般是「go」,其中必须有「b...

网友评论

  • 990a033cf40b:怎么会捕获不到panic呢
    董泽润:recover只能捕获当前goroutine的panic
  • qlchan:董总不是搞数据库么?怎么搞go了
    董泽润:@qlchan 换换方向,多接触下工程性的东西
  • gonil:我Mysql redis的句柄都是在init初始化的,奇怪你怎么用的ServerOnRun
    董泽润:@gonil 因为MySQL Redis 这类初始化要等 main 解析完 config 配置后才可以,所以不能放在init 里面,要单独初始化。但是必须注册到全局唯一的map里,要不main里去写死不方便扩展,看起来也乱。下面就是一个代码示例:
    func init() {
    defined.RegisterOnRun("cache", func() {
    common.Info("Server On Run cache.InitCache")
    InitCache()
    })
    }

    func InitCache() {
    if GlobalRedisCache == nil {
    GlobalRedisCache = NewRedisWrap(config.GlobalConfig.CacheConfig)
    }

    if GlobalShowCache == nil {
    GlobalShowCache = NewRedisWrap(config.GlobalConfig.ShowCacheConfig)
    }
    }
    gonil:@董泽润 那这个map还要加锁,不知道跟全局的初始化mysql redis连接池有什么区别
    董泽润:@gonil 就是在各个模块的init里生成一个闭包,把这个闭包在全局的map里注册,由Main来执行。 // 模块在这里注册
    func RegisterOnRun(name string, fn func()) {
    _, exists := ServerOnRun[name]
    if exists {
    panic(name + " ServerOnRun duplicate Register")
    }
    ServerOnRun[name] = fn
    }

本文标题:go项目的一些心得

本文链接:https://www.haomeiwen.com/subject/hnbqlttx.html