重构confloader.go

旧代码(代码的问题直接在代码中指出)如下:

package mconfig

import (
	"bufio"
	"errors"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	log "util/log"

	"github.com/Unknwon/goconfig"
)

// path是外部传入,不需要对pkg外开放
var ScmPath *string
// 这个实例是该文件支持的其中一种类型的conf,单独作为全局变量,就表示软件结构设计的可能不合理
var ScmConf *goconfig.ConfigFile     //scm for ini
// 有一个统一存储data的map没有问题,但是也不需要pkg外开放
var ScmMap = make(map[string]string) //scm for text

// 作为通用类库,不应该上层怎么获得ini文件
func init() {
	ScmPath = flag.String("scm", "conf/scm_config.ini", "scm config file location")
	flag.Parse()
}

// 文件是否存在与文件主旨没关系,需要提出去
func Exists(name string) bool {
	if _, err := os.Stat(name); err != nil {
		if os.IsNotExist(err) {
			return false
		}
	}
	return true
}

// 对pkg外开放的对象,搞一个get方法,但又不需要做预处理,也应该去掉
func GetScmConfig() (scmPath string) {
	return *ScmPath
}

// 这个方法糅合了两种conf文件的处理方法,需要拆分,防止支持更多类型的conf文件,导致对已有逻辑的影响
func ParseScm(field string) (value string, err error) {

	confPath := GetScmConfig()

	if !Exists(confPath) {
		fmt.Println("can't find scm config file")
		Usage()
		os.Exit(1)
	}
	ext := strings.ToLower(filepath.Ext(confPath))
	if ext == ".ini" {
		if ScmConf == nil {
			if confPath == "" {
				return "", errors.New("scm_config files not found")
			}
			ScmConf, err = goconfig.LoadConfigFile(confPath)

			if err != nil {
				return "", err
			}
		}
		value, err = ScmConf.GetValue("", field)

	} else if ext == ".php" {

		//ini不需要前缀
		prefix := "xiaomi_shopapi_server."

		field = prefix + field

		if len(ScmMap) == 0 {
			file, err := os.Open(confPath)
			if err != nil {
				log.Critical(err)
			}
			defer file.Close()

			scanner := bufio.NewScanner(file)
			for scanner.Scan() {
				line := scanner.Text()
				regex, _ := regexp.Compile("(.+)=>((.+))")
				match := regex.FindStringSubmatch(line)

				if len(match) > 0 {
					ScmMap[strings.Trim(strings.TrimSpace(match[1]), "'")] = strings.Trim(strings.TrimSpace(match[2]), "'','")

				}

			}
			if err := scanner.Err(); err != nil {
				log.Critical(err)
			}
		}

		if value, ok := ScmMap[field]; ok {
			return value, nil
		} else {
			value = ""
			err = errors.New("not find " + field)
		}
	} else {
		return "", errors.New("not support " + ext)
	}

	return
}

// 这块个人感觉没用,面向cli的应用才需要这种辅助
func Usage() {
	log.Debug("Usage:\n\r\t\t freeman -scm=/path/to/scm_config , take a look at ../doc/readme for example")
}

// 这个是最大的问题,在ini文件中添加k和v,需要在这里注册k,才能被app使用
// 整块逻辑去掉
func LoadConf() (conf map[string]string, err error) {
	fields := [...]string{
		"service.web.port",
		"service.log.info",
		"service.log.error",
	}

	conf = make(map[string]string)
	for _, field := range fields {
		conf[field], err = ParseScm(field)
		if err != nil {
			fmt.Println(err.Error())
			return
		}
	}

	return
}

优化后代码:

package conf

import (
	"bufio"
	"errors"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/Unknwon/goconfig"
)

var (
	dataMap  map[string]string
	parseErr error
)

// 参数从外部传入
func LoadConf(path string) {
	dataMap, parseErr = confFactory(path).parse()
	if parseErr != nil {
		panic(parseErr)
	}
}

// 工厂用于封装根据扩展名选择不同parser的逻辑,后续新增,只修改这里
func confFactory(path string) ConfParserInf {
	var parserInf ConfParserInf
	ext := strings.ToLower(filepath.Ext(path))
	switch ext {
	case ".ini":
		parserInf = &IniParser{
			ConfParser{Path: path},
		}
	case ".php":
		parserInf = &PhpParser{
			ConfParser{Path: path},
		}
	default:
		panic("unsupport conf ext")
	}
	return parserInf
}

// pkg外只提供一个方法,client自动就知道怎么用了
func GetValue(name string) string {
	return dataMap[name]
}

// 代码可以拆分出去,允许pkg外访问
// pkg外部可以传入parser
// 相当于解耦parser和conf主逻辑,但没那么大辟谣
type ConfParserInf interface {
	// parse后数据写入confData,对外提供方法调用
	parse() (map[string]string, error)
}

type ConfParser struct {
	Path string
}

type IniParser struct {
	ConfParser
}

type PhpParser struct {
	ConfParser
}

func (p *IniParser) parse() (map[string]string, error) {
	data := make(map[string]string)

	if p.Path == "" {
		return nil, errors.New("conf path is empty")
	}

	conf, err := goconfig.LoadConfigFile(p.Path)
	if err != nil {
		return nil, err
	}

	sectionList := conf.GetSectionList()
	for _, sectionName := range sectionList {
		keyList, err := conf.GetSection(sectionName)
		if err != nil {
			return nil, err
		}

		for keyName, keyValue := range keyList {
			data[keyName] = keyValue
		}
	}
	return data, nil
}

func (p *PhpParser) parse() (map[string]string, error) {
	data := make(map[string]string)

	file, err := os.Open(p.Path)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := scanner.Text()
		regex, _ := regexp.Compile("(.+)=>((.+))")
		match := regex.FindStringSubmatch(line)
		if len(match) > 0 {
			data[strings.Trim(strings.TrimSpace(match[1]), "'")] = strings.Trim(strings.TrimSpace(match[2]), "'','")
		}
	}
	if err := scanner.Err(); err != nil {
		return nil, err
	}
	return data, nil
}

做了go开发大概有1个月,看了beego、groupcache、go-cas、gorilla等一些比较小的项目的代码书写风格。觉得还是对牛人风格比较笃信,groupcache值得推荐,memcache作者在google优化dl搞出来的一个小项目。

后续也捡起robert c.martin的讲敏捷开发的那本书,里面对于软件架构原则的描述值得多读几遍,深入理解。再接合设计模式,面对日常编码,可能会更轻松。

下面说一个最近优化的confloader.go文件。这个文件,主要帮助client封装conf文件的处理逻辑,ini或者其他。

重构后的代码明显比旧代码好读,代码的质量在当前正在提速的互联网公司可能是很难保证的东西,但作为个体,建议有一定程度的坚持。

重构confloader.go
Share this