写在前面
beego 是一个快速开发 Go 应用的 HTTP 框架,他可以用来快速开发 API、Web 及后端服务等各种应用,是一个 RESTful 的框架,主要设计灵感来源于 tornado、sinatra 和 flask 这三个框架,但是结合了 Go 本身的一些特性(interface、struct 嵌入等)而设计的一个框架;MVC架构如下:具体内容会在后面部分给出,先从Config入口
关于config
首先要搞清楚一点Config在整个beego框架承担的任务:提供Ini、yaml、xml、json、env等方式参数配置,并提供了不同config方式的接口。
config构成
config对应beego项目的config包构成涉及了env(env配置)、xml(xml配置)、yaml(yaml配置)、ini(ini配置,也是默认方式)、json(json配置方式)、fake(伪造配置类)、config(配置功能接口)
2.剖析
2.1 Configer接口
该接口定义如何从配置原始数据获取或设置内容相关规范行为,而完成最终的操作是由具体实现Configer接口的具体Configer类来完成的。
IniConfigContainer:ini配置类
JSONConfigContainer:json配置类
ConfigContainer:yaml配置类
ConfigContainer:xml配置类
关于env是没有配置类, 实际上env只需要get、set操作即可: env-key:env-value形式
前面四种配置类采用的类似database/sql模式,定义两部分接口配置文件的Parse(见Config接口部分)和Operation,其具体实现配置类则根据自己的实际情况实现对应的接口(后面会有专门的源码来进行说明)
2.2 Config接口
该接口定义对应的配置文件的Parse操作,主要通过解析配置文件获取其原始数据内容绑定到Configer,可进行Configer里面的get、set等操作
2.3 Register方法
该方法主要完成将不同配置类型及其具体配置实现类进行注册,能够在本地进行缓存,便于使用。通过map[string]Config来存放。
2.4 NewConfig方法和NewConfigData方法
新建Config:第一个根据filepath来新建,第二种直接根据文件内容来新建;其中第一个参数adapterName包括:ini/json/xml/yaml
2.5 源码
接口定义
配置文件操作
type Configer interface {
// 添加
Set(key, val string) error //support section::key type in given key when using ini type.
//support section::key type in key string when using ini and json type; Int,Int64,Bool,Float,DIY are same.
// 获取
String(key string) string
// 可能会存在某个key有多个值
Strings(key string) []string //get string slice
// 根据value具体的类型进行转换
Int(key string) (int, error)
Int64(key string) (int64, error)
Bool(key string) (bool, error)
Float(key string) (float64, error)
// support section::key type in key string when using ini and json type; Int,Int64,Bool,Float,DIY are same.
DefaultString(key string, defaultVal string) string
DefaultStrings(key string, defaultVal []string) []string //get string slice
DefaultInt(key string, defaultVal int) int
DefaultInt64(key string, defaultVal int64) int64
DefaultBool(key string, defaultVal bool) bool
DefaultFloat(key string, defaultVal float64) float64
DIY(key string) (interface{}, error)
GetSection(section string) (map[string]string, error)
SaveConfigFile(filename string) error
}
解析配置文件
type Config interface {
Parse(key string) (Configer, error)
ParseData(data []byte) (Configer, error)
}
注册配置解析类
// 对应的Config实现类不能被注入两次或者adapter不能为nil 否则都会触发panic
func Register(name string, adapter Config) {
if adapter == nil {
panic("config: Register adapter is nil")
}
if _, ok := adapters[name]; ok {
panic("config: Register called twice for adapter " + name)
}
adapters[name] = adapter
}
创建Config
// adapter名字和对应的配置文件 创建对应的Configer
func NewConfig(adapterName, filename string) (Configer, error) {
adapter, ok := adapters[adapterName]
if !ok {
return nil, fmt.Errorf("config: unknown adaptername %q (forgotten import?)", adapterName)
}
return adapter.Parse(filename)
}
// 根据配置文件的内容及对应adapter来创建Configer
func NewConfigData(adapterName string, data []byte) (Configer, error) {
adapter, ok := adapters[adapterName]
if !ok {
return nil, fmt.Errorf("config: unknown adaptername %q (forgotten import?)", adapterName)
}
return adapter.ParseData(data)
}
获取环境变量的值
前提:
// 若是value的{env}" , "{env||defaultValue}" , "defaultvalue"
// 例子:
// v1 := config.ExpandValueEnv("${GOPATH}") //返回环境变量GOPATH的值.
// v2 := config.ExpandValueEnv("${GOAsta||/usr/local/go}") // 环境变量内容为空或没有则输出默认值 "/usr/local/go/".
// v3 := config.ExpandValueEnv("Astaxie") // 直接返回默认值 "Astaxie".
func ExpandValueEnv(value string) (realValue string) {
realValue = value
vLen := len(value)
// 3 = ${}
if vLen < 3 { // 直接返回默认值
return
}
// 需要以"${"开始并以"}"结束, 否则直接返回默认值.
if value[0] != '$' || value[1] != '{' || value[vLen-1] != '}' {
return
}
key := ""
defaultV := ""
// value start with "${"
for i := 2; i < vLen; i++ { // 是否指定默认值: key||value
if value[i] == '|' && (i+1 < vLen && value[i+1] == '|') {
key = value[2:i] // key
defaultV = value[i+2 : vLen-1] // 默认值
break
} else if value[i] == '}' { // 没有默认值
key = value[2:i]
break
}
}
realValue = os.Getenv(key) // 获取对应的环境变量的值
if realValue == "" { // 若是对应的环境变量值为空 则返回默认值
realValue = defaultV
}
return
}
接下来选择一种配置文件格式进行讲解 默认使用ini格式,就拿IniConfig来说吧
// IniConfig实现Config来解析 ini 格式文件.
type IniConfig struct {}
// 根据配置文件名创建一个新的Config并解析文件
// 具体解析操作在方法parseFile实现
func (ini *IniConfig) Parse(name string) (Configer, error) {
return ini.parseFile(name)
}
// 读取文件内容 使用parseData来完成真正的解析操作,
// 换句话说Parse和ParseData两个方法底层操作都一样的
func (ini *IniConfig) parseFile(name string) (*IniConfigContainer, error) {
data, err := ioutil.ReadFile(name) // 读取文件内容
if err != nil {
return nil, err
}
return ini.parseData(filepath.Dir(name), data)
}
// 完成配置文件的解析
// ini文件格式: 节、键、值组成
// ; 配置文件的内容 => 注释
// [Section1 Name] => 节
// KeyName1=value1 => 键、值
func (ini *IniConfig) parseData(dir string, data []byte) (*IniConfigContainer, error) {
cfg := &IniConfigContainer{ // 初始化配置类
data: make(map[string]map[string]string), // 数据
sectionComment: make(map[string]string), //
keyComment: make(map[string]string),
RWMutex: sync.RWMutex{},
}
cfg.Lock()
defer cfg.Unlock()
var comment bytes.Buffer
buf := bufio.NewReader(bytes.NewBuffer(data)) // 读取数据
// check the BOM
head, err := buf.Peek(3) // 检查BOM
if err == nil && head[0] == 239 && head[1] == 187 && head[2] == 191 {
for i := 1; i <= 3; i++ {
buf.ReadByte()
}
}
section := defaultSection // 节
for {
line, _, err := buf.ReadLine() // 解析每行内容
if err == io.EOF {
break
}
// 直接返回异常比再做处理要好
if _, ok := err.(*os.PathError); ok {
return nil, err
}
line = bytes.TrimSpace(line) // 去掉空白符
if bytes.Equal(line, bEmpty) {
continue
}
var bComment []byte // 注解
switch {
case bytes.HasPrefix(line, bNumComment):
bComment = bNumComment
case bytes.HasPrefix(line, bSemComment):
bComment = bSemComment
}
if bComment != nil { // 获取注解的内容
line = bytes.TrimLeft(line, string(bComment))
// Need append to a new line if multi-line comments.
if comment.Len() > 0 {
comment.WriteByte('\n')
}
comment.Write(line)
continue
}
if bytes.HasPrefix(line, sectionStart) && bytes.HasSuffix(line, sectionEnd) { // 节内容
section = strings.ToLower(string(line[1 : len(line)-1])) // section name case insensitive
if comment.Len() > 0 {
cfg.sectionComment[section] = comment.String()
comment.Reset()
}
if _, ok := cfg.data[section]; !ok {
cfg.data[section] = make(map[string]string)
}
continue
}
if _, ok := cfg.data[section]; !ok {
cfg.data[section] = make(map[string]string)
}
keyValue := bytes.SplitN(line, bEqual, 2)
key := string(bytes.TrimSpace(keyValue[0])) // key name case insensitive
key = strings.ToLower(key)
// handle include "other.conf"
if len(keyValue) == 1 && strings.HasPrefix(key, "include") { // 内容
includefiles := strings.Fields(key)
if includefiles[0] == "include" && len(includefiles) == 2 { // 特殊处理include
otherfile := strings.Trim(includefiles[1], "\"")
if !filepath.IsAbs(otherfile) {
otherfile = filepath.Join(dir, otherfile)
}
i, err := ini.parseFile(otherfile)
if err != nil {
return nil, err
}
for sec, dt := range i.data {
if _, ok := cfg.data[sec]; !ok {
cfg.data[sec] = make(map[string]string)
}
for k, v := range dt {
cfg.data[sec][k] = v // 得到ini文件数据
}
}
for sec, comm := range i.sectionComment {
cfg.sectionComment[sec] = comm // 节内容
}
for k, comm := range i.keyComment {
cfg.keyComment[k] = comm // key内容
}
continue
}
}
if len(keyValue) != 2 {
return nil, errors.New("read the content error: \"" + string(line) + "\", should key = val")
}
val := bytes.TrimSpace(keyValue[1])
if bytes.HasPrefix(val, bDQuote) {
val = bytes.Trim(val, `"`)
}
cfg.data[section][key] = ExpandValueEnv(string(val))
if comment.Len() > 0 {
cfg.keyComment[section+"."+key] = comment.String()
comment.Reset()
}
}
return cfg, nil // 返回对应的Configer
}
// ParseData parse ini the data
func (ini *IniConfig) ParseData(data []byte) (Configer, error) {
dir := "beego"
currentUser, err := user.Current()
if err == nil {
dir = "beego-" + currentUser.Username
}
dir = filepath.Join(os.TempDir(), dir)
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
return nil, err
}
return ini.parseData(dir, data)
}
其实上述的源码可以看到通过实现config.go文件中的Config接口方法即认为当前的struct是其具体实现类,同时也将Config接口方法按需实现并不一定需要提前来完成增强Config的可扩展性(类似Java内置的SPI机制),也将配置文件与其具体操作类关联起来了Configer(解析方法返回的结果是便是具体配置操作实现类)
接下来看下配置实现类还是以Ini配置文件为主
// Ini配置文件具体类,一般配置实现类具备data(配置文件内容)、RWMutex(保证读写goroutine安全的)即可
// 其他新增的字段多半是该配置文件专属特征
// Ini文件
// ;这是一个例子
// [Category]
// count=5
// Default=2
// Category0=2|4|0|已下载|C:\QQDownload|0
type IniConfigContainer struct {
data map[string]map[string]string // section=> key:val
sectionComment map[string]string // section : comment
keyComment map[string]string // id: []{comment, key...}; id 1 is for main comment.
sync.RWMutex
}
// 方法1、根据key 获取对应的value并将value按照其真实的类型返回
// 方法2、在方法1的基础上 增加了默认值 当没有返回结果时 则使用默认值来填充 而不是像方法1输出error
// 根据指定的key 返回一个bool值,解析错误时会触发输出一个error; 正常结果返回则err=nil
// Bool returns the boolean value for a given key.
func (c *IniConfigContainer) Bool(key string) (bool, error) {
return ParseBool(c.getdata(key))
}
// 根据指定的key返回一个bool值,出现error!=nil时 则使用默认值来填充
// DefaultBool returns the boolean value for a given key.
// if err != nil return defaultval
func (c *IniConfigContainer) DefaultBool(key string, defaultval bool) bool {
v, err := c.Bool(key)
if err != nil {
return defaultval
}
return v
}
// 根据指定key返回对应的Int值, 当不能正常输出int值,则对应的给出error
// Int returns the integer value for a given key.
func (c *IniConfigContainer) Int(key string) (int, error) {
return strconv.Atoi(c.getdata(key))
}
// 根据给定的key返回对应的值 若是处理过程出现error 则输出默认值
// DefaultInt returns the integer value for a given key.
// if err != nil return defaultval
func (c *IniConfigContainer) DefaultInt(key string, defaultval int) int {
v, err := c.Int(key)
if err != nil {
return defaultval
}
return v
}
// Int64 returns the int64 value for a given key.
func (c *IniConfigContainer) Int64(key string) (int64, error) {
return strconv.ParseInt(c.getdata(key), 10, 64)
}
// DefaultInt64 returns the int64 value for a given key.
// if err != nil return defaultval
func (c *IniConfigContainer) DefaultInt64(key string, defaultval int64) int64 {
v, err := c.Int64(key)
if err != nil {
return defaultval
}
return v
}
// Float returns the float value for a given key.
func (c *IniConfigContainer) Float(key string) (float64, error) {
return strconv.ParseFloat(c.getdata(key), 64)
}
// DefaultFloat returns the float64 value for a given key.
// if err != nil return defaultval
func (c *IniConfigContainer) DefaultFloat(key string, defaultval float64) float64 {
v, err := c.Float(key)
if err != nil {
return defaultval
}
return v
}
// String returns the string value for a given key.
func (c *IniConfigContainer) String(key string) string {
return c.getdata(key)
}
// DefaultString returns the string value for a given key.
// if err != nil return defaultval
func (c *IniConfigContainer) DefaultString(key string, defaultval string) string {
v := c.String(key)
if v == "" {
return defaultval
}
return v
}
// 返回指定key对应的[]string(slice) 当值不存在或为空 则结果=nil
// Strings returns the []string value for a given key.
// Return nil if config value does not exist or is empty.
func (c *IniConfigContainer) Strings(key string) []string {
v := c.String(key)
if v == "" {
return nil
}
return strings.Split(v, ";") // 使用;分割value的内容
}
// // 返回指定key对应的[]string(slice) 当结果=nil 则使用指定的默认值来填充
// DefaultStrings returns the []string value for a given key.
// if err != nil return defaultval
func (c *IniConfigContainer) DefaultStrings(key string, defaultval []string) []string {
v := c.Strings(key)
if v == nil {
return defaultval
}
return v
}
// 根据给定ini中的节 获取其内容结果是一个map
// 若是对应的内容不存在 则输出nil和error(“not exist section”)
// GetSection returns map for the given section
func (c *IniConfigContainer) GetSection(section string) (map[string]string, error) {
if v, ok := c.data[section]; ok {
return v, nil
}
return nil, errors.New("not exist section")
}
// 将配置内容存放到文件中
// 注意:环境变量会以真实结果存放
// SaveConfigFile save the config into file.
//
// BUG(env): The environment variable config item will be saved with real value in SaveConfigFile Function.
func (c *IniConfigContainer) SaveConfigFile(filename string) (err error) {
// Write configuration file by filename.
f, err := os.Create(filename) // 创建对应的配置文件
if err != nil {
return err
}
defer f.Close() // 保证文件保存结束后 回收对应的资源
// Get section or key comments. Fixed #1607
// 获取节和key的注释内容
getCommentStr := func(section, key string) string {
var (
comment string // 注释内容
ok bool
)
if len(key) == 0 { // 当没key时 直接获取对应section节的注释
comment, ok = c.sectionComment[section]
} else { // 当有key时 需要根据section(节).key的格式获取对应key的注释
comment, ok = c.keyComment[section+"."+key]
}
if ok { // 获取到对应的注释 需要对注释真实的内容进行验证:空内容、空白符
// Empty comment
if len(comment) == 0 || len(strings.TrimSpace(comment)) == 0 {
return string(bNumComment)
}
prefix := string(bNumComment)
// Add the line head character "#"
return prefix + strings.Replace(comment, lineBreak, lineBreak+prefix, -1)
}
return ""
}
// 使用buffer提高内容读取
buf := bytes.NewBuffer(nil)
// 保存默认section(节)在第一行
// Save default section at first place
if dt, ok := c.data[defaultSection]; ok { // 获取默认节的内容
for key, val := range dt { // 遍历该section节下的内容
if key != " " { // 当key存在时
// Write key comments.
// 写key相关的注释
if v := getCommentStr(defaultSection, key); len(v) > 0 {
if _, err = buf.WriteString(v + lineBreak); err != nil {
return err
}
}
// 写key和value:类似key=value
// Write key and value.
if _, err = buf.WriteString(key + string(bEqual) + val + lineBreak); err != nil {
return err
}
}
}
// 当该section(节)内容写完 则需要增加一个空白行 以分割
// Put a line between sections.
if _, err = buf.WriteString(lineBreak); err != nil {
return err
}
}
// 保存section和data
// 将每个section(节)及其内容、注释分别写入
// Save named sections
for section, dt := range c.data {
if section != defaultSection {
// Write section comments.
if v := getCommentStr(section, ""); len(v) > 0 {
if _, err = buf.WriteString(v + lineBreak); err != nil {
return err
}
}
// Write section name.
if _, err = buf.WriteString(string(sectionStart) + section + string(sectionEnd) + lineBreak); err != nil {
return err
}
for key, val := range dt {
if key != " " {
// Write key comments.
if v := getCommentStr(section, key); len(v) > 0 {
if _, err = buf.WriteString(v + lineBreak); err != nil {
return err
}
}
// Write key and value.
if _, err = buf.WriteString(key + string(bEqual) + val + lineBreak); err != nil {
return err
}
}
}
// Put a line between sections.
if _, err = buf.WriteString(lineBreak); err != nil {
return err
}
}
}
_, err = buf.WriteTo(f)
return err
}
// 写入一个key-value
// 若是给指定的section(节)写入内容 则对应的key: section::key
// 注意:一旦给定section不存在时 则会触发panic
// Set writes a new value for key.
// if write to one section, the key need be "section::key".
// if the section is not existed, it panics.
func (c *IniConfigContainer) Set(key, value string) error {
c.Lock()
defer c.Unlock()
if len(key) == 0 {
return errors.New("key is empty")
}
var (
section, k string
sectionKey = strings.Split(strings.ToLower(key), "::")
)
if len(sectionKey) >= 2 {
section = sectionKey[0]
k = sectionKey[1]
} else {
section = defaultSection
k = sectionKey[0]
}
if _, ok := c.data[section]; !ok {
c.data[section] = make(map[string]string)
}
c.data[section][k] = value
return nil
}
// 返回指定key的原数据
// DIY returns the raw value by a given key.
func (c *IniConfigContainer) DIY(key string) (v interface{}, err error) {
if v, ok := c.data[strings.ToLower(key)]; ok {
return v, nil
}
return v, errors.New("key not find")
}
// section.key or key
func (c *IniConfigContainer) getdata(key string) string {
if len(key) == 0 {
return ""
}
c.RLock()
defer c.RUnlock()
var (
section, k string
sectionKey = strings.Split(strings.ToLower(key), "::")
)
if len(sectionKey) >= 2 {
section = sectionKey[0]
k = sectionKey[1]
} else {
section = defaultSection
k = sectionKey[0]
}
if v, ok := c.data[section]; ok {
if vv, ok := v[k]; ok {
return vv
}
}
return ""
}
其实可以看到对应的Configer关于配置文件的操作类 相关行为比较简单,在实现上也保持和Config同样的可扩展性,当某个strcut实现了Configer接口的方法即认为是其具体的实现。
关于json、xml、yaml基本与ini配置文件实现类似,在这里就不逐一讲解,关于环境变量方式相对比较简单,不过又不同于前面几种配置方式,只需要实现get、set操作即可: set key = value, get key.
使用
func main() {
ini := new(config.IniConfig)
cnf, err := ini.Parse(FILE_PATH) // FILE_PATH见下面的配置文件
if err != nil{
log.Fatal(err)
}
fmt.Println(cnf.String("dbinfo::name"))
fmt.Println(cnf.String("name")) // 不能获取到内容 未指定对应的section(节)
fmt.Println(cnf.Int("httpport"))
fmt.Println(cnf.GetSection("dbinfo")) // 获取指定section(节)的内容
fmt.Println(cnf.DIY("default")) // IniConfiger内部data: map[section]map[key]value 而此处的key其实就是section
}
======= 配置文件========
app=app
#comment one
#comment two
# comment three
appname=beeapi
httpport=8080
;DB Info, enable db
# DB Info
# enable db
[dbinfo]
# db type name
# suport mysql,sqlserver
name=mysql
相关推荐
"beego-develop"可能是一个开发目录,其中可能包含了Beego框架的核心源码、示例项目、开发工具或者特定的扩展模块。 在这个目录下,我们可以期待找到以下内容: 1. `main.go`:项目的入口文件,通常会包含初始化...
如需要重新运行本程序,请删除conf/config.ini文件,并重启程序,重新进行安装 本程序一般情况下您只需要修改运行端口,并保证服务器防火墙开放此端口 关于程序的更多信息,请查看 CCMS系统使用说明.docx
beego的设计思想是模块化,它由八个独立的模块构成,这些模块高度解耦,允许开发者独立使用,比如cache模块用于实现缓存逻辑,日志模块用于记录操作信息,以及config模块用于解析各种格式的配置文件等。这种设计使得...
这是一个基于Golang编程语言和Beego框架开发的接口在线文档管理系统源码项目,名为"mindoc"。在本文中,我们将深入探讨这个系统的相关知识点,包括Golang、Beego框架以及接口文档管理。 首先,Golang(也称为Go语言...
- Go语言有许多优秀的框架,如Gin、Beego、Echo等。OPBook项目可能采用了其中的一种,这些框架提供了便利的路由管理、中间件支持和错误处理,加速了开发进程。 3. **项目结构** - `OPBook-master`目录很可能包含...
此外,可能还会包含`main.go`作为项目的入口文件,`config`目录用于配置管理,`models`目录存放数据模型,`controllers`目录包含API的处理逻辑,以及`tests`目录下的测试代码。 总结起来,通过学习这个Go语言的开源...
在Web开发领域,Go提供了多种框架,如Gin、Beego、Echo等,ATC便是其中之一,它专注于快速开发和可定制性。 二、ATC框架特性 1. **RESTful API 支持**:ATC支持创建符合REST原则的API接口,使开发者可以轻松地构建...
在Go-exercise-main目录中,我们可以期待看到Go源码文件(.go)和其他相关配置文件,如main.go(项目的起点),或者用于构建和测试的脚本。 在Go项目中,我们通常会遇到以下结构: 1. `main.go`:这是应用的入口点...
- `config/`:配置文件,如数据库连接字符串、端口设置等。 通过研究和理解这个项目,开发者不仅能掌握Go语言的基础,还能深入理解Web开发的流程和最佳实践。同时,这也是一个很好的机会去学习Go语言的并发特性,...
- 第三方库如 `gin`, `echo`, `beego` 等提供更高级别的路由和中间件功能。 - ORM(对象关系映射)库如 `GORM` 或 `sqlx` 进行数据库操作。 - 使用 `go build` 或 `go run` 命令编译和运行程序,以及 `go test` 进行...
5. `config`目录:存储配置文件,如数据库连接信息、服务器端口等。 6. `middleware`目录:可能包含自定义的中间件,用于添加额外的功能,如日志记录、权限验证等。 7. `database`或`storage`目录:数据库操作相关的...
在Go项目中,源码通常按照模块或者包(package)组织,每个包内包含相关的.go文件。此外,还可能有如main.go(程序入口文件)、models(数据模型)、controllers(控制器,处理HTTP请求)、views(视图,HTML模板)...
功能十分简单,客户,产品,销售,采用Beego + Layui开发,不断完善中默认使用Sqlite3数据库如需使用Mysql请在config / app.conf中修改设置账号:admin密码:123456 安装方法 源码直接go run main.go install 编译好...