通过用Go编写shell小工具来实践设计模式

zhangwei · · 1098 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

不久以前看过Rob Pike写的一篇文章(Self-referential functions and the design of options),对其中的提到关于如何配置结构体或者类成员属性的一种设计模式印象很深刻,其中的selft-referential更是魔幻但又不失实用性,看过细品,受益匪浅。他说为了为他正在编写的一个package(如果没猜错应该是net/rpc
包)找到更合适的设计模式,他在几年间尝试了多种方案,最终选择了这种,为老前辈的这种精益求精的精神深深折服。

巧合的是,前几天看了个开源项目,阅读其源码,发现英雄所见略同,也许是该作者也读了Rob的这篇文章,我不得而知,其中也用到了我们常说的“生产-消费”模式,这个项目不大,一千多行代码,实现的功能也是很简单,但作者代码的优雅程度令人印象深刻,很有学习价值。

看过的东西,哪怕曾经深入理解过的东西,如果不用于实战,或者用blog给与记录,总担心会忘。正好周末有一天空闲,就试着写了个小工具(文本替换工具,可以通过配置命令行参数,参数可以是个文件,也可以是个路径,如果是个路径可以配置是否递归替换,来替换配置的文本)仓库地址:github.com/foolishway/rp,我试了下大概3000+个文件每个文件3000+行,替换下来,大概用时不到3秒。
如图:
WX202005162108372x.png

但,功能不是重点,很简单,我们上面说过,主要是用来练手,实践学习过的设计模式。

代码也不多,两个文件,main.go和replacer.go,main是入口文件,replacer见文知意,执行replace任务。

我们从main说起;

main里面主要做接收命令行参数

flag.StringVar(&content, "con", "", "The content to be replaced.")
flag.StringVar(&rep, "rep", "", "The content to replace.")
flag.BoolVar(&recursive, "rec", false, "Allow repalce recursivly.")

flag.Parse()

main获取到con、rep、rec之后,通过调用replacer提供的method获取到option,


func withPaths(paths ...string) option {
	return func(replacer *Replacer) {
		//remove default paths
		replacer.paths = []string{}
		var exist bool
		for i := 0; i < len(paths); i++ {
			for j := 0; j < len(replacer.paths); j++ {
				if paths[i] == replacer.paths[j] {
					exist = true
					break
				}
			}
			if !exist {
				replacer.paths = append(replacer.paths, paths[i])
			}
		}
		if len(replacer.paths) == 0 {
			replacer.paths = []string{"./"}
		}
	}
}
func withExtents(exts ...string) option {
	return func(replacer *Replacer) {
		for _, ext := range exts {
			if _, ok := replacer.extents[ext]; !ok {
				replacer.extents[ext] = struct{}{}
			}
		}
	}
}

func withContent(content string) option {
	return func(replace *Replacer) {
		replace.content = content
	}
}

这里就比较有意思了,option是我们自定义的一个类型

type option func(*Replacer)

它是一个method,接收Repalcer指针,然后设置其成员属性。

main文件中,通过调用这些生成option的method,来生成个option的切片

options := []option{}
	if len(src) != 0 {
		options = append(options, withPaths(src...))
	}

	if content != "" {
		options = append(options, withContent(content))
	}

	if rep != "" {
		options = append(options, withRep(rep))
	}

	if recursive {
		options = append(options, withRec(recursive))
	}

然后调用replacer提供的构造函数,把option切片传给构造函数

replacer := newReplacer(options...)

在newReplacer构造函数中,调用切片的每个option,来生成Replacer


type Replacer struct {
	wg        sync.WaitGroup
	paths     []string
	extents   map[string]struct{}
	content   string
	replace   string
	recursive bool
	ch        chan string
}

type option func(*Replacer)

func newReplacer(options ...option) *Replacer {
	var wg sync.WaitGroup
	defaultPaths := []string{"./"}
	defaultExtents := map[string]struct{}{".go": {}, ".js": {}, ".jsx": {}, ".html": {}, ".txt": {}}
	ch := make(chan string, 10)
	replace := &Replacer{wg: wg, paths: defaultPaths, extents: defaultExtents, ch: ch}
	for _, f := range options {
		f(replace)
	}
	return replace
}

这就是文章开篇提到的Rob Pike先生的方式,当然他在文章中也提到了Self-referential functions,在option中调用生成该option的method自身并返回一个新option,是不是听绕?不是,其实很简单。推荐阅读。

在这个小工具中,用到了“生产消费模式”,生产者负责搜索用户给的路径,从目录中抓取需要替换的文件,然后把路径抛给一个channel,可以理解channel是一个管子,生产者从管子一头“灌”需要replace的文件路径,管子另一头有消费者去接收,消费者是真正执行replace任务的一方。

生产者代码如下:


func (r *Replacer) start() {
	var once sync.Once
	once.Do(r.init)

	dumper := func(path string) {
		r.wg.Add(1)
		r.ch <- path
	}
	for _, path := range r.paths {
		s, err := os.Stat(path)

		if os.IsNotExist(err) {
			log.Printf("%s not found", path)
			continue
		}
		if s.IsDir() {
			if r.recursive {
				filepath.Walk(path, func(p string, info os.FileInfo, err error) error {
					if !info.IsDir() {
						if _, ok := r.extents[filepath.Ext(p)]; ok {
							absPath, err := filepath.Abs(p)

							checkErr(err)
							dumper(absPath)
						}
					}
					return nil
				})
			} else {
				files, err := ioutil.ReadDir(path)
				if err != nil {
					log.Printf("Read dir %s error: %v", path, err)
					continue
				}

				for _, f := range files {
					if !f.IsDir() {
						if _, ok := r.extents[filepath.Ext(f.Name())]; ok {
							absPath, err := filepath.Abs(filepath.Join(path, f.Name()))
							checkErr(err)
							dumper(absPath)
						}
					}
				}
			}
		} else {
			if _, ok := r.extents[filepath.Ext(path)]; ok {
				absPath, err := filepath.Abs(path)
				checkErr(err)
				dumper(absPath)
			}
		}
	}
	//wait utill all the replace complete
	r.wg.Wait()
	//close the channel
	close(r.ch)
}

消费者去“监听”channel,一旦有数据到达,就派发一个worker去执行replace任务。
代码如下:

func (r *Replacer) init() {
	cpuNums := runtime.NumCPU()
	for i := 0; i < cpuNums; i++ {
		go extracter(&r.wg, r.ch, r.content, r.replace)
	}
}

获取CPU核心数,然后根据核心数,生成对应的线程数(准确的说是协程)去接收channel

func extracter(wg *sync.WaitGroup, ch <-chan string, content, rep string) {
	for path := range ch {
		//control the flow to avoid open too many files
		go replace(wg, path, content, rep)
	}
}

replace方法去真正执行替换文本的任务


func replace(wg *sync.WaitGroup, src, content, replace string) {
	defer func() {
		wg.Done()
		<-bucket
	}()
	//control the flow
	bucket <- struct{}{}

	var once sync.Once
	f, err := os.Open(src)
	checkErr(err)

	var bs bytes.Buffer
	checkErr(err)

	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
	    ...//获取每行文本,然后替换关键字,然后写入buffer
		bs.WriteString(line + "\n")
	}
	...
	//删除源文件
	err = os.Remove(src)
	checkErr(err)

	//生成新文件
	tf, err := os.Create(src)
	checkErr(err)

    ...
    //拷贝
	_, err = io.Copy(tf, &bs)
	checkErr(err)
}

我们看到replace方法中有一个bucket,我们在main中定义的这个bucket

bucket chan struct{} = make(chan struct{}, 100)

bucket是一个容量是100的channel,我们可以用此来控制replace同时打开的文件数量,在macOS中一个进程最多同时打开256个文件,linux中大概1024个,都有限制,所以我们保险起见,设置个100的队列。

至此,我们就告一段落了。

总结下,我们通过withXXX,如withContent、withPaths…来生成option切片,然后调用构造函数并把option切片传递给构造函数,在构造函数中调用option并把需要设置的对象(replacer)传递给option去设置成员属性。

这样做的好处是如果要做数据验证,我们可以在withXXX中做,并且如果要是开放给外部用的api的话,这种方式更加清晰易懂,开篇提到的Rob Pike的文章中的Self-referential functions也是好处之一,有兴趣的可以阅读这篇文章。

我们通过“生产-消费”模式来执行真正的替换任务,来让代码看起来更加优雅,而且流控处理更加容易把控。

最后,再次送上代码的连接:https://github.com/foolishway/rp,对Go语言和设计模式感兴趣的同学,可以和我沟通交流,我们共同学习。


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1098 次点击  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传