久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

Golang工程經(jīng)驗

 紫火神兵 2019-03-05

[TOC]

作為一個C/C++的開發(fā)者而言,,開啟Golang語言開發(fā)之路是很容易的,,從語法,、語義上的理解到工程開發(fā),,都能夠快速熟悉起來;相比C,、C++,,Golang語言更簡潔,更容易寫出高并發(fā)的服務后臺系統(tǒng)

轉戰(zhàn)Golang一年有余,,經(jīng)歷了兩個線上項目的洗禮,,總結出一些工程經(jīng)驗,一個是總結出一些實戰(zhàn)經(jīng)驗,,一個是用來發(fā)現(xiàn)自我不足之處

Golang語言簡介

Go語言是谷歌推出的一種全新的編程語言,,可以在不損失應用程序性能的情況下降低代碼的復雜性。Go語言專門針對多處理器系統(tǒng)應用程序的編程進行了優(yōu)化,,使用Go編譯的程序可以媲美C或C++代碼的速度,,而且更加安全、支持并行進程,。

基于Golang的IM系統(tǒng)架構

我基于Golang的兩個實際線上項目都是IM系統(tǒng),,本文基于現(xiàn)有線上系統(tǒng)做一些總結性,、引導性的經(jīng)驗輸出。

Golang TCP長連接 & 并發(fā)

既然是IM系統(tǒng),,那么必然需要TCP長連接來維持,,由于Golang本身的基礎庫和外部依賴庫非常之多,我們可以簡單引用基礎net網(wǎng)絡庫,,來建立TCP server,。一般的TCP Server端的模型,可以有一個協(xié)程【或者線程】去獨立執(zhí)行accept,,并且是for循環(huán)一直accept新的連接,,如果有新連接過來,那么建立連接并且執(zhí)行Connect,,由于Golang里面協(xié)程的開銷非常之小,,因此,,TCP server端還可以一個連接一個goroutine去循環(huán)讀取各自連接鏈路上的數(shù)據(jù)并處理,。當然, 這個在C++語言的TCP Server模型中,,一般會通過EPoll模型來建立server端,,這個是和C++的區(qū)別之處。

關于讀取數(shù)據(jù),,Linux系統(tǒng)有recv和send函數(shù)來讀取發(fā)送數(shù)據(jù),,在Golang中,自帶有io庫,,里面封裝了各種讀寫方法,,如io.ReadFull,它會讀取指定字節(jié)長度的數(shù)據(jù)

為了維護連接和用戶,,并且一個連接一個用戶的一一對應的,,需要根據(jù)連接能夠找到用戶,同時也需要能夠根據(jù)用戶找到對應的連接,,那么就需要設計一個很好結構來維護,。我們最初采用map來管理,但是發(fā)現(xiàn)Map里面的數(shù)據(jù)太大,,查找的性能不高,,為此,優(yōu)化了數(shù)據(jù)結構,,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 // 為每個連接創(chuàng)建一個唯一標識符
	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 {
			
			// 包計數(shù),,用來限制頻率
			
			//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庫里面提供的方法讀取數(shù)據(jù),如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端的數(shù)據(jù),。如果是C、C++程序,,如果一個連接一個線程的話,,如果上萬個或者十萬個線程,那么性能會極低甚至于無法工作,,cpu會全部消耗在線程之間的調(diào)度上了,,因此C、C++程序無法這樣玩,。Golang的話,,goroutine可以幾十萬、幾百萬的在一個系統(tǒng)中良好運行,。同時對于TCP長連接而言,,一個節(jié)點上的連接數(shù)要有限制策略。

連接超時

每個連接需要有心跳來維持,,在心跳間隔時間內(nèi)沒有收到,,服務端要檢測超時并斷開連接釋放資源,golang可以很方便的引用需要的數(shù)據(jù)結構,,同時對變量的賦值(包括指針)非常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 基礎數(shù)據(jù)結構

Golang中,很多基礎數(shù)據(jù)都通過庫來引用,,我們可以方便引用我們所需要的庫,,通過import包含就能直接使用,如源碼里面提供了sync庫,,里面有mutex鎖,,在需要鎖的時候可以包含進來

常用的如list,mutex,once,,singleton等都已包含在內(nèi)

  1. list鏈表結構,,當我們需要類似隊列的結構的時候,可以采用,,針對IM系統(tǒng)而言,,在長連接層處理的消息id的列表,可以通過list來維護,,如果用戶有了回應則從list里面移除,,否則在超時時間到后還沒有回應,則入offline處理

  2. mutex鎖,,當需要并發(fā)讀寫某個數(shù)據(jù)的時候使用,,包含互斥鎖和讀寫鎖

    var ackWaitListMutex sync.RWMutex
    var ackWaitListMutex sync.Mutex
    復制代碼
  3. once表示任何時刻都只會調(diào)用一次,一般的用法是初始化實例的時候使用,,代碼片段如下

    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),
    		}),
    		...
    	}
    }
    復制代碼

    這樣我們可以在任何需要的地方調(diào)用GetRedisCluster,,并且不用擔心實例會被初始化多次,once會保證一定只執(zhí)行一次

  4. singleton單例模式,,這個在C++里面是一個常用的模式,,一般需要開發(fā)者自己通過類來實現(xiàn),類的定義決定單例模式設計的好壞,;在Golang中,,已經(jīng)有成熟的庫實現(xiàn)了,,開發(fā)者無須重復造輪子,,關于什么時候該使用單例模式請自行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并發(fā)的兩大基石,,那么接口interface是Go語言編程中數(shù)據(jù)類型的關鍵,。在Go語言的實際編程中,幾乎所有的數(shù)據(jù)結構都圍繞接口展開,,接口是Go語言中所有數(shù)據(jù)結構的核心,。

interface - 泛型編程

嚴格來說,在 Golang 中并不支持泛型編程,。在 C++ 等高級語言中使用泛型編程非常的簡單,,所以泛型編程一直是 Golang 詬病最多的地方。但是使用 interface 我們可以實現(xiàn)泛型編程,,如下是一個參考示例

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 函數(shù)的形參是一個 interface,,包含了三個方法:Len(),Less(i,j int),,Swap(i, j int),。使用的時候不管數(shù)組的元素類型是什么類型(int, float, string…),只要我們實現(xiàn)了這三個方法就可以使用 Sort 函數(shù),這樣就實現(xiàn)了“泛型編程”,。

這種方式,,我在項目里面也有實際應用過,具體案例就是對消息排序,。

下面給一個具體示例,,代碼能夠說明一切,一看就懂:

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 - 隱藏具體實現(xiàn)

隱藏具體實現(xiàn),,這個很好理解,。比如我設計一個函數(shù)給你返回一個 interface,那么你只能通過 interface 里面的方法來做一些操作,,但是內(nèi)部的具體實現(xiàn)是完全不知道的,。

例如我們常用的context包,就是這樣的,,context 最先由 google 提供,,現(xiàn)在已經(jīng)納入了標準庫,而且在原有 context 的基礎上增加了:cancelCtx,,timerCtx,,valueCtx。

如果函數(shù)參數(shù)是interface或者返回值是interface,,這樣就可以接受任何類型的參數(shù)

基于Golang的model service 模型【類MVC模型】

在一個項目工程中,,為了使得代碼更優(yōu)雅,需要抽象出一些模型出來,,同時基于C++面向?qū)ο缶幊痰乃枷?,需要考慮到一些類、繼承相關,。在Golang中,,沒有類、繼承的概念,,但是我們完全可以通過struct和interface來建立我們想要的任何模型,。在我們的工程中,抽象出一種我自認為是類似MVC的模型,,但是不完全一樣,,個人覺得這個模型抽象的比較好,容易擴展,,模塊清晰,。對于使用java和PHP編程的同學對這個模型應該是再熟悉不過了,我這邊通過代碼來說明下這個模型

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

    package model
    
    // 定義一個基礎model
    type MsgModel interface {
    	Persist(context context.Context, msg interface{}) bool
    	UpdateDbContent(context context.Context, msgIface interface{}) bool
        ...
    }
    復制代碼
  2. 再定義一個msg包,用來具體實現(xiàn)model包中MsgModel模型的所有方法

    package msg
    
    type msgModelImpl struct{}
    
    var MsgModelImpl = msgModelImpl{}
    
    func (m msgModelImpl) Persist(context context.Context, msgIface interface{}) bool {
    	 // 具體實現(xiàn)
    }
    
    func (m msgModelImpl) UpdateDbContent(context context.Context, msgIface interface{}) bool {
        // 具體實現(xiàn)
    
    }
    
    ...
    
    復制代碼
  3. model 和 具體實現(xiàn)方定義并實現(xiàn)ok后,,那么就還需要一個service來統(tǒng)籌管理

    package service
    
    // 定義一個msgService struct包含了model里面的UserModel和MsgModel兩個model
    type msgService struct {
    	msgModel   model.MsgModel
    }
    
    // 定義一個MsgService的變量,,并初始化,這樣通過MsgService,,就能引用并訪問model的所有方法
    var (
        MsgService = msgService{
    		msgModel:       msg.MsgModelImpl,
    	}
    )	
    
    復制代碼
  4. 調(diào)用訪問

    import service
    
    service.MsgService.Persist(ctx, xxx)
    
    復制代碼

總結一下,,model對應MVC的M,service 對應 MVC的C,, 調(diào)用訪問的地方對應MVC的V

Golang 基礎資源的封裝

在MVC模型的基礎下,,我們還需要考慮另外一點,就是基礎資源的封裝,,服務端操作必然會和mysql,、redis、memcache等交互,,一些常用的底層基礎資源,,我們有必要進行封裝,這是基礎架構部門所需要承擔的,,也是一個好的項目工程所需要的

redis

redis,,我們在github.com/garyburd/redigo/redis的庫的基礎上,做了一層封裝,,實現(xiàn)了一些更為貼合工程的機制和接口,,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. 設置最大連接數(shù)和最大活躍連接(同一時刻能夠提供的連接),設置合理的讀寫超時時間
  3. 實現(xiàn)主從讀寫分離,,提高性能,,需要注意如果沒有從庫則只讀主庫
  4. TestOnBorrow用來進行健康檢測
  5. 單獨開一個goroutine協(xié)程用來定期?;睢緋ing-pong】
  6. hash分片算法的選擇,,一致性hash還是hash取模,hash取模在擴縮容的時候比較方便,,一致性hash并沒有帶來明顯的優(yōu)勢,,我們公司內(nèi)部統(tǒng)一建議采用hash取模
  7. 考慮如何支持雙寫策略

memcache

memcached客戶端代碼封裝,依賴 github.com/dropbox/godropbox/memcache, 實現(xiàn)其ShardManager接口,,支持Connection Timeout,,支持Fail Fast和Rehash

goroutine & chann

實際開發(fā)過程中,經(jīng)常會有這樣場景,,每個請求通過一個goroutine協(xié)程去做,,如批量獲取消息,但是,為了防止后端資源連接數(shù)太多等,,或者防止goroutine太多,,往往需要限制并發(fā)數(shù)。給出如下示例供參考

package main

import (
    "fmt"
    "sync"
    "time"
)

var over = make(chan bool)

const MAXConCurrency = 3

//var sem = make(chan int, 4) //控制并發(fā)任務數(shù)
var sem = make(chan bool, MAXConCurrency) //控制并發(fā)任務數(shù)

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分析

這里想要說明的是,,在項目工程中,我們經(jīng)常會用到這樣的一個場景,,通過goroutine并發(fā)去處理某些批量任務,,當某個條件觸發(fā)的時候,這些goroutine要能夠控制停止執(zhí)行,。如果有這樣的場景,,那么咱們就需要用到context的With 系列函數(shù)了,context.WithCancel生成了一個withCancel的實例以及一個cancelFuc,,這個函數(shù)就是用來關閉ctxWithCancel中的 Done channel 函數(shù),。

示例代碼片段如下

func Example(){

  // context.WithCancel 用來生成一個新的Context,可以接受cancel方法用來隨時停止執(zhí)行
	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()  // 當條件觸發(fā),,則調(diào)用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上下文存在于一整條調(diào)用鏈路中,在服務端并發(fā)場景下,,n多個請求里面,,我們?nèi)绾文軌蚩焖贉蚀_的找到一條請求的來龍去脈,專業(yè)用語就是指調(diào)用鏈路,,通過調(diào)用鏈我們能夠知道這條請求經(jīng)過了哪些服務,、哪些模塊、哪些方法,,這樣可以非常方便我們定位問題

traceid就是我們抽象出來的這樣一個調(diào)用鏈的唯一標識,,再通過Context進行傳遞,在任何代碼模塊[函數(shù),、方法]里面都包含Context參數(shù),,我們就能形成一個完整的調(diào)用鏈。那么如何實現(xiàn)呢 ,?在我們的工程中,,有RPC模塊,有HTTP模塊,,兩個模塊的請求來源肯定不一樣,,因此,,要實現(xiàn)所有服務和模塊的完整調(diào)用鏈,需要考慮http和rpc兩個不同的網(wǎng)絡請求的調(diào)用鏈

traceid的實現(xiàn)

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來實現(xiàn)的話,,代碼片段如下,,其他http server的庫的實現(xiàn)方式類似即可


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 省略實現(xiàn)
			}
		}()
		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,響應碼,,用來記錄并可以統(tǒng)計服務的響應情況,,當然,,也有其他輔助系統(tǒng)如SLA來專門記錄http的響應情況

Golang語言實現(xiàn)這個也非常簡單,而且這個是個通用功能,建議可以抽象為一個基礎模塊,,所有業(yè)務都能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}",

具體實現(xiàn)省略

復制代碼

最終得到的日志如下:

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 -
復制代碼

開關策略,、降級策略

線上服務端系統(tǒng),,必須要有降級機制,,也最好能夠有開關機制。降級機制在于出現(xiàn)異常情況能夠舍棄某部分服務保證其他主線服務正常,;開關也有著同樣的功效,,在某些情況下打開開關,,則能夠執(zhí)行某些功能或者說某套功能,關閉開關則執(zhí)行另外一套功能或者不執(zhí)行某個功能,。

這不是Golang的語言特性,,但是是工程項目里面必要的,在Golang項目中的具體實現(xiàn)代碼片段如下:

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)
}


// 具體實現(xiàn)結構和實現(xiàn)方法
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)
}


// 這里開始調(diào)用
if switches.AsyncProcedure.IsOn() {
    // do sth
}else{
    // do other sth
}

復制代碼

prometheus + grafana

prometheus + grafana 是業(yè)界常用的監(jiān)控方案,prometheus進行數(shù)據(jù)采集,,grafana進行圖表展示,。

Golang里面prometheus進行數(shù)據(jù)采集非常簡單,有對應client庫,,應用程序只需暴露出http接口即可,,這樣,,prometheus server端就可以定期采集數(shù)據(jù),,并且還可以根據(jù)這個接口來監(jiān)控服務端是否異?!救鐠斓舻那闆r】,。

import 	"github.com/prometheus/client_golang/prometheus"

engine.GET("/metrics", gin.WrapH(prometheus.Handler()))

復制代碼

這樣就實現(xiàn)了數(shù)據(jù)采集,,但是具體采集什么樣的數(shù)據(jù),,數(shù)據(jù)從哪里生成的,,還需要進入下一步:

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)
	...
}


// 使用,在對應的位置調(diào)用prometheus接口生成數(shù)據(jù)
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 異常

捕獲異常是否有存在的必要,,根據(jù)各自不同的項目自行決定,,但是一般出現(xiàn)panic,,如果沒有異常,,那么服務就會直接掛掉,如果能夠捕獲異常,那么出現(xiàn)panic的時候,,服務不會掛掉,,只是當前導致panic的某個功能,無法正常使用,,個人建議還是在某些有必要的條件和入口處進行異常捕獲,。

常見拋出異常的情況:數(shù)組越界,、空指針空對象,,類型斷言失敗等,;Golang里面捕獲異常通過 defer + recover來實現(xiàn)

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 {
				// 異常捕獲的處理
			}
		}
	}()
}

復制代碼

在請求來源入口處的函數(shù)或者某個方法里面實現(xiàn)這么一段代碼進行捕獲,,這樣,只要通過這個入口出現(xiàn)的異常都能被捕獲,,并打印詳細日志

error 錯誤

error錯誤,,可以自定義返回,,一般工程應用中的做法,會在方法的返回值上增加一個error返回值,,Golang允許每個函數(shù)返回多個返回值,,增加一個error的作用在于,獲取函數(shù)返回值的時候,,根據(jù)error參數(shù)進行判斷,,如果是nil表示沒有錯誤,正常處理,,否則處理錯誤邏輯,。這樣減少代碼出現(xiàn)異常情況

panic 拋出的堆棧信息排查

如果某些情況下,沒有捕獲異常,,程序在運行過程中出現(xiàn)panic,,一般都會有一些堆棧信息,我們?nèi)绾胃鶕?jù)這些堆棧信息快速定位并解決呢 ,?

一般信息里面都會表明是哪種類似的panic,,如是空指針異常還是數(shù)組越界,還是xxx,;

然后會打印一堆信息出來包括出現(xiàn)異常的代碼調(diào)用塊及其文件位置,需要定位到最后的位置然后反推上去

分析示例如下

{"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”,,也就是空指針異常,然后逐步定位日志,,可以發(fā)現(xiàn)最終導致出現(xiàn)異常的函數(shù)在這個,,如下:

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,都會有上述錯誤日志,,然后通過日志,,可以追蹤到具體函數(shù),然后看到OpenTracingClientInterceptor后,,是在client.go的49行,,然后開始反推,通過代碼可以看到,,可能是trace指針為空,。然后一步一步看是從哪里開始調(diào)用的

最終發(fā)現(xiàn)代碼如下:

	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是不是可能為空,,在哪里初始化,,初始化有沒有錯,然后發(fā)現(xiàn)這個函數(shù)是在init里面,,然后conf.Tracer確實在main函數(shù)里面顯示調(diào)用的,,main函數(shù)里面會引用或者間接引用所有包,,那么init就一定在main之前執(zhí)行。

這樣的話,,init執(zhí)行的時候,,conf.Tracer還沒有被賦值,因此就是nil,,就會導致panic了

項目工程級別接口

項目中如果能夠有一些調(diào)試debug接口,,有一些pprof性能分析接口,有探測,、健康檢查接口的話,,會給整個項目在線上穩(wěn)定運行帶來很大的作用。 除了pprof性能分析接口屬于Golang特有,,其他的接口在任何語言都有,,這里只是表明在一個工程中,需要有這類型的接口

上下線接口

我們的工程是通過etcd進行服務發(fā)現(xiàn)和注冊的,,同時還提供http服務,,那么就需要有個機制來上下線,這樣上線過程中,,如果服務本身還沒有全部啟動完成準備就緒,,那么就暫時不要在etcd里面注冊,不要上線,,以免有請求過來,,等到就緒后再注冊;下線過程中,,先從etcd里面移除,,這樣流量不再導入過來,然后再等待一段時間用來處理還未完成的任務

我們的做法是,,start 和 stop 服務的時候,,調(diào)用API接口,然后再在服務的API接口里面注冊和反注冊到etcd


    var OnlineHook = func() error {
    	return nil
    }
    
    var OfflineHook = func() error {
    	return nil
    }


   // 初始化兩個函數(shù),,注冊和反注冊到etcd的函數(shù)
	api.OnlineHook = func() error {
		return registry.Register(conf.Discovery.RegisterAddress)
	}

	api.OfflineHook = func() error {
		return registry.Deregister()
	}
	
	
	// 設置在線的函數(shù)里面分別調(diào)用上述兩個函數(shù),,用來上下線
    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接口調(diào)用的函數(shù)

復制代碼

nginx 探測接口,健康檢查接口

對于http的服務,,一般訪問都通過域名訪問,,nginx配置代理,這樣保證服務可以隨意擴縮容,,但是nginx既然配置了代碼,,后端節(jié)點的情況,就必須要能夠有接口可以探測,,這樣才能保證流量導入到的節(jié)點一定的在健康運行中的節(jié)點,;為此,,服務必須要提供健康檢測的接口,這樣才能方便nginx代理能夠?qū)崟r更新節(jié)點,。

這個接口如何實現(xiàn),?nginx代理一般通過http code來處理,如果返回code=200,,認為節(jié)點正常,,如果是非200,認為節(jié)點異常,,如果連續(xù)采樣多次都返回異常,,那么nginx將節(jié)點下掉

如提供一個/devops/status 的接口,用來檢測,,接口對應的具體實現(xiàn)為:

func CheckHealth(c *gin.Context) {
   // 首先狀態(tài)碼設置為非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調(diào)試接口

	// 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)
	})
復制代碼

開關【降級】實時調(diào)整接口

前面有講到過,,在代碼里面需要有開關和降級機制,并講了實現(xiàn)示例,,那么如果需要能夠?qū)崟r改變開關狀態(tài),,并且實時生效,我們就可以提供一下http的API接口,,供運維人員或者開發(fā)人員使用,。

	// 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 包,,然后執(zhí)行go test,,就能夠測試某個模塊代碼

如,在某個user文件夾下有個user包,,包文件為user.go,,里面有個Func UpdateThemesCounts,如果想要進行test,,那么在同級目錄下,,建立一個user_test.go的文件,包含testing包,,編寫test用例,,然后調(diào)用go test即可

一般的規(guī)范有:

  • 每個測試函數(shù)必須導入testing包
  • 測試函數(shù)的名字必須以Test開頭,可選的后綴名必須以大寫字母開頭
  • 將測試文件和源碼放在相同目錄下,并將名字命名為{source_filename}_test.go
  • 通常情況下,,將測試文件和源碼放在同一個包內(nèi),。

如下:


// 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)
}

在此目錄下執(zhí)行 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 

復制代碼

測試單個文件下的單個方法

在測試單個文件之下,,假如我們單個文件下,,有多個方法,我們還想只是測試單個文件下的單個方法,,要如何實現(xiàn),?我們需要再在此基礎上,用 -run 參數(shù)指定具體方法或者使用正則表達式,。

假如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工具給我們提供了測試覆蓋度的參數(shù),,

go test -v -cover

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

go tool cover -func=cover.out

goalng GC 、編譯運行

服務端開發(fā)者如果在mac上開發(fā),,那么Golang工程的代碼可以直接在mac上編譯運行,,然后如果需要部署在Linux系統(tǒng)的時候,在編譯參數(shù)里面指定GOOS即可,,這樣可以本地調(diào)試ok后再部署到Linux服務器,。

如果要部署到Linux服務,編譯參數(shù)的指定為

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,,我們要收集起來,,記錄到日志文件中,這樣方便后續(xù)排查和定位,,啟動的時候指定一下即可執(zhí)行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你的代碼是內(nèi)部還是外部的,總之都在GOPATH里,,任何import包的路徑都是從GOPATH開始的,;唯一的區(qū)別,就是內(nèi)部依賴的包是開發(fā)者自己寫的,,外部依賴的包是go get下來的,。

依賴GOPATH來解決go import有個很嚴重的問題:如果項目依賴的包做了修改,或者干脆刪掉了,,會影響到其他現(xiàn)有的項目,。為了解決這個問題,go在1.5版本引入了vendor屬性(默認關閉,,需要設置go環(huán)境變量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;

但是后面隨著我們使用和項目的進一步加強,,我們發(fā)現(xiàn)godep有諸多痛點,,目前已經(jīng)逐步開始棄用godep,新項目都開始采用dep進行管理了,。

godep的痛點:

  • godep如果遇到依賴項目里有vendor的時候就可能會導致編譯不過,,vendor下再嵌套vendor,就會導致編譯的時候出現(xiàn)版本不一致的錯誤,,會提示某個方法接口不對,,全部放在當前項目的vendor下

  • godep鎖定版本太麻煩了,,在項目進一步發(fā)展過程中,我們依賴的項目(包)可能是早期的,,后面由于升級更新,,某些API接口可能有變;但是我們項目如果已經(jīng)上線穩(wěn)定運行,,我們不想用新版,,那么就需要鎖定某個特定版本。但是這個對于godep而言,,操作著實不方便,。

  • godep的時候,經(jīng)常會有一些包需要特定版本,,然后包依賴編譯不過,,尤其是在多人協(xié)作的時候,本地gopath和vendor下的版本不一樣,,然后本地gopath和別人的gopath的版本不一樣,,導致編譯會遇到各種依賴導致的問題

glide

glide也是在vendor之后出來的。glide的依賴包信息在glide.yaml和glide.lock中,,前者記錄了所有依賴的包,,后者記錄了依賴包的版本信息

glide create # 創(chuàng)建glide工程,生成glide.yaml glide install # 生成glide.lock,,并拷貝依賴包 glide update # 更新依賴包信息,,更新glide.lock

因為glide官方說我們不更新功能了,只bugfix,,請大家開始使用dep吧,,所以鑒于此,我們在選擇中就放棄了,。同時,,glide如果遇到依賴項目里有vendor的時候就直接跪了,dep的話,,就會濾掉,,不會再vendor下出現(xiàn)嵌套的,全部放在當前項目的vendor下

dep

golang官方出品,,dep最近的版本已經(jīng)做好了從其他依賴工具的vendor遷移過來的功能,,功能很強大,是我們目前的最佳選擇,。不過目前還沒有release1.0 ,,但是已經(jīng)可以用在生成環(huán)境中,對于新項目,我建議采用dep進行管理,,不會有歷史問題,,而且當新項目上線的時候,dep也會進一步優(yōu)化并且可能先于你的項目上線,。

dep默認從github上拉取最新代碼,,如果想優(yōu)先使用本地gopath,那么3.x版本的dep需要顯式參數(shù)注明,,如下

dep init -gopath -v
復制代碼

總結

  • godep是最初使用最多的,,能夠滿足大部分需求,也比較穩(wěn)定,,但是有一些不太好的體驗,;

  • glide 有版本管理,相對強大,,但是官方表示不再進行開發(fā),;

  • dep是官方出品,目前沒有release,,功能同樣強大,,是目前最佳選擇;

看官方的對比

Golang容易出現(xiàn)的問題

包引用缺少導致panic

go vendor 缺失導致import多次導致panic

本工程下沒有vendor目錄,,然而,,引入了這個包“github.com.xxx/demo/biz/model/impl/hash”, 這個biz包里面包含了vendor目錄,。

這樣,,編譯此工程的時候,會導致一部分import是從oracle下的vendor,,另一部分是從gopath,,這樣就會出現(xiàn)一個包被兩種不同方式import,導致出現(xiàn)重復注冊而panic

并發(fā) 導致 panic

fatal error: concurrent map read and map write

并發(fā)編程中最容易出現(xiàn)資源競爭,,以前玩C++的時候,,資源出現(xiàn)競爭只會導致數(shù)據(jù)異常,不會導致程序異常panic,,Golang里面會直接拋錯,,這個是比較好的做法,因為異常數(shù)據(jù)最終導致用戶的數(shù)據(jù)異常,,影響很大,,甚至無法恢復,直接拋錯后交給開發(fā)者去修復代碼bug,,一般在測試過程中或者代碼review過程中就能夠發(fā)現(xiàn)并發(fā)問題。

并發(fā)的處理方案有二:

  1. 通過chann 串行處理
  2. 通過加鎖控制

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

Golang不允許包直接相互import,,會導致編譯不過,。但是有個項目里面,,A同學負責A模塊,B同學負責B模塊,,由于產(chǎn)品需求導致,,A模塊要調(diào)用B模塊中提供的方法,B模塊要調(diào)用A模塊中提供的方法,,這樣就導致了相互引用了

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

Golang json類型轉換異常

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

  1. 成員變量的首字母沒有大寫,導致json后生成不了對應字段
  2. json string的類型不對,,導致json Unmarshal 的時候拋錯

Golang 總結

golang使用一年多以來,,個人認為golang有如下優(yōu)點:

  • 學習入門快;讓開發(fā)者開發(fā)更為簡潔
  • 不用關心內(nèi)存分配和釋放,,gc會幫我們處理,;
  • 效率性能高;
  • 不用自己去實現(xiàn)一些基礎數(shù)據(jù)結構,,官方或者開源庫可以直接import引用,;
  • struct 和 interface 可以實現(xiàn)類、繼承等面向?qū)ο蟮牟僮髂J剑?/li>
  • 初始化和賦值變量簡潔,;
  • 并發(fā)編程goroutine非常容易,,結合chann可以很好的實現(xiàn);
  • Context能夠自我控制開始,、停止,,傳遞上下文信息

    本站是提供個人知識管理的網(wǎng)絡存儲空間,所有內(nèi)容均由用戶發(fā)布,,不代表本站觀點,。請注意甄別內(nèi)容中的聯(lián)系方式、誘導購買等信息,,謹防詐騙,。如發(fā)現(xiàn)有害或侵權內(nèi)容,請點擊一鍵舉報,。
    轉藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多