用Golang作点自动化的东西

用protobuf的时候就已经以为挺好玩的,一个.proto文件,用一个命令加一个language type的参数就能生成相应语言的pb文件,神奇。这阵子闲了一点,调查了下,发现golang原生支持这种东西。核心,go generate。前端

go generate

简介

go generate命令是go 1.4版本里面新添加的一个命令,当运行go generate时,它将扫描当前目录下的go文件,找出全部包含"//go:generate"的特殊注释,提取并执行该注释后面的命令,命令为可执行程序。python

须要看Go的官方使用方法,在命令行下git

 $ go help generate
usage: go generate [-run regexp] [-n] [-v] [-x] [build flags] [file.go... | packages]

Generate runs commands described by directives within existing
files. Those commands can run any process but the intent is to
create or update Go source files.

Go generate is never run automatically by go build, go get, go test,
and so on. It must be run explicitly.

....

To convey to humans and machine tools that code is generated,
generated source should have a line that matches the following
regular expression (in Go syntax):

	^// Code generated .* DO NOT EDIT\.$

The line may appear anywhere in the file, but is typically
placed near the beginning so it is easy to find.

....
复制代码

说明很长,摘了一部分出来,简要来讲,go generate就是运行声明在文件里面的命令,这些命令的意图是生成或更新go文件(个人理解是:官方但愿的约束);go generate不会被相似go get,go build,go test等的命令触发执行,必须由开发者显式使用。github

还有,为了让人类和机器的工具都知道代码是生成,最好在比较容易发现的地方加上golang

// Code generated .* DO NOT EDIT\.$
复制代码

在我看来,其实go generate就是运行命令生一个文件而已,具体生成的文件是什么格式,有什么用,有什么内容,这都是由开发者自定义的。只不过,最好就是只用来生成和更新go文件,而且在文件内容里面加一个注释来标志这个go文件是自动生成的,仅此而已。shell

小试

为了验证上面说的话,我决定不走寻常路,不像常规的生成代码了,用go generate来一张二维码。express

写一个命令文件

package main

import (
	"github.com/skip2/go-qrcode"
	"os"
)

func main() {
	data,_ := qrcode.Encode("go generate 生成的图片",qrcode.Medium,256)
	f,_ := os.Create("hello.png")
	f.Write(data)
    f.Close()
}

// P.S. 疯狂忽略error的demo,真正写业务的时候请勿模仿 
复制代码

以上代码很简单,用第三方库qrcode生成了一张叫hello.png的二维码.json

写一个gen.go

package main
//go:generate gen_png

复制代码

嗯,是的,你没看错,包含最后一个空行,只需三行代码后端

生成二维码

当前目录状况bash

$ pwd
/Users/cltx/workspace/go/src/github.com/LSivan/go_codes/test/test_generate/gen_png
 $ ls
gen.go  main.go

复制代码

编译,而后在目录下执行go generate

$ go build
 $ go generate 1 ↵
gen.go:2: running "gen_png": exec: "gen_png": executable file not found in $PATH
复制代码

执行文件得在PATH目录下,把它移动到GOPATH,确保GOPATH加进了PATH下

$ sudo mv gen_png $GOPATH/bin/
复制代码

再次generate,就能看到目录下多了个二维码

$ go generate 
 $ ls
gen.go  hello.png  main.go
复制代码

这里就不贴图啦

正片

咱们来写一个json格式的struct生成器。具体来讲就是给定一个json,而后根据这个json,生成相应的Go文件。为何写这个?好比说先后端定义了json的接口,或者接口有所改动,这个东西就很好用了。

json 2 structure

需求大概是这样子,我和前端小明定了一个接口,获取用户风险信息,接口协议以下:

{
  "risk_query_response": {
    "code": "10000",
    "msg": "Success",
    "risk_result": {
      "merchant_fraud": "has_risk" ,
      "merchant_general":"rank_1"
    },
    "risk_result_desc": "{\"has_risk\":\"有风险\",\"rank_1\":\"等级1\",\"71.5\":\"评分71.5\"}"
  },
  "sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE",
  "risk_rule": [
    101,
    1001
  ],
  "time": 1553481619,
  "is_black_user": false
}
复制代码

我这边须要在业务封装一个strcut,而后转json返回给前端,以下

type Risk struct {
	RiskQueryResponse struct {
		Code       string `json:"code"`
		Msg        string `json:"msg"`
		RiskResult struct {
			MerchantFraud   string `json:"merchant_fraud"`
			MerchantGeneral string `json:"merchant_general"`
		} `json:"risk_result"`
		RiskResultDesc string `json:"risk_result_desc"`
	} `json:"risk_query_response"`
	Sign        string `json:"sign"`
	RiskRule    []int  `json:"risk_rule"`
	Time        int    `json:"time"`
	IsBlackUser bool   `json:"is_black_user"`
}
复制代码

手写固然简单,不到三分钟就写好了。可是几十上百个接口的时候,这就可怕了。为此,咱们来试下,go generate。

编写命令(可执行)文件

这个命令(可执行文件)的本质其实就是解析json,而后获得一个Go文件。

先是读取文件,解析json

if f == "" {
    panic("file can not be nil")
}

jsonFile, err := os.Open(f)
if err != nil {
    panic(fmt.Sprintf("open file error:%s", err.Error()))
}
fi, err := jsonFile.Stat()
if err != nil {
    panic(fmt.Sprintf("get file stat error:%s",err.Error()))
}

if fi.Size() > 40960 {
    panic("json too big")
}
data := make([]byte, 40960)
bio := bufio.NewReader(jsonFile)
n, err := bio.Read(data)
if err != nil {
    panic(err)
}
fmt.Println(string(data[:n]))

m := new(map[string]interface{})
err = json.Unmarshal(data[:n], m)
if err != nil {
    panic(fmt.Sprintf("unmarshal json error:%s", err.Error()))
}
复制代码

反射获得json各对键值的类型,这里有个细节,go会用float64来接收数值类型;此外,这里用到了一个转驼峰命名的第三方库strcase

field := ""
for k, v := range *m {
    t := reflect.TypeOf(v)
    kind := t.Kind()
    fieldType := t.String()
    switch kind {
    case reflect.String:
        field += strcase.ToCamel(k) + " " + fieldType + fmt.Sprintf("`json:\"%s\"`\n",k)
    case reflect.Map:
        field += strcase.ToCamel(k) + " struct {\n"
        fields := parserMap(v.(map[string]interface{}))
        field += fields
        field += "} " +fmt.Sprintf("`json:\"%s\"`\n",k)
    case reflect.Slice:
        fieldType = parserSlice(v.([]interface{}))
        field += strcase.ToCamel(k) + "[]" + fieldType + fmt.Sprintf("`json:\"%s\"`\n",k)
    case reflect.Float64:
        fieldType = "int"
        field += strcase.ToCamel(k) + " " + fieldType + fmt.Sprintf("`json:\"%s\"`\n",k)
    case reflect.Bool:
        field += strcase.ToCamel(k) + " " + fieldType + fmt.Sprintf("`json:\"%s\"`\n",k)
    default:
        fmt.Println("other kind", k, kind)
    }
}

复制代码

对map和slice类型作特殊处理,让他们变成相应的struct

func parserMap(m map[string]interface{}) string {
	field := ""
	for k,v := range m {
		t := reflect.TypeOf(v)
		kind := t.Kind()
		fieldType := t.String()
		switch kind {
		case reflect.String:
			field += strcase.ToCamel(k) + " " + fieldType + fmt.Sprintf("`json:\"%s\"`\n",k)
		case reflect.Map:
			field += strcase.ToCamel(k) + " struct {\n"
			fields := parserMap(v.(map[string]interface{}))
			field += fields
			field += "} " +fmt.Sprintf("`json:\"%s\"`\n",k)
		case reflect.Slice:
			parserSlice(v.([]interface{}))
		case reflect.Float64:
			fieldType = "int"
			field += strcase.ToCamel(k) + " " + fieldType + fmt.Sprintf("`json:\"%s\"`\n",k)
		case reflect.Bool:
			field += strcase.ToCamel(k) + " " + fieldType + fmt.Sprintf("`json:\"%s\"`\n",k)
		default:
			fmt.Println("other kind", k, kind)
		}
	}
	return field
}

func parserSlice(s []interface{}) string {
	field := ""
	for k,v := range s {
		t := reflect.TypeOf(v)
		kind := t.Kind()
		fieldType := t.String()
		switch kind {
		case reflect.String:
			return fieldType
		case reflect.Float64:
			fieldType = "int"
			return fieldType
		case reflect.Bool:
			return fieldType
		default:
			fmt.Println("other kind", k, kind)
		}
	}
	return field
}
复制代码

写入到go文件中,同时别忘了遵循下官方的约束,在文件开头加个自动生成声明

fileName := strings.Split(fi.Name(), ".")[0]
goFile := fmt.Sprintf(output+"/%s.go", fileName)
f, _ := os.Create(goFile)

template := "// Code generated json_2_struct. DO NOT EDIT.\n" +
	"package %s\n" +
	"\n" +
	" type %s struct {\n"

_, _ = f.Write([]byte(fmt.Sprintf(template+field+"}", pack, strcase.ToCamel(fileName))))
_ = f.Close()
复制代码

最后fmt一下生成的go文件

cmd := exec.Command("go", "fmt", goFile)
err = cmd.Run()
if err != nil {
	panic(err)
}
复制代码

编译好以后放到GOPATH

$ go build -o json_2_struct 
$ sudo mv json_2_struct $GOPATH/bin/ 
复制代码

第一步大功告成

写个脚本用起来

很遗憾的是,go generate命令扫描的一定是go文件,所以脚本得先写个go文件,而后go文件里面增长go:generate的注释,而后执行go generate,脚本以下

#!/bin/bash
echo "package main
//go:generate json_2_struct -file=$1 -output=$2

" > tmp.go

go generate

rm tmp.go
复制代码

P.S. 没有作非法校验

试下效果

让咱们使用脚本生成go文件(P.S. 我将脚本移到$PATH下了)

$ export_go_file /Users/cltx/workspace/go/src/github.com/LSivan/go_codes/test/test_generate/data/risk.json /Users/cltx/workspace/go/src/github.com/LSivan/go_codes/test/test_generate/data/ 
{
  "risk_query_response": {
    "code": "10000",
    "msg": "Success",
    "risk_result": {
      "merchant_fraud": "has_risk" ,
      "merchant_general":"rank_1"
    },
    "risk_result_desc": "{\"has_risk\":\"有风险\",\"rank_1\":\"等级1\",\"71.5\":\"评分71.5\"}"
  },
  "sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE",
  "risk_rule": [
    101,
    1001
  ],
  "time": 1553481619,
  "is_black_user": false
}
 $ ls [21:35:04]
risk.go   risk.json
 $ cat risk.go [21:35:42]
// Code generated test_generate. DO NOT EDIT.
package main

type Risk struct {
        RiskQueryResponse struct {
                Code       string `json:"code"`
                Msg        string `json:"msg"`
                RiskResult struct {
                        MerchantFraud   string `json:"merchant_fraud"`
                        MerchantGeneral string `json:"merchant_general"`
                } `json:"risk_result"`
                RiskResultDesc string `json:"risk_result_desc"`
        } `json:"risk_query_response"`
        Sign        string `json:"sign"`
        RiskRule    []int  `json:"risk_rule"`
        Time        int    `json:"time"`
        IsBlackUser bool   `json:"is_black_user"`
}

复制代码

OK,大功告成

总结

作了一大半以后,发现原来已经早有实现了,尴尬,硬着头皮重复写了个轮子,且当学习吧。

或许会有疑问,为何不直接写脚本执行那个可执行文件,非得用go generate?并且为何不用脚本属性更强的python呢?

问得好,因而看了两个generate tool的代码,在stringer的代码里找到了一点痕迹,我认为go generate适用于go file -> go file的状况,由于golang有ast支持。

举个例子,对于hello.go,分别有函数A和函数B,只但愿生成函数B的表格单元测试代码,这种状况用golang 原生的ast包 + go:generate就十分方便快捷。

最后附上全代码

package main

import (
	"bufio"
	"encoding/json"
	"flag"
	"fmt"
	"github.com/iancoleman/strcase"
	"os"
	"os/exec"
	"reflect"
	"strings"
)

var output string
var pack string
var f string

func main() {

	flag.StringVar(&output, "output", "", "output dir")
	flag.StringVar(&pack, "package", "main", "package")
	flag.StringVar(&f, "file", "", "json file")
	flag.Parse()

	if f == "" {
		panic("file can not be nil")
	}

	if output == "" {
		panic("output dir is nil")
	}
	jsonFile, err := os.Open(f)
	if err != nil {
		panic(fmt.Sprintf("open file error:%s", err.Error()))
	}
	fi, err := jsonFile.Stat()
	if err != nil {
		panic(fmt.Sprintf("get file stat error:%s",err.Error()))
	}

	if fi.Size() > 40960 {
		panic("json too big")
	}
	data := make([]byte, 40960)
	bio := bufio.NewReader(jsonFile)
	n, err := bio.Read(data)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(data[:n]))

	m := new(map[string]interface{})
	err = json.Unmarshal(data[:n], m)
	if err != nil {
		panic(fmt.Sprintf("unmarshal json error:%s", err.Error()))
	}

	field := ""
	for k, v := range *m {
		t := reflect.TypeOf(v)
		kind := t.Kind()
		fieldType := t.String()
		switch kind {
		case reflect.String:
			field += strcase.ToCamel(k) + " " + fieldType + fmt.Sprintf("`json:\"%s\"`\n",k)
		case reflect.Map:
			field += strcase.ToCamel(k) + " struct {\n"
			fields := parserMap(v.(map[string]interface{}))
			field += fields
			field += "} " +fmt.Sprintf("`json:\"%s\"`\n",k)
		case reflect.Slice:
			fieldType = parserSlice(v.([]interface{}))
			field += strcase.ToCamel(k) + "[]" + fieldType + fmt.Sprintf("`json:\"%s\"`\n",k)
		case reflect.Float64:
			fieldType = "int"
			field += strcase.ToCamel(k) + " " + fieldType + fmt.Sprintf("`json:\"%s\"`\n",k)
		case reflect.Bool:
			field += strcase.ToCamel(k) + " " + fieldType + fmt.Sprintf("`json:\"%s\"`\n",k)
		default:
			fmt.Println("other kind", k, kind)
		}
	}

	fileName := strings.Split(fi.Name(), ".")[0]
	goFile := fmt.Sprintf(output+"%s.go", fileName)
	file, _ := os.Create(goFile)

	template := "// Code generated test_generate. DO NOT EDIT.\n" +
		"package %s\n" +
		"\n" +
		" type %s struct {\n"

	_, _ = file.Write([]byte(fmt.Sprintf(template+field+"}", pack, strcase.ToCamel(fileName))))
	_ = file.Close()
	
	cmd := exec.Command("go", "fmt", goFile)
	err = cmd.Run()
	if err != nil {
		panic(err)
	}

}


func parserMap(m map[string]interface{}) string {
	field := ""
	for k,v := range m {
		t := reflect.TypeOf(v)
		kind := t.Kind()
		fieldType := t.String()
		switch kind {
		case reflect.String:
			field += strcase.ToCamel(k) + " " + fieldType + fmt.Sprintf("`json:\"%s\"`\n",k)
		case reflect.Map:
			field += strcase.ToCamel(k) + " struct {\n"
			fields := parserMap(v.(map[string]interface{}))
			field += fields
			field += "} " +fmt.Sprintf("`json:\"%s\"`\n",k)
		case reflect.Slice:
			parserSlice(v.([]interface{}))
		case reflect.Float64:
			fieldType = "int"
			field += strcase.ToCamel(k) + " " + fieldType + fmt.Sprintf("`json:\"%s\"`\n",k)
		case reflect.Bool:
			field += strcase.ToCamel(k) + " " + fieldType + fmt.Sprintf("`json:\"%s\"`\n",k)
		default:
			fmt.Println("other kind", k, kind)
		}
	}
	return field
}

func parserSlice(s []interface{}) string {
	field := ""
	for k,v := range s {
		t := reflect.TypeOf(v)
		kind := t.Kind()
		fieldType := t.String()
		switch kind {
		case reflect.String:
			return fieldType
		case reflect.Float64:
			fieldType = "int"
			return fieldType
		case reflect.Bool:
			return fieldType
		default:
			fmt.Println("other kind", k, kind)
		}
	}
	return field
}

复制代码
相关文章
相关标签/搜索