[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)
-
list鏈表結構,,當我們需要類似隊列的結構的時候,可以采用,,針對IM系統(tǒng)而言,,在長連接層處理的消息id的列表,可以通過list來維護,,如果用戶有了回應則從list里面移除,,否則在超時時間到后還沒有回應,則入offline處理
-
mutex鎖,,當需要并發(fā)讀寫某個數(shù)據(jù)的時候使用,,包含互斥鎖和讀寫鎖
var ackWaitListMutex sync.RWMutex
var ackWaitListMutex sync.Mutex
復制代碼
-
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í)行一次
-
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編程的同學對這個模型應該是再熟悉不過了,我這邊通過代碼來說明下這個模型
-
首先一個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
...
}
復制代碼
-
再定義一個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)
}
...
復制代碼
-
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,
}
)
復制代碼
-
調(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
}
復制代碼
總結一下:
- 使用連接池提高性能,,每次都從連接池里面取連接而不是每次都重新建立連接
- 設置最大連接數(shù)和最大活躍連接(同一時刻能夠提供的連接),設置合理的讀寫超時時間
- 實現(xiàn)主從讀寫分離,,提高性能,,需要注意如果沒有從庫則只讀主庫
- TestOnBorrow用來進行健康檢測
- 單獨開一個goroutine協(xié)程用來定期?;睢緋ing-pong】
- hash分片算法的選擇,,一致性hash還是hash取模,hash取模在擴縮容的時候比較方便,,一致性hash并沒有帶來明顯的優(yōu)勢,,我們公司內(nèi)部統(tǒng)一建議采用hash取模
- 考慮如何支持雙寫策略
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ā)的處理方案有二:
- 通過chann 串行處理
- 通過加鎖控制
相互依賴引用導致編譯不過
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)的問題主要有:
- 成員變量的首字母沒有大寫,導致json后生成不了對應字段
- 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能夠自我控制開始,、停止,,傳遞上下文信息
|