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 callers. Callers use this structure to interact with Redis database
func 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 main
import (
“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 = 6
func 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 switches
var (
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 prometheus
import “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/m
trace-middleware-go/grpc/client.go:49
一般panic,都會有上述錯誤日志,然後通過日志,可以追蹤到具體函數,然後看到OpenTracingClientInterceptor後,是在client.go的49行,然後開始反推,通過代碼可以看到,可能是trace指針為空。然後一步一步看是從哪裡開始調用的
最終發現代碼如下:
ucConn, err := grpcclient.NewClientConn(conf.Discovery.UserCenter, newBalancer, time.Second*3, 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.go
func 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.go
package user
import (
“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用例,假如我們隻想測試某一個方法的時候,那麼我們需要指定某個檔案的某個方案
如下:
[email protected]:~/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 centralhub
import (
“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 TestSendTimerInvite
go 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 r e p o / v e r s i o n . v e r s i o n = {repo}/version.version= repo/version.version={version}
-X r e p o / v e r s i o n . b r a n c h = {repo}/version.branch= repo/version.branch={branch}
-X r e p o / v e r s i o n . g o V e r s i o n = {repo}/version.goVersion= repo/version.goVersion={go_version}
-X r e p o / v e r s i o n . b u i l d T i m e = {repo}/version.buildTime= repo/version.buildTime={build_time}
-X r e p o / v e r s i o n . b u i l d U s e r = {repo}/version.buildUser= repo/version.buildUser={build_user}
"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags “${ldflags}” -o b i n a r y d i r / binary_dir/ binarydir/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能夠自我控制開始、停止,傳遞上下文資訊