Golang工程经验
作为一个C/C++的开发者而言,开启Golang语言开发之路是很容易的,从语法、语义上的理解到工程开发,都能够快速熟悉起来;相比C、C++,Golang语言更简洁,更容易写出高并发的服务后台系统
转战Golang一年有余,经历了两个线上项目的洗礼,总结出一些工程经验,一个是总结出一些实战经验,一个是用来发现自我不足之处
Golang语言简介
Go语言是谷歌推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。
基于Golang的IM系统架构
我基于Golang的两个实际线上项目都是IM系统,本文基于现有线上系统做一些总结性、引导性的经验输出。
Golang TCP长连接 & 并发
既然是IM系统,那么必然需要TCP长连接来维持,由于Golang本身的基础库和外部依赖库非常之多,我们可以简单引用基础net网络库,来建立TCP server。一般的TCP Server端的模型,可以有一个协程【或者线程】去独立执行accept,并且是for循环一直accept新的连接,如果有新连接过来,那么建立连接并且执行Connect,由于Golang里面协程的开销非常之小,因此,TCP server端还可以一个连接一个goroutine去循环读取各自连接链路上的数据并处理。当然, 这个在C++语言的TCP Server模型中,一般会通过EPoll模型来建立server端,这个是和C++的区别之处。
关于读取数据,Linux系统有recv和send函数来读取发送数据,在Golang中,自带有io库,里面封装了各种读写方法,如io.ReadFull,它会读取指定字节长度的数据
为了维护连接和用户,并且一个连接一个用户的一一对应的,需要根据连接能够找到用户,同时也需要能够根据用户找到对应的连接,那么就需要设计一个很好结构来维护。我们最初采用map来管理,但是发现Map里面的数据太大,查找的性能不高,为此,优化了数据结构,conn里面包含user,user里面包含conn,结构如下【只包括重要字段】。
// 一个用户对应一个连接
type User struct {
uid int64
conn *MsgConn
BKicked bool // 被另外登陆的一方踢下线
BHeartBeatTimeout bool // 心跳超时
。。。
}
type MsgConn struct {
conn net.Conn
lastTick time.Time // 上次接收到包时间
remoteAddr string // 为每个连接创建一个唯一标识符
user *User // MsgConn与User一一映射
。。。
}
建立TCP server 代码片段如下
func ListenAndServe(network, address string) {
tcpAddr, err := net.ResolveTCPAddr(network, address)
if err != nil {
logger.Fatalf(nil, “ResolveTcpAddr err:%v”, err)
}
listener, err = net.ListenTCP(network, tcpAddr)
if err != nil {
logger.Fatalf(nil, “ListenTCP err:%v”, err)
}
go accept()
}
func accept() {
for {
conn, err := listener.AcceptTCP()
if err == nil {
// 包计数,用来限制频率
//anti-attack, 黑白名单
...
// 新建一个连接
imconn := NewMsgConn(conn)
// run
imconn.Run()
}
}
}
func (conn *MsgConn) Run() {
//on connect
conn.onConnect()
go func() {
tickerRecv := time.NewTicker(time.Second * time.Duration(rateStatInterval))
for {
select {
case <-conn.stopChan:
tickerRecv.Stop()
return
case <-tickerRecv.C:
conn.packetsRecv = 0
default:
// 在 conn.parseAndHandlePdu 里面通过Golang本身的io库里面提供的方法读取数据,如io.ReadFull
conn_closed := conn.parseAndHandlePdu()
if conn_closed {
tickerRecv.Stop()
return
}
}
}
}()
}
// 将 user 和 conn 一一对应起来
func (conn *MsgConn) onConnect() *User {
user := &User{conn: conn, durationLevel: 0, startTime: time.Now(), ackWaitMsgIdSet: make(map[int64]struct{})}
conn.user = user
return user
}
TCP Server的一个特点在于一个连接一个goroutine去处理,这样的话,每个连接独立,不会相互影响阻塞,保证能够及时读取到client端的数据。如果是C、C++程序,如果一个连接一个线程的话,如果上万个或者十万个线程,那么性能会极低甚至于无法工作,cpu会全部消耗在线程之间的调度上了,因此C、C++程序无法这样玩。Golang的话,goroutine可以几十万、几百万的在一个系统中良好运行。同时对于TCP长连接而言,一个节点上的连接数要有限制策略。
连接超时
每个连接需要有心跳来维持,在心跳间隔时间内没有收到,服务端要检测超时并断开连接释放资源,golang可以很方便的引用需要的数据结构,同时对变量的赋值(包括指针)非常easy
var timeoutMonitorTree *rbtree.Rbtree
var timeoutMonitorTreeMutex sync.Mutex
var heartBeatTimeout time.Duration //心跳超时时间, 配置了默认值ssss
var loginTimeout time.Duration //登陆超时, 配置了默认值ssss
type TimeoutCheckInfo struct {
conn *MsgConn
dueTime time.Time
}
func AddTimeoutCheckInfo(conn *MsgConn) {
timeoutMonitorTreeMutex.Lock()
timeoutMonitorTree.Insert(&TimeoutCheckInfo{conn: conn, dueTime: time.Now().Add(loginTimeout)})
timeoutMonitorTreeMutex.Unlock()
}
如 &TimeoutCheckInfo{},赋值一个指针对象
Golang 基础数据结构
Golang中,很多基础数据都通过库来引用,我们可以方便引用我们所需要的库,通过import包含就能直接使用,如源码里面提供了sync库,里面有mutex锁,在需要锁的时候可以包含进来
常用的如list,mutex,once,singleton等都已包含在内
list链表结构,当我们需要类似队列的结构的时候,可以采用,针对IM系统而言,在长连接层处理的消息id的列表,可以通过list来维护,如果用户有了回应则从list里面移除,否则在超时时间到后还没有回应,则入offline处理
mutex锁,当需要并发读写某个数据的时候使用,包含互斥锁和读写锁
var ackWaitListMutex sync.RWMutex
var ackWaitListMutex sync.Mutex
once表示任何时刻都只会调用一次,一般的用法是初始化实例的时候使用,代码片段如下
var initRedisOnce sync.Once
func GetRedisCluster(name string) (*redis.Cluster, error) {
initRedisOnce.Do(setupRedis)
if redisClient, inMap := redisClusterMap[name]; inMap {
return redisClient, nil
} else {
}
}
func setupRedis() {
redisClusterMap = make(map[string]*redis.Cluster)
commonsOpts := []redis.Option{
redis.ConnectionTimeout(conf.RedisConnTimeout),
redis.ReadTimeout(conf.RedisReadTimeout),
redis.WriteTimeout(conf.RedisWriteTimeout),
redis.IdleTimeout(conf.RedisIdleTimeout),
redis.MaxActiveConnections(conf.RedisMaxConn),
redis.MaxIdleConnections(conf.RedisMaxIdle),
}),
…
}
}
这样我们可以在任何需要的地方调用GetRedisCluster,并且不用担心实例会被初始化多次,once会保证一定只执行一次
singleton单例模式,这个在C++里面是一个常用的模式,一般需要开发者自己通过类来实现,类的定义决定单例模式设计的好坏;在Golang中,已经有成熟的库实现了,开发者无须重复造轮子,关于什么时候该使用单例模式请自行Google。一个简单的例子如下
import "github.com/dropbox/godropbox/singleton"
var SingleMsgProxyService = singleton.NewSingleton(func() (interface{}, error) {
cluster, _ := cache.GetRedisCluster("singlecache")
return &singleMsgProxy{
Cluster: cluster,
MsgModel: msg.MsgModelImpl,
}, nil
})
Golang interface 接口
如果说goroutine和channel是Go并发的两大基石,那么接口interface是Go语言编程中数据类型的关键。在Go语言的实际编程中,几乎所有的数据结构都围绕接口展开,接口是Go语言中所有数据结构的核心。
interface - 泛型编程
严格来说,在 Golang 中并不支持泛型编程。在 C++ 等高级语言中使用泛型编程非常的简单,所以泛型编程一直是 Golang 诟病最多的地方。但是使用 interface 我们可以实现泛型编程,如下是一个参考示例
package sort
// A type, typically a collection, that satisfies sort.Interface can be
// sorted by the routines in this package. The methods require that the
// elements of the collection be enumerated by an integer index.
type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less reports whether the element with
// index i should sort before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}
…
// Sort sorts data.
// It makes one call to data.Len to determine n, and O(nlog(n)) calls to
// data.Less and data.Swap. The sort is not guaranteed to be stable.
func Sort(data Interface) {
// Switch to heapsort if depth of 2ceil(lg(n+1)) is reached.
n := data.Len()
maxDepth := 0
for i := n; i > 0; i >>= 1 {
maxDepth++
}
maxDepth *= 2
quickSort(data, 0, n, maxDepth)
}
Sort 函数的形参是一个 interface,包含了三个方法:Len(),Less(i,j int),Swap(i, j int)。使用的时候不管数组的元素类型是什么类型(int, float, string…),只要我们实现了这三个方法就可以使用 Sort 函数,这样就实现了“泛型编程”。
这种方式,我在项目里面也有实际应用过,具体案例就是对消息排序。
下面给一个具体示例,代码能够说明一切,一看就懂:
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s: %d", p.Name, p.Age)
}
// ByAge implements sort.Interface for []Person based on
// the Age field.
type ByAge []Person //自定义
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func main() {
people := []Person{
{“Bob”, 31},
{“John”, 42},
{“Michael”, 17},
{“Jenny”, 26},
}
fmt.Println(people)
sort.Sort(ByAge(people))
fmt.Println(people)
}
interface - 隐藏具体实现
隐藏具体实现,这个很好理解。比如我设计一个函数给你返回一个 interface,那么你只能通过 interface 里面的方法来做一些操作,但是内部的具体实现是完全不知道的。
例如我们常用的context包,就是这样的,context 最先由 google 提供,现在已经纳入了标准库,而且在原有 context 的基础上增加了:cancelCtx,timerCtx,valueCtx。
如果函数参数是interface或者返回值是interface,这样就可以接受任何类型的参数
基于Golang的model service 模型【类MVC模型】
在一个项目工程中,为了使得代码更优雅,需要抽象出一些模型出来,同时基于C++面向对象编程的思想,需要考虑到一些类、继承相关。在Golang中,没有类、继承的概念,但是我们完全可以通过struct和interface来建立我们想要的任何模型。在我们的工程中,抽象出一种我自认为是类似MVC的模型,但是不完全一样,个人觉得这个模型抽象的比较好,容易扩展,模块清晰。对于使用java和PHP编程的同学对这个模型应该是再熟悉不过了,我这边通过代码来说明下这个模型
首先一个model包,通过interface来实现,包含一些基础方法,需要被外部引用者来具体实现
package model
// 定义一个基础model
type MsgModel interface {
Persist(context context.Context, msg interface{}) bool
UpdateDbContent(context context.Context, msgIface interface{}) bool
…
}
再定义一个msg包,用来具体实现model包中MsgModel模型的所有方法
package msg
type msgModelImpl struct{}
var MsgModelImpl = msgModelImpl{}
func (m msgModelImpl) Persist(context context.Context, msgIface interface{}) bool {
// 具体实现
}
func (m msgModelImpl) UpdateDbContent(context context.Context, msgIface interface{}) bool {
// 具体实现
}
…
model 和 具体实现方定义并实现ok后,那么就还需要一个service来统筹管理
package service
// 定义一个msgService struct包含了model里面的UserModel和MsgModel两个model
type msgService struct {
msgModel model.MsgModel
}
// 定义一个MsgService的变量,并初始化,这样通过MsgService,就能引用并访问model的所有方法
var (
MsgService = msgService{
msgModel: msg.MsgModelImpl,
}
)
调用访问
import service
service.MsgService.Persist(ctx, xxx)
总结一下,model对应MVC的M,service 对应 MVC的C, 调用访问的地方对应MVC的V
Golang 基础资源的封装
在MVC模型的基础下,我们还需要考虑另外一点,就是基础资源的封装,服务端操作必然会和mysql、redis、memcache等交互,一些常用的底层基础资源,我们有必要进行封装,这是基础架构部门所需要承担的,也是一个好的项目工程所需要的
redis
redis,我们在github.com/garyburd/redigo/redis的库的基础上,做了一层封装,实现了一些更为贴合工程的机制和接口,redis cluster封装,支持分片、读写分离
// NewCluster creates a client-side cluster for c