首页 » Web前端 » phpredishashsort技巧_Golang工程经验

phpredishashsort技巧_Golang工程经验

访客 2024-12-06 0

扫一扫用手机浏览

文章目录 [+]

转战Golang一年有余,经历了两个线上项目的洗礼,总结出一些工程履历,一个是总结出一些实战履历,一个是用来创造自我不敷之处

Golang措辞简介

phpredishashsort技巧_Golang工程经验

Go措辞是谷歌推出的一种全新的编程措辞,可以在不丢失运用程序性能的情形低落低代码的繁芜性。
Go措辞专门针对多处理器系统运用程序的编程进行了优化,利用Go编译的程序可以媲美C或C++代码的速率,而且更加安全、支持并行进程。

phpredishashsort技巧_Golang工程经验
(图片来自网络侵删)

基于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.Rbtreevar timeoutMonitorTreeMutex sync.Mutexvar heartBeatTimeout time.Duration //心跳超时时间, 配置了默认值ssssvar loginTimeout time.Duration //上岸超时, 配置了默认值sssstype 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.RWMutexvar ackWaitListMutex sync.Mutexonce表示任何时候都只会调用一次,一样平常的用法是初始化实例的时候利用,代码片段如下

var initRedisOnce sync.Oncefunc 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// 定义一个根本modeltype MsgModel interface { Persist(context context.Context, msg interface{}) bool UpdateDbContent(context context.Context, msgIface interface{}) bool ...}再定义一个msg包,用来详细实现model包中MsgModel模型的所有方法

package msgtype 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 serviceservice.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 callers. Callers use this structure to interact with Redis databasefunc NewCluster(config ClusterConfig, instrumentOpts instrument.Options) Cluster { cluster := new(Cluster) cluster.pool = make([]client, len(config.Configs)) masters := make([]string, 0, len(config.Configs)) for i, sharding := range config.Configs { master, slaves := sharding.Master, sharding.Slaves masters = append(masters, master) masterAddr, masterDb := parseServer(master) cli := new(client) cli.master = &redisNode{ server: master, Pool: func() redis.Pool { pool := &redis.Pool{ MaxIdle: config.MaxIdle, IdleTimeout: config.IdleTimeout, Dial: func() (redis.Conn, error) { c, err := redis.Dial( \"大众tcp\公众, masterAddr, redis.DialDatabase(masterDb), redis.DialPassword(config.Password), redis.DialConnectTimeout(config.ConnTimeout), redis.DialReadTimeout(config.ReadTimeout), redis.DialWriteTimeout(config.WriteTimeout), ) if err != nil { return nil, err } return c, err }, TestOnBorrow: func(c redis.Conn, t time.Time) error { if time.Since(t) < time.Minute { return nil } _, err := c.Do(\公众PING\"大众) return err }, MaxActive: config.MaxActives, } if instrumentOpts == nil { return pool } return instrument.NewRedisPool(pool, instrumentOpts) }(), } // allow nil slaves if slaves != nil { cli.slaves = make([]redisNode, 0) for _, slave := range slaves { addr, db := parseServer(slave) cli.slaves = append(cli.slaves, &redisNode{ server: slave, Pool: func() redis.Pool { pool := &redis.Pool{ MaxIdle: config.MaxIdle, IdleTimeout: config.IdleTimeout, Dial: func() (redis.Conn, error) { c, err := redis.Dial( \公众tcp\"大众, addr, redis.DialDatabase(db), redis.DialPassword(config.Password), redis.DialConnectTimeout(config.ConnTimeout), redis.DialReadTimeout(config.ReadTimeout), redis.DialWriteTimeout(config.WriteTimeout), ) if err != nil { return nil, err } return c, err }, TestOnBorrow: func(c redis.Conn, t time.Time) error { if time.Since(t) < time.Minute { return nil } _, err := c.Do(\"大众PING\"大众) return err }, MaxActive: config.MaxActives, } if instrumentOpts == nil { return pool } return instrument.NewRedisPool(pool, instrumentOpts) }(), }) } } // call init cli.init() cluster.pool[i] = cli } if config.Hashing == sharding.Ketama { cluster.sharding, _ = sharding.NewKetamaSharding(sharding.GetShardServers(masters), true, 6379) } else { cluster.sharding, _ = sharding.NewCompatSharding(sharding.GetShardServers(masters)) } return cluster}

总结一下:

利用连接池提高性能,每次都从连接池里面取连接而不是每次都重新建立连接设置最大连接数和最大生动连接(同一时候能够供应的连接),设置合理的读写超时时间实现主从读写分离,提高性能,须要把稳如果没有从库则只读主库TestOnBorrow用来进行康健检测单独开一个goroutine协程用来定期保活【ping-pong】hash分片算法的选择,同等性hash还是hash取模,hash取模在扩缩容的时候比较方便,同等性hash并没有带来明显的上风,我们公司内部统一建议采取hash取模考虑如何支持双写策略

memcache

memcached客户端代码封装,依赖 github.com/dropbox/godropbox/memcache, 实现其ShardManager接口,支持Connection Timeout,支持Fail Fast和Rehash

goroutine & chann

实际开拓过程中,常常会有这样场景,每个要求通过一个goroutine协程去做,如批量获取消息,但是,为了防止后端资源连接数太多等,或者防止goroutine太多,每每须要限定并发数。
给出如下示例供参考

package mainimport ( \公众fmt\"大众 \"大众sync\公众 \公众time\"大众)var over = make(chan bool)const MAXConCurrency = 3//var sem = make(chan int, 4) //掌握并发任务数var sem = make(chan bool, MAXConCurrency) //掌握并发任务数var maxCount = 6func Worker(i int) bool { sem <- true defer func() { <-sem }() // 仿照出错处理 if i == 5 { return false } fmt.Printf(\"大众now:%v num:%v\n\公众, time.Now().Format(\公众04:05\"大众), i) time.Sleep(1 time.Second) return true}func main() { //wg := &sync.WaitGroup{} var wg sync.WaitGroup for i := 1; i <= maxCount; i++ { wg.Add(1) fmt.Printf(\"大众for num:%v\n\"大众, i) go func(i int) { defer wg.Done() for x := 1; x <= 3; x++ { if Worker(i) { break } else { fmt.Printf(\公众retry :%v\n\"大众, x) } } }(i) } wg.Wait() //等待所有goroutine退出}

goroutine & context.cancel

Golang 的 context非常强大,详细的可以参考我的其余一篇文章 Golang Context剖析

这里想要解释的是,在项目工程中,我们常常会用到这样的一个场景,通过goroutine并刊行止理某些批量任务,当某个条件触发的时候,这些goroutine要能够掌握停滞实行。
如果有这样的场景,那么咱们就须要用到context的With 系列函数了,context.WithCancel天生了一个withCancel的实例以及一个cancelFuc,这个函数便是用来关闭ctxWithCancel中的 Done channel 函数。

示例代码片段如下

func Example(){ // context.WithCancel 用来天生一个新的Context,可以接管cancel方法用来随时停滞实行 newCtx, cancel := context.WithCancel(context.Background()) for peerIdVal, lastId := range lastIdMap { wg.Add(1) go func(peerId, minId int64) { defer wg.Done() msgInfo := Get(newCtx, uid, peerId, minId, count).([]pb.MsgInfo) if msgInfo != nil && len(msgInfo) > 0 { if singleMsgCounts >= maxCount { cancel() // 当条件触发,则调用cancel停滞 mutex.Unlock() return } } mutex.Unlock() }(peerIdVal, lastId) } wg.Wait() } func Get(ctx context.Context, uid, peerId, sinceId int64, count int) interface{} { for { select { // 如果收到Done的chan,则立马return case <-ctx.Done(): msgs := make([]pb.MsgInfo, 0) return msgs default: // 处理逻辑 } }}

traceid & context

在大型项目工程中,为了更好的排查定位问题,我们须要有一定的技巧,Context高下文存在于一整条调用链路中,在做事端并发场景下,n多个要求里面,我们如何能够快速准确的找到一条要求的来龙去脉,专业用语便是指调用链路,通过调用链我们能够知道这条要求经由了哪些做事、哪些模块、哪些方法,这样可以非常方便我们定位问题

traceid便是我们抽象出来的这样一个调用链的唯一标识,再通过Context进行通报,在任何代码模块[函数、方法]里面都包含Context参数,我们就能形成一个完全的调用链。
那么如何实现呢 ?在我们的工程中,有RPC模块,有HTTP模块,两个模块的要求来源肯定不一样,因此,要实现所有做事和模块的完全调用链,须要考虑http和rpc两个不同的网络要求的调用链

traceid的实现

const TraceKey = \"大众traceId\公众func NewTraceId(tag string) string { now := time.Now() return fmt.Sprintf(\公众%d.%d.%s\"大众, now.Unix(), now.Nanosecond(), tag)}func GetTraceId(ctx context.Context) string { if ctx == nil { return \"大众\公众 } // 从Context里面取 traceInfo := GetTraceIdFromContext(ctx) if traceInfo == \公众\公众 { traceInfo = GetTraceIdFromGRPCMeta(ctx) } return traceInfo}func GetTraceIdFromGRPCMeta(ctx context.Context) string { if ctx == nil { return \"大众\公众 } if md, ok := metadata.FromIncomingContext(ctx); ok { if traceHeader, inMap := md[meta.TraceIdKey]; inMap { return traceHeader[0] } } if md, ok := metadata.FromOutgoingContext(ctx); ok { if traceHeader, inMap := md[meta.TraceIdKey]; inMap { return traceHeader[0] } } return \"大众\公众}func GetTraceIdFromContext(ctx context.Context) string { if ctx == nil { return \"大众\"大众 } traceId, ok := ctx.Value(TraceKey).(string) if !ok { return \"大众\"大众 } return traceId}func SetTraceIdToContext(ctx context.Context, traceId string) context.Context { return context.WithValue(ctx, TraceKey, traceId)}

http的traceid

对付http的做事,要求方可能是客户端,也能是其他做事端,http的入口里面就须要增加上traceid,然后打印日志的时候,将TraceID打印出来形成完全链路。
如果http server采取gin来实现的话,代码片段如下,其他http server的库的实现办法类似即可

import \公众github.com/gin-gonic/gin\"大众func recoveryLoggerFunc() gin.HandlerFunc { return func(c gin.Context) { c.Set(trace.TraceKey, trace.NewTraceId(c.ClientIP())) defer func() { ...... func 省略实现 } }() c.Next() }}engine := gin.New()engine.Use(OpenTracingFunc(), httpInstrumentFunc(), recoveryLoggerFunc()) session := engine.Group(\"大众/sessions\公众) session.Use(sdkChecker) { session.POST(\"大众/recent\"大众, httpsrv.MakeHandler(RecentSessions)) }这样,在RecentSessions接口里面如果打印日志,就能够通过Context取到traceid

access log

access log是针对http的要求来的,记录http要求的API,相应韶光,ip,相应码,用来记录并可以统计做事的相应情形,当然,也有其他赞助系统如SLA来专门记录http的相应情形

Golang措辞实现这个也非常大略,而且这个是个通用功能,建议可以抽象为一个根本模块,所有业务都能import后利用

大致格式如下:http_log_pattern='%{2006-01-02T15:04:05.999-0700}t %a - %{Host}i \"大众%r\"大众 %s - %T \"大众%{X-Real-IP}i\"大众 \"大众%{X-Forwarded-For}i\"大众 %{Content-Length}i - %{Content-Length}o %b %{CDN}i' \公众%a\"大众, \"大众${RemoteIP}\"大众, \"大众%b\"大众, \"大众${BytesSent|-}\公众, \公众%B\"大众, \"大众${BytesSent|0}\"大众, \"大众%H\"大众, \公众${Proto}\"大众, \"大众%m\"大众, \"大众${Method}\"大众, \"大众%q\公众, \公众${QueryString}\"大众, \"大众%r\"大众, \"大众${Method} ${RequestURI} ${Proto}\公众, \"大众%s\"大众, \公众${StatusCode}\"大众, \公众%t\"大众, \公众${ReceivedAt|02/Jan/2006:15:04:05 -0700}\公众, \公众%U\公众, \"大众${URLPath}\"大众, \"大众%D\公众, \"大众${Latency|ms}\公众, \"大众%T\公众, \"大众${Latency|s}\"大众,详细实现省略

终极得到的日志如下:

2017-12-20T20:32:58.787+0800 192.168.199.15 - www.demo.com:50001 \公众POST /arcp/unregister HTTP/1.1\公众 200 - 0.035 \"大众-\"大众 \"大众-\"大众 14 - - 13 -2017-12-20T20:33:27.741+0800 192.168.199.15 - www.demo.com:50001 \"大众POST /arcp/register HTTP/1.1\"大众 200 - 0.104 \"大众-\公众 \"大众-\"大众 68 - - 13 -2017-12-20T20:42:01.803+0800 192.168.199.15 - www.demo.com:50001 \公众POST /arcp/unregister HTTP/1.1\"大众 200 - 0.035 \公众-\公众 \"大众-\"大众 14 - - 13 -

开关策略、降级策略

线上做事端系统,必须要有降级机制,也最好能够有开关机制。
降级机制在于涌现非常情形能够舍弃某部分做事担保其他主线做事正常;开关也有着同样的功效,在某些情形下打开开关,则能够实行某些功能或者说某套功能,关闭开关则实行其余一套功能或者不实行某个功能。

这不是Golang的措辞特性,但是是工程项目里面必要的,在Golang项目中的详细实当代码片段如下:

package switchesvar ( xxxSwitchManager = SwitchManager{switches: make(map[string]Switch)} AsyncProcedure = &Switch{Name: \"大众xxx.msg.procedure.async\"大众, On: true} // 使能音视频 EnableRealTimeVideo = &Switch{Name: \公众xxx.real.time.video\"大众, On: true})func init() { xxxSwitchManager.Register(AsyncProcedure, EnableRealTimeVideo)}// 详细实现构造和实现方法type Switch struct { Name string On bool listeners []ChangeListener}func (s Switch) TurnOn() { s.On = true s.notifyListeners()}func (s Switch) notifyListeners() { if len(s.listeners) > 0 { for _, l := range s.listeners { l.OnChange(s.Name, s.On) } }}func (s Switch) TurnOff() { s.On = false s.notifyListeners()}func (s Switch) IsOn() bool { return s.On}func (s Switch) IsOff() bool { return !s.On}func (s Switch) AddChangeListener(l ChangeListener) { if l == nil { return } s.listeners = append(s.listeners, l)}type SwitchManager struct { switches map[string]Switch}func (m SwitchManager) Register(switches ...Switch) { for _, s := range switches { m.switches[s.Name] = s }}func (m SwitchManager) Unregister(name string) { delete(m.switches, name)}func (m SwitchManager) TurnOn(name string) (bool, error) { if s, ok := m.switches[name]; ok { s.TurnOn() return true, nil } else { return false, errors.New(\公众switch \"大众 + name + \"大众 is not registered\公众) }}func (m SwitchManager) TurnOff(name string) (bool, error) { if s, ok := m.switches[name]; ok { s.TurnOff() return true, nil } else { return false, errors.New(\公众switch \"大众 + name + \"大众 is not registered\"大众) }}func (m SwitchManager) IsOn(name string) (bool, error) { if s, ok := m.switches[name]; ok { return s.IsOn(), nil } else { return false, errors.New(\"大众switch \"大众 + name + \"大众 is not registered\"大众) }}func (m SwitchManager) List() map[string]bool { switches := make(map[string]bool) for name, switcher := range m.switches { switches[name] = switcher.On } return switches}type ChangeListener interface { OnChange(name string, isOn bool)}// 这里开始调用if switches.AsyncProcedure.IsOn() { // do sth}else{ // do other sth}

prometheus + grafana

prometheus + grafana 是业界常用的监控方案,prometheus进行数据采集,grafana进行图表展示。

Golang里面prometheus进行数据采集非常大略,有对应client库,运用程序只需暴露出http接口即可,这样,prometheus server端就可以定期采集数据,并且还可以根据这个接口来监控做事端是否非常【如挂掉的情形】。

import \公众github.com/prometheus/client_golang/prometheus\公众

engine.GET(\"大众/metrics\公众, gin.WrapH(prometheus.Handler()))

这样就实现了数据采集,但是详细采集什么样的数据,数据从哪里天生的,还须要进入下一步:

package prometheusimport \"大众github.com/prometheus/client_golang/prometheus\"大众var DefaultBuckets = []float64{10, 50, 100, 200, 500, 1000, 3000}var MySQLHistogramVec = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Namespace: \公众allen.wu\"大众, Subsystem: \"大众xxx\公众, Name: \"大众mysql_op_milliseconds\"大众, Help: \"大众The mysql database operation duration in milliseconds\"大众, Buckets: DefaultBuckets, }, []string{\公众db\"大众},)var RedisHistogramVec = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Namespace: \公众allen.wu\"大众, Subsystem: \公众xxx\"大众, Name: \"大众redis_op_milliseconds\"大众, Help: \公众The redis operation duration in milliseconds\"大众, Buckets: DefaultBuckets, }, []string{\公众redis\"大众},)func init() { prometheus.MustRegister(MySQLHistogramVec) prometheus.MustRegister(RedisHistogramVec) ...}// 利用,在对应的位置调用prometheus接口天生数据instanceOpts := []redis.Option{ redis.Shards(shards...), redis.Password(viper.GetString(conf.RedisPrefix + name + \公众.password\"大众)), redis.ClusterName(name), redis.LatencyObserver(func(name string, latency time.Duration) { prometheus.RedisHistogramVec.WithLabelValues(name).Observe(float64(latency.Nanoseconds()) 1e-6) }), }

捕获非常 和 缺点处理

panic 非常

捕获非常是否有存在的必要,根据各自不同的项目自行决定,但是一样平常涌现panic,如果没有非常,那么做事就会直接挂掉,如果能够捕获非常,那么涌现panic的时候,做事不会挂掉,只是当前导致panic的某个功能,无法正常利用,个人建议还是在某些有必要的条件和入口处进行非常捕获。

常见抛出非常的情形:数组越界、空指针空工具,类型断言失落败等;Golang里面捕获非常通过 defer + recover来实现

C++有try。


catch来进行代码片段的非常捕获,Golang里面有recover来进行非常捕获,这个是Golang措辞的基本功,是一个比较大略的功能,不多说,看代码

func consumeSingle(kafkaMsg sarama.ConsumerMessage) { var err error defer func() { if r := recover(); r != nil { if e, ok := r.(error); ok { // 非常捕获的处理 } } }()}

在要求来源入口处的函数或者某个方法里面实现这么一段代码进行捕获,这样,只要通过这个入口涌现的非常都能被捕获,并打印详细日志

error 缺点

error缺点,可以自定义返回,一样平常工程运用中的做法,会在方法的返回值上增加一个error返回值,Golang许可每个函数返回多个返回值,增加一个error的浸染在于,获取函数返回值的时候,根据error参数进行判断,如果是nil表示没有缺点,正常处理,否则处理缺点逻辑。
这样减少代码涌现非常情形

panic 抛出的堆栈信息排查

如果某些情形下,没有捕获非常,程序在运行过程中涌现panic,一样平常都会有一些堆栈信息,我们如何根据这些堆栈信息快速定位并办理呢 ?

一样平常信息里面都会表明是哪种类似的panic,如是空指针非常还是数组越界,还是xxx;

然后会打印一堆信息出来包括涌现非常的代码调用块及其文件位置,须要定位到末了的位置然后反推上去

剖析示例如下

{\"大众date\公众:\"大众2017-11-22 19:33:20.921\"大众,\"大众pid\"大众:17,\公众level\"大众:\"大众ERROR\"大众,\公众file\"大众:\"大众recovery.go\"大众,\"大众line\公众:16,\公众func\"大众:\"大众1\公众,\公众msg\公众:\公众panic in /Message.MessageService/Proces

s: runtime error: invalid memory address or nil pointer dereference

github.com.xxx/demo/biz/vendor/github.com.xxx/demo/commons/interceptor.newUnaryServerRecoveryInterceptor.func1.1

/www/jenkins_home/.jenkins/jobs/demo/jobs/demo--biz/workspace/src/github.com.xxx/demo/biz/vendor/github.com.xxx/demo/commons/

interceptor/recovery.go:17

runtime.call64

/www/jenkins_home/.jenkins/tools/org.jenkinsci.plugins.golang.GolangInstallation/go1.9/go/src/runtime/asm_amd64.s:510

runtime.gopanic

/www/jenkins_home/.jenkins/tools/org.jenkinsci.plugins.golang.GolangInstallation/go1.9/go/src/runtime/panic.go:491

runtime.panicmem

/www/jenkins_home/.jenkins/tools/org.jenkinsci.plugins.golang.GolangInstallation/go1.9/go/src/runtime/panic.go:63

runtime.sigpanic

/www/jenkins_home/.jenkins/tools/org.jenkinsci.plugins.golang.GolangInstallation/go1.9/go/src/runtime/signal_unix.go:367

github.com.xxx/demo/biz/vendor/github.com.xxx/demo/mtrace-middleware-go/grpc.OpenTracingClientInterceptor.func1

/www/jenkins_home/.jenkins/jobs/demo/jobs/demo--biz/workspace/src/github.com.xxx/demo/biz/vendor/github.com.xxx/demo/m

trace-middleware-go/grpc/client.go:49

github.com.xxx/demo/biz/vendor/github.com/grpc-ecosystem/go-grpc-middleware.ChainUnaryClient.func2.1.1

/www/jenkins_home/.jenkins/jobs/demo/jobs/demo--biz/workspace/src/github.com.xxx/demo/biz/vendor/github.com/grpc-ecosystem/go-gr

pc-middleware/chain.go:90

github.com.xxx/demo/biz/vendor/github.com/grpc-ecosystem/go-grpc-middleware/retry.UnaryClientInterceptor.func1

问题剖析

通过报错的堆栈信息,可以看到详细缺点是“runtime error: invalid memory address or nil pointer dereference”,也便是空指针非常,然后逐步定位日志,可以创造终极导致涌现非常的函数在这个,如下:

github.com.xxx/demo/biz/vendor/github.com.xxx/demo/mtrace-middleware-go/grpc.OpenTracingClientInterceptor.func1 /www/jenkins_home/.jenkins/jobs/demo/jobs/demo--biz/workspace/src/github.com.xxx/demo/biz/vendor/github.com.xxx/demo/mtrace-middleware-go/grpc/client.go:49

一样平常panic,都会有上述缺点日志,然后通过日志,可以追踪到详细函数,然后看到OpenTracingClientInterceptor后,是在client.go的49行,然后开始反推,通过代码可以看到,可能是trace指针为空。
然后一步一步看是从哪里开始调用的

终极创造代码如下:

ucConn, err := grpcclient.NewClientConn(conf.Discovery.UserCenter, newBalancer, time.Second3, conf.Tracer) if err != nil { logger.Fatalf(nil, \公众init user center client connection failed: %v\"大众, err) return } UserCenterClient = pb.NewUserCenterServiceClient(ucConn)

那么开始排查,conf.Tracer是不是可能为空,在哪里初始化,初始化有没有错,然后创造这个函数是在init里面,然后conf.Tracer确实在main函数里面显示调用的,main函数里面会引用或者间接引用所有包,那么init就一定在main之前实行。

这样的话,init实行的时候,conf.Tracer还没有被赋值,因此便是nil,就会导致panic了

项目工程级别接口

项目中如果能够有一些调试debug接口,有一些pprof性能剖析接口,有探测、康健检讨接口的话,会给全体项目在线上稳定运行带来很大的浸染。
除了pprof性能剖析接口属于Golang特有,其他的接口在任何措辞都有,这里只是表明在一个工程中,须要有这类型的接口

高下线接口

我们的工程是通过etcd进行做事创造和注册的,同时还供应http做事,那么就须要有个机制来高下线,这样上线过程中,如果做事本身还没有全部启动完成准备就绪,那么就暂时不要在etcd里面注册,不要上线,以免有要求过来,等到就绪后再注册;下线过程中,先从etcd里面移除,这样流量不再导入过来,然后再等待一段韶光用来处理还未完成的任务

我们的做法是,start 和 stop 做事的时候,调用API接口,然后再在做事的API接口里面注册和反注册到etcd

var OnlineHook = func() error {

return nil

}

var OfflineHook = func() error {

return nil

}

// 初始化两个函数,注册和反注册到etcd的函数

api.OnlineHook = func() error {

return registry.Register(conf.Discovery.RegisterAddress)

}

api.OfflineHook = func() error {

return registry.Deregister()

}

// 设置在线的函数里面分别调用上述两个函数,用来高下线

func SetOnline(isOnline bool) (err error) {

if conf.Discovery.RegisterEnabled {

if !isServerOnline && isOnline {

err = OnlineHook()

} else if isServerOnline && !isOnline {

err = OfflineHook()

}

}

if err != nil {

return

}

isServerOnline = isOnline

return

}

SetOnline 为Http API接口调用的函数

nginx 探测接口,康健检讨接口

对付http的做事,一样平常访问都通过域名访问,nginx配置代理,这样担保做事可以随意扩缩容,但是nginx既然配置了代码,后端节点的情形,就必须要能够有接口可以探测,这样才能担保流量导入到的节点一定的在康健运行中的节点;为此,做事必须要供应康健检测的接口,这样才能方便nginx代理能够实时更新节点。

这个接口如何实现?nginx代理一样平常通过http code来处理,如果返回code=200,认为节点正常,如果是非200,认为节点非常,如果连续采样多次都返回非常,那么nginx将节点下掉

如供应一个/devops/status 的接口,用来检测,接口对应的详细实现为:

func CheckHealth(c gin.Context) {

// 首先状态码设置为非200,如503

httpStatus := http.StatusServiceUnavailable

// 如果当前做事正常,并做事没有下线,则更新code

if isServerOnline {

httpStatus = http.StatusOK

}

// 否则返回code为503

c.IndentedJSON(httpStatus, gin.H{

onlineParameter: isServerOnline,

})

}

PProf性能排查接口

// PProf profGroup := debugGroup.Group(\"大众/pprof\公众) profGroup.GET(\"大众/\"大众, func(c gin.Context) { pprof.Index(c.Writer, c.Request) }) profGroup.GET(\公众/goroutine\"大众, gin.WrapH(pprof.Handler(\公众goroutine\公众))) profGroup.GET(\公众/block\公众, gin.WrapH(pprof.Handler(\"大众block\"大众))) profGroup.GET(\公众/heap\"大众, gin.WrapH(pprof.Handler(\"大众heap\"大众))) profGroup.GET(\"大众/threadcreate\公众, gin.WrapH(pprof.Handler(\"大众threadcreate\"大众))) profGroup.GET(\"大众/cmdline\公众, func(c gin.Context) { pprof.Cmdline(c.Writer, c.Request) }) profGroup.GET(\"大众/profile\公众, func(c gin.Context) { pprof.Profile(c.Writer, c.Request) }) profGroup.GET(\"大众/symbol\"大众, func(c gin.Context) { pprof.Symbol(c.Writer, c.Request) }) profGroup.GET(\"大众/trace\"大众, func(c gin.Context) { pprof.Trace(c.Writer, c.Request) })

debug调试接口

// Debug debugGroup := engine.Group(\"大众/debug\公众) debugGroup.GET(\"大众/requests\公众, func(c gin.Context) { c.Writer.Header().Set(\公众Content-Type\"大众, \公众text/html; charset=utf-8\"大众) trace.Render(c.Writer, c.Request, true) }) debugGroup.GET(\"大众/events\"大众, func(c gin.Context) { c.Writer.Header().Set(\"大众Content-Type\"大众, \"大众text/html; charset=utf-8\"大众) trace.RenderEvents(c.Writer, c.Request, true) })

开关【降级】实时调度接口

前面有讲到过,在代码里面须要有开关和降级机制,并讲了实现示例,那么如果须要能够实时改变开关状态,并且实时生效,我们就可以供应一下http的API接口,供运维职员或者开拓职员利用。

// Switch console := engine.Group(\"大众/switch\公众) { console.GET(\"大众/list\"大众, httpsrv.MakeHandler(ListSwitches)) console.GET(\"大众/status\"大众, httpsrv.MakeHandler(CheckSwitchStatus)) console.POST(\公众/turnOn\公众, httpsrv.MakeHandler(TurnSwitchOn)) console.POST(\公众/turnOff\"大众, httpsrv.MakeHandler(TurnSwitchOff)) }

go test 单元测试用例

单元测试用例是必须,是自测的一个必要手段,Golang里面单元测试非常大略,import testing 包,然后实行go test,就能够测试某个模块代码

如,在某个user文件夹下有个user包,包文件为user.go,里面有个Func UpdateThemesCounts,如果想要进行test,那么在同级目录下,建立一个user_test.go的文件,包含testing包,编写test用例,然后调用go test即可

一样平常的规范有:

每个测试函数必须导入testing包测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头将测试文件和源码放在相同目录下,并将名字命名为{source_filename}_test.go常日情形下,将测试文件和源码放在同一个包内。

如下:

// user.gofunc UpdateThemesCounts(ctx context.Context, themes []int, count int) error { redisClient := model.GetRedisClusterForTheme(ctx) key := themeKeyPattern for _, theme := range themes { if redisClient == nil { return errors.New(\"大众now redis client\"大众) } total, err := redisClient.HIncrBy(ctx, key, theme, count) if err != nil { logger.Errorf(ctx, \公众add key:%v for theme:%v count:%v failed:%v\公众, key, theme, count, err) return err } else { logger.Infof(ctx, \"大众now key:%v theme:%v total:%v\"大众, key, theme, total) } } return nil}//user_test.gopackage userimport ( \"大众fmt\"大众 \"大众testing\"大众 \"大众Golang.org/x/net/context\"大众)func TestUpdateThemeCount(t testing.T) { ctx := context.Background() theme := 1 count := 123 total, err := UpdateThemeCount(ctx, theme, count) fmt.Printf(\"大众update theme:%v counts:%v err:%v \n\"大众, theme, total, err)}在此目录下实行 go test即可出结果

测试单个文件 or 测试单个包

常日,一个包里面会有多个方法,多个文件,因此也有多个test用例,如果我们只想测试某一个方法的时候,那么我们须要指定某个文件的某个方案

如下:

allen.wu@allen.wudeMacBook-Pro-4:~/Documents/work_allen.wu/goDev/Applications/src/github.com.xxx/avatar/app_server/service/centralhub$tree .

.

├── msghub.go

├── msghub_test.go

├── pushhub.go

├── rtvhub.go

├── rtvhub_test.go

├── userhub.go

└── userhub_test.go

0 directories, 7 files

统共有7个文件,个中有三个test文件,如果我们只想要测试rtvhub.go里面的某个方法,如果直接运行go test,就会测试所有test.go文件了。

因此我们须要在go test 后面再指定我们须要测试的test.go 文件和 它的源文件,如下:

go test -v msghub.go msghub_test.go

测试单个文件下的单个方法

在测试单个文件之下,如果我们单个文件下,有多个方法,我们还想只是测试单个文件下的单个方法,要如何实现?我们须要再在此根本上,用 -run 参数指定详细方法或者利用正则表达式。

如果test文件如下:

package centralhubimport ( \"大众context\公众 \公众testing\"大众)func TestSendTimerInviteToServer(t testing.T) { ctx := context.Background() err := sendTimerInviteToServer(ctx, 1461410596, 1561445452, 2) if err != nil { t.Errorf(\公众send to server friendship build failed. %v\"大众, err) }}func TestSendTimerInvite(t testing.T) { ctx := context.Background() err := sendTimerInvite(ctx, \"大众test\"大众, 1461410596, 1561445452) if err != nil { t.Errorf(\"大众send timeinvite to client failed:%v\公众, err) }}go test -v msghub.go msghub_test.go -run TestSendTimerInvitego test -v msghub.go msghub_test.go -run \"大众SendTimerInvite\"大众

测试所有方法

指定目录即可

go test

测试覆盖度

go test工具给我们供应了测试覆盖度的参数,

go test -v -cover

go test -cover -coverprofile=cover.out -covermode=count

go tool cover -func=cover.out

goalng GC 、编译运行

做事端开拓者如果在mac上开拓,那么Golang工程的代码可以直接在mac上编译运行,然后如果须要支配在Linux系统的时候,在编译参数里面指定GOOS即可,这样可以本地调试ok后再支配到Linux做事器。

如果要支配到Linux做事,编译参数的指定为

ldflags=\"大众 -X ${repo}/version.version=${version} -X ${repo}/version.branch=${branch} -X ${repo}/version.goVersion=${go_version} -X ${repo}/version.buildTime=${build_time} -X ${repo}/version.buildUser=${build_user}\"大众CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags \公众${ldflags}\公众 -o $binary_dir/$binary_name ${repo}/

对付GC,我们要网络起来,记录到日志文件中,这样方便后续排查和定位,启动的时候指定一下即可实行gc,网络gc日志可以重定向

export GIN_MODE=release GODEBUG=gctrace=1 $SERVER_ENTRY 1>/dev/null 2>$LOGDIR/gc.log.`date \"大众+%Y%m%d%H%M%S\"大众` &

Golang包管理 目录代码管理

目录代码管理

全体项目包括两大类,一个是自己编写的代码模块,一个是依赖的代码,依赖包须要有进行包管理,自己的编写的代码工程须要有一个得当的目录进行管理

main.go :入口

doc : 文档

conf : 配置干系

ops : 运维操作干系【http接口】

api : API接口【http交互接口】

daemon : 后台daemon干系

model : model模块,操作底层资源

service : model的service

grpcclient : rpc client

registry : etcd 注册

processor : 异步kafka消费

.├── README.md├── api├── conf├── daemon├── dist├── doc├── grpcclient├── main.go├── misc├── model├── ops├── processor├── registry├── service├── tools├── vendor└── version

包管理

go许可import不同代码库的代码,例如github.com, golang.org等等;对付须要import的代码,可以利用 go get 命令取下来放到GOPATH对应的目录中去。

对付go来说,实在并不care你的代码是内部还是外部的,总之都在GOPATH里,任何import包的路径都是从GOPATH开始的;唯一的差异,便是内部依赖的包是开拓者自己写的,外部依赖的包是go get下来的。

依赖GOPATH来办理go import有个很严重的问题:如果项目依赖的包做了修正,或者干脆删掉了,会影响到其他现有的项目。
为理解决这个问题,go在1.5版本引入了vendor属性(默认关闭,须要设置go环境变量GO15VENDOREXPERIMENT=1),并在1.6版本之后都默认开启了vendor属性。
这样的话,所有的依赖包都在项目工程的vendor中了,每个项目都有各自的vendor,互不影响;但是vendor里面的包没有版本信息,未便利进行版本管理。

目前市场上常用的包管理工具紧张有godep、glide、dep

godep

godep的利用者浩瀚,如docker,kubernetes, coreos等go项目很多都是利用godep来管理其依赖,当然缘故原由可能是早期也没的工具可选,早期我们也是利用godep进行包管理。

利用比较大略,godep save;godep restore;godep update;

但是后面随着我们利用和项目的进一步加强,我们创造godep有诸多痛点,目前已经逐步开始弃用godep,新项目都开始采取dep进行管理了。

godep的痛点:

godep如果碰着依赖项目里有vendor的时候就可能会导致编译不过,vendor下再嵌套vendor,就会导致编译的时候涌现版本不一致的缺点,会提示某个方法接口不对,全部放在当前项目的vendor下godep锁定版本太麻烦了,在项目进一步发展过程中,我们依赖的项目(包)可能是早期的,后面由于升级更新,某些API接口可能有变;但是我们项目如果已经上线稳定运行,我们不想用新版,那么就须要锁定某个特定版本。
但是这个对付godep而言,操作其实未便利。
godep的时候,常常会有一些包须要特定版本,然后包依赖编译不过,尤其是在多人协作的时候,本地gopath和vendor下的版本不一样,然后本地gopath和别人的gopath的版本不一样,导致编译会碰着各种依赖导致的问题

glide

glide也是在vendor之后出来的。
glide的依赖包信息在glide.yaml和glide.lock中,前者记录了所有依赖的包,后者记录了依赖包的版本信息

glide create # 创建glide工程,天生glide.yaml

glide install # 天生glide.lock,并拷贝依赖包

glide update # 更新依赖包信息,更新glide.lock

由于glide官方说我们不更新功能了,只bugfix,请大家开始利用dep吧,以是鉴于此,我们在选择中就放弃了。
同时,glide如果碰着依赖项目里有vendor的时候就直接跪了,dep的话,就会滤掉,不会再vendor下涌现嵌套的,全部放在当前项目的vendor下

dep

golang官方出品,dep最近的版本已经做好了从其他依赖工具的vendor迁移过来的功能,功能很强大,是我们目前的最佳选择。
不过目前还没有release1.0 ,但是已经可以用在天生环境中,对付新项目,我建议采取dep进行管理,不会有历史问题,而且当新项目上线的时候,dep也会进一步优化并且可能先于你的项目上线。

dep默认从github上拉取最新代码,如果想优先利用本地gopath,那么3.x版本的dep须要显式参数注明,如下

dep init -gopath -v

总结

godep是最初利用最多的,能够知足大部分需求,也比较稳定,但是有一些不太好的体验;glide 有版本管理,相对强大,但是官方表示不再进行开拓;dep是官方出品,目前没有release,功能同样强大,是目前最佳选择;

看官方的比拟

Golang随意马虎涌现的问题

包引用短缺导致panic

go vendor 缺失落导致import多次导致panic

本工程下没有vendor目录,然而,引入了这个包“github.com.xxx/demo/biz/model/impl/hash”, 这个biz包里面包含了vendor目录。

这样,编译此工程的时候,会导致一部分import是从oracle下的vendor,另一部分是从gopath,这样就会涌现一个包被两种不同办法import,导致涌现重复注册而panic

并发 导致 panic

fatal error: concurrent map read and map write

并发编程中最随意马虎涌现资源竞争,以前玩C++的时候,资源涌现竞争只会导致数据非常,不会导致程序非常panic,Golang里面会直接抛错,这个是比较好的做法,由于非常数据终极导致用户的数据非常,影响很大,乃至无法规复,直接抛错后交给开拓者去修复代码bug,一样平常在测试过程中或者代码review过程中就能够创造并发问题。

并发的处理方案有二:

通过chann 串行处理通过加锁掌握

相互依赖引用导致编译不过

Golang不许可包直接相互import,会导致编译不过。
但是有个项目里面,A同学卖力A模块,B同学卖力B模块,由于产品需求导致,A模块要调用B模块中供应的方法,B模块要调用A模块中供应的方法,这样就导致了相互引用了

我们的办理方案是: 将个中一个相互引用的模块中的方法提炼出来,独立为其余一个模块,也便是其余一个包,这样就不至于相互引用

Golang json类型转换非常

Golang进行json转换的时候,常用做法是一个定义struct,成员变量利用tag标签,然后通过自带的json包进行处理,随意马虎涌现的问题紧张有:

成员变量的首字母没有大写,导致json后天生不了对应字段json string的类型不对,导致json Unmarshal 的时候抛错

Golang 总结

golang利用一年多以来,个人认为golang有如下优点:

学习入门快;让开发者开拓更为简洁不用关心内存分配和开释,gc会帮我们处理;效率性能高;不用自己去实现一些根本数据构造,官方或者开源库可以直接import引用;struct 和 interface 可以实现类、继续等面向工具的操作模式;初始化和赋值变量简洁;并发编程goroutine非常随意马虎,结合chann可以很好的实现;Context能够自我掌握开始、停滞,通报高下文信息
标签:

相关文章

C采集php技巧_PHP采集 抓取

一、 什么是php采集程序?二、 为什么要采集?三、 采集些什么?四、 如何采集?五、 采集思路六、 采集范例程序七、 采集心得什...

Web前端 2024-12-08 阅读0 评论0