天天看點

用Go自己實作配置檔案熱加載功能

說到配置檔案熱加載,這個功能在很多架構中都提供了,如beego,實作的效果就是當你修改檔案後,會把你修改後的配置重新加載到配置檔案中,而不用重新開機程式,這個功能在日常中還是非常實用的,畢竟很多時候,線上的配置檔案不是想改就能改的。

這次就自己實作一個配置檔案的熱加載功能的包,并通過一個簡單的例子對完成的包進行使用驗證

配置檔案熱加載包的是實作

其實整體的思路還是比較簡單的,當擷取配置檔案内容後,會開啟一個goroutine,去 循環讀配置檔案,當然這裡不可能不限制的一直循環,而是設定了一個定時器,定時去讀檔案,根據檔案的修改時間是否變化,進而确定是否重新reload配置檔案

實作的config 包的檔案結構為:

├── config.go
└── config_notify.go      

config.go:代碼的主要處理邏輯

config_notify.go:主要定義了一個接口,用于當檔案修改時間變化的時候執行回調

config_notify.go的代碼相對來說比較簡單,我們先看看這個代碼:

package config

// 定義一個通知的接口
type Notifyer interface {
    Callback(*Config)
}      

這樣當我們實作了Callback這個方法的時候,我們就實作了Notifyer這個接口,具體的調用在後面會說

在config.go中我們頂一個了一個結構體:

type Config struct {
    filename string
    lastModifyTime int64
    data map[string]string
    rwLock sync.RWMutex
    notifyList []Notifyer
}      

結構體中主要包含幾個字段:

filename:配置檔案名字

lastModifyTime:配置檔案的最後修改時間

data:用于将從配置檔案中讀取的内容存儲為map

rwlock:讀寫鎖

notifyList:用于将調用該包的程式追加到切片中,用于通知調用上面在config_notify.go定義的callback回調函數

關于讀取配置檔案中的内容并存儲到map中,這裡定義了一個方法實作:

func (c *Config) parse()(m map[string]string,err error){
    // 讀檔案并或将檔案中的資料以k/v的形式存儲到map中
    m = make(map[string]string,1024)
    file,err := os.Open(c.filename)
    if err != nil{
        return
    }
    var lineNo int
    reader := bufio.NewReader(file)
    for{
        // 一行行的讀檔案
        line,errRet := reader.ReadString('\n')
        if errRet == io.EOF{
            // 表示讀到檔案的末尾
            break
        }
        if errRet != nil{
            // 表示讀檔案出問題
            err = errRet
            return
        }
        lineNo++
        line = strings.TrimSpace(line) // 取出空格
        if len(line) == 0 || line[0] == '\n' || line[0] == '+' || line[0] == ';'{
            // 目前行為空行或者是注釋行等
            continue
        }
        arr := strings.Split(line,"=") // 通過=進行切割取出k/v結構
        if len(arr) == 0{
            fmt.Printf("invalid config,line:%d\n",lineNo)
            continue
        }
        key := strings.TrimSpace(arr[0])
        if len(key) == 0{
            fmt.Printf("invalid config,line:%d\n",lineNo)
            continue
        }
        if len(arr) == 1{
            m[key] = ""
            continue
        }
        value := strings.TrimSpace(arr[1])
        m[key] = value
    }
    return
}      

而最後我們就需要一個定時器,每隔一段時間判斷配置檔案的最後修改時間是否變化,如果變化則重新讀取一次檔案并将檔案内容存儲到map中。

func (c *Config) reload(){
    // 這裡啟動一個定時器,每5秒重新加載一次配置檔案
    ticker := time.NewTicker(time.Second*5)
    for _ = range ticker.C{
        func(){
            file,err := os.Open(c.filename)
            if err != nil{
                fmt.Printf("open %s failed,err:%v\n",c.filename,err)
                return
            }
            defer file.Close()
            fileInfo,err := file.Stat()
            if err != nil{
                fmt.Printf("stat %s failed,err:%v\n",c.filename,err)
                return
            }
            curModifyTime := fileInfo.ModTime().Unix()
            fmt.Printf("%v --- %v\n",curModifyTime,c.lastModifyTime)
            //判斷檔案的修改時間是否大于最後一次修改時間
            if curModifyTime > c.lastModifyTime{
                m,err := c.parse()
                if err != nil{
                    fmt.Println("parse failed,err:",err)
                    return
                }
                c.rwLock.Lock()
                c.data = m
                c.rwLock.Unlock()
                for _, n:=range c.notifyList{
                    n.Callback(c)
                }
                c.lastModifyTime = curModifyTime
            }
        }()
    }
}      

關于config完整的代碼位址:https://github.com/pythonsite/go_simple_code/tree/master/config

一個示範上述包的例子

這裡一個簡單的例子,代碼的邏輯也非常簡單就是寫一個循環從配置檔案讀取配置資訊,當然這裡是為了測試效果,寫成了循環。這裡有個問題需要注意,就是在配置檔案中存放資料的時候應該是如下格式存儲

listen_addr = localhost
server_port = 1000

# Nginx addr
nginx_addr = 192.168.1.2:9090      

測試代碼的主要結構如下:

├── config.conf
└── main.go      

config.conf為配置檔案

main.go 為主要測試代碼

type AppConfig struct {
    port int
    nginxAddr string
}

type AppconfigMgr struct {
    config atomic.Value
}

var appConfigMgr = &AppconfigMgr{}


func(a *AppconfigMgr)Callback(conf *config.Config){

    var appConfig = &AppConfig{}

    port,err := conf.GetInt("server_port")
    if err != nil{
        fmt.Println("get port failed,err:",err)
        return
    }
    appConfig.port = port
    fmt.Println("port:",appConfig.port)
    nginxAddr,err := conf.GetString("nginx_addr")
    if err != nil{
        fmt.Println("get nginx addr failed,err:",err)
        return
    }
    appConfig.nginxAddr = nginxAddr
    fmt.Println("nginx addr :",appConfig.nginxAddr)

    appConfigMgr.config.Store(appConfig)

}

func run(){
    for {
        // 每5秒列印一次資料,檢視自己更改配置檔案後是否可以熱重新整理
        appConfig := appConfigMgr.config.Load().(*AppConfig)
        fmt.Println("port:",appConfig.port)
        fmt.Println("nginx addr:",appConfig.nginxAddr)
        time.Sleep(5* time.Second)
    }
}

func main() {
    conf,err := config.NewConfig("/Users/zhaofan/go_project/src/go_dev/13/config_test/config.conf")
    if err != nil{
        fmt.Println("parse config failed,err:",err)
        return
    }
    //打開檔案擷取内容後,将自己加入到被通知的切片中
    conf.AddNotifyer(appConfigMgr)

    var appConfig = &AppConfig{}

    appConfig.port,err = conf.GetInt("server_port")
    if err != nil{
        fmt.Println("get port failed,err:",err)
        return
    }
    fmt.Println("port:",appConfig.port)

    appConfig.nginxAddr,err = conf.GetString("nginx_addr")
    if err != nil{
        fmt.Println("get nginx addr failed,err:",err)
        return
    }
    fmt.Println("nginx addr:",appConfig.nginxAddr)
    appConfigMgr.config.Store(appConfig)
    run()

}      

上面代碼中有一段代碼非常重要:

func(a *AppconfigMgr)Callback(conf *config.Config){

    var appConfig = &AppConfig{}

    port,err := conf.GetInt("server_port")
    if err != nil{
        fmt.Println("get port failed,err:",err)
        return
    }
    appConfig.port = port
    fmt.Println("port:",appConfig.port)
    nginxAddr,err := conf.GetString("nginx_addr")
    if err != nil{
        fmt.Println("get nginx addr failed,err:",err)
        return
    }
    appConfig.nginxAddr = nginxAddr
    fmt.Println("nginx addr :",appConfig.nginxAddr)

    appConfigMgr.config.Store(appConfig)

}      

這裡我們實作了Callback方法,同時就實作了我們在config包中定義的那個接口

測試效果如下,當我們更改配置檔案後,程式中的配置檔案也被重新加載

用Go自己實作配置檔案熱加載功能

完整的測試代碼位址:https://github.com/pythonsite/go_simple_code/tree/master/config_test

所有的努力都值得期許,每一份夢想都應該灌溉!