Golang工程經驗

吳德寶AllenWu發表於2018-01-28

[TOC]

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等都已包含在內

  1. list連結串列結構,當我們需要類似佇列的結構的時候,可以採用,針對IM系統而言,在長連線層處理的訊息id的列表,可以通過list來維護,如果使用者有了回應則從list裡面移除,否則在超時時間到後還沒有回應,則入offline處理

  2. mutex鎖,當需要併發讀寫某個資料的時候使用,包含互斥鎖和讀寫鎖

    var ackWaitListMutex sync.RWMutex
    var ackWaitListMutex sync.Mutex
    複製程式碼
  3. 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會保證一定只執行一次

  4. 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(n*log(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 2*ceil(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程式設計的同學對這個模型應該是再熟悉不過了,我這邊通過程式碼來說明下這個模型

  1. 首先一個model包,通過interface來實現,包含一些基礎方法,需要被外部引用者來具體實現

    package model
    
    // 定義一個基礎model
    type MsgModel interface {
    	Persist(context context.Context, msg interface{}) bool
    	UpdateDbContent(context context.Context, msgIface interface{}) bool
        ...
    }
    複製程式碼
  2. 再定義一個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 {
        // 具體實現
    
    }
    
    ...
    
    複製程式碼
  3. 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,
    	}
    )	
    
    複製程式碼
  4. 呼叫訪問

    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
}


複製程式碼

總結一下:

  1. 使用連線池提高效能,每次都從連線池裡面取連線而不是每次都重新建立連線
  2. 設定最大連線數和最大活躍連線(同一時刻能夠提供的連線),設定合理的讀寫超時時間
  3. 實現主從讀寫分離,提高效能,需要注意如果沒有從庫則只讀主庫
  4. TestOnBorrow用來進行健康檢測
  5. 單獨開一個goroutine協程用來定期保活【ping-pong】
  6. hash分片演算法的選擇,一致性hash還是hash取模,hash取模在擴縮容的時候比較方便,一致性hash並沒有帶來明顯的優勢,我們公司內部統一建議採用hash取模
  7. 考慮如何支援雙寫策略

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用例,假如我們只想測試某一個方法的時候,那麼我們需要指定某個檔案的某個方案

如下:

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 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 ${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過程中就能夠發現併發問題。

併發的處理方案有二:

  1. 通過chann 序列處理
  2. 通過加鎖控制

相互依賴引用導致編譯不過

Golang不允許包直接相互import,會導致編譯不過。但是有個專案裡面,A同學負責A模組,B同學負責B模組,由於產品需求導致,A模組要呼叫B模組中提供的方法,B模組要呼叫A模組中提供的方法,這樣就導致了相互引用了

我們的解決方案是: 將其中一個相互引用的模組中的方法提煉出來,獨立為另外一個模組,也就是另外一個包,這樣就不至於相互引用

Golang json型別轉換異常

Golang進行json轉換的時候,常用做法是一個定義struct,成員變數使用tag標籤,然後通過自帶的json包進行處理,容易出現的問題主要有:

  1. 成員變數的首字母沒有大寫,導致json後生成不了對應欄位
  2. json string的型別不對,導致json Unmarshal 的時候拋錯

Golang 總結

golang使用一年多以來,個人認為golang有如下優點:

  • 學習入門快;讓開發者開發更為簡潔
  • 不用關心記憶體分配和釋放,gc會幫我們處理;
  • 效率效能高;
  • 不用自己去實現一些基礎資料結構,官方或者開源庫可以直接import引用;
  • struct 和 interface 可以實現類、繼承等物件導向的操作模式;
  • 初始化和賦值變數簡潔;
  • 併發程式設計goroutine非常容易,結合chann可以很好的實現;
  • Context能夠自我控制開始、停止,傳遞上下文資訊

相關文章