使用Test来生成代码

一般我们会起一个main函数的go主文件来写逻辑用于生成代码,但有的时候不太方便在一个包里面有main函数,而生成代码的逻辑又是必须的且最好是就在同一个包内或临近包中,此时,可以利用Test的编译机制,将我们需要的代码生成逻辑写在TestAbc函数中。

辅助脚本

// file: github.com/cloudwego/frugal/internal/reflect/append_gen.sh
#!/bin/bash

FRUGAL_GEN_APPEND_MAP_FILE="append_map_gen.go"
FRUGAL_GEN_APPEND_LIST_FILE="append_list_gen.go"

rm -f $FRUGAL_GEN_APPEND_MAP_FILE
rm -f $FRUGAL_GEN_APPEND_LIST_FILE

exec go test -v -run=TestGenAppend -gencode=true

关键Test函数

// file: github.com/cloudwego/frugal/internal/reflect/append_gen_test.go
package reflect

var (
	gencode = flag.Bool("gencode", false, "generate list/map code for better performance")
)
// file: github.com/cloudwego/frugal/internal/reflect/append_list_gen_test.go
package reflect

const appendListFileName = "append_list_gen.go"

func TestGenAppendListCode(t *testing.T) {
	if *gencode {
		genAppendListCode(t, appendListFileName)
	return
	}
	// ...other logic...
}

func genAppendListCode(t *testing.T, filename string) {
	defineErr := map[ttype]bool{tOTHER: true}
	defineStr := map[ttype]bool{tSTRING: true}

	f := &bytes.Buffer{}
	f.WriteString(appendListGenFileHeader)

	// func init
	fmt.Fprintln(f, "func init() {")
	supportTypes := []ttype{
		tBYTE, tI16, tI32, tI64, tDOUBLE,
		tENUM, tSTRING, tSTRUCT, tMAP, tSET, tLIST,
	}
	t2var := map[ttype]string{
		tBYTE: "tBYTE", tI16: "tI16", tI32: "tI32", tI64: "tI64", tDOUBLE: "tDOUBLE",
		tENUM: "tENUM", tSTRING: "tSTRING",
		tSTRUCT: "tSTRUCT", tMAP: "tMAP", tSET: "tSET", tLIST: "tLIST",
	}
	for _, v := range supportTypes {
		fmt.Fprintf(f, "registerListAppendFunc(%s, %s)\n",
			t2var[v], appendListFuncName(v))
	}
	fmt.Fprintln(f, "}")
	fmt.Fprintln(f, "")

	// func appendList_XXX
	for _, v := range []ttype{tBYTE, tI16, tI32, tI64, tENUM, tSTRING, tOTHER} {
		fmt.Fprintf(f, "func %s(t *tType, b []byte, p unsafe.Pointer) ([]byte, error) {\n",
			appendListFuncName(v))
		fmt.Fprintln(f, "t = t.V")
		fmt.Fprintln(f, "b, n, vp := appendListHeader(t, b, p)")
		fmt.Fprintln(f, "if n == 0 { return b, nil }")
		if defineErr[v] {
			fmt.Fprintln(f, "var err error")
		} else if defineStr[v] {
			fmt.Fprintln(f, "var s string")
		}
		fmt.Fprintln(f, "for i := uint32(0); i < n; i++ {")
		fmt.Fprintln(f, "if i != 0 { vp = unsafe.Add(vp, t.Size) }")
		fmt.Fprintln(f, getAppendCode(v, "t", "vp"))
		fmt.Fprintln(f, "}")
		fmt.Fprintln(f, "return b, nil")
		fmt.Fprintln(f, "}")
		fmt.Fprintln(f, "")
	}

	fileb, err := format.Source(f.Bytes())
	if err != nil {
		t.Log(codeWithLine(f.Bytes()))
		t.Fatal(err)
	}
	err = os.WriteFile(filename, fileb, 0o644)
	if err != nil {
		t.Fatal(err)
	}
	t.Logf("generated: %s", filename)
}
// file: github.com/cloudwego/frugal/internal/reflect/append_map_gen_test.go
package reflect

const appendMapFileName = "append_map_gen.go"

func TestGenAppendMapCode(t *testing.T) {
	if *gencode {
			genAppendMapCode(t, appendMapFileName)
		return
	}
	// ...other logic...
}

func genAppendMapCode(t *testing.T, filename string) {
	f := &bytes.Buffer{}
	f.WriteString(appendMapGenFileHeader)
	
	// func init
	fmt.Fprintln(f, "func init() {")
	supportTypes := []ttype{
		tBYTE, tI16, tI32, tI64, tDOUBLE,
		tENUM, tSTRING, tSTRUCT, tMAP, tSET, tLIST,
	}
	t2var := map[ttype]string{
		tBYTE: "tBYTE", tI16: "tI16", tI32: "tI32", tI64: "tI64", tDOUBLE: "tDOUBLE",
		tENUM: "tENUM", tSTRING: "tSTRING",
		tSTRUCT: "tSTRUCT", tMAP: "tMAP", tSET: "tSET", tLIST: "tLIST",
	}
	for _, k := range supportTypes {
		for _, v := range supportTypes {
			fmt.Fprintf(f, "registerMapAppendFunc(%s, %s, %s)\n",
				t2var[k], t2var[v], appendMapFuncName(k, v))
		}
	}
	fmt.Fprintln(f, "}")
	fmt.Fprintln(f, "")

	// func appendMapXXX
	for _, k := range []ttype{tBYTE, tI16, tI32, tI64, tENUM, tSTRING, tOTHER} {
		for _, v := range []ttype{tBYTE, tI16, tI32, tI64, tENUM, tSTRING, tOTHER} {
			fmt.Fprintf(f, "func %s(t *tType, b []byte, p unsafe.Pointer) ([]byte, error) {\n",
				appendMapFuncName(k, v))
			fmt.Fprintln(f, "b, n := appendMapHeader(t, b, p)")
			fmt.Fprintln(f, "if n == 0 { return b, nil }")
			if defineErr[k] || defineErr[v] {
				fmt.Fprintln(f, "var err error")
			}
			if defineStr[k] || defineStr[v] {
				fmt.Fprintln(f, "var s string")
			}
			fmt.Fprintln(f, "it := newMapIter(rvWithPtr(t.RV, p))")
			fmt.Fprintln(f, "for kp, vp := it.Next(); kp != nil;kp, vp = it.Next() {")
			fmt.Fprintln(f, "n--")
			fmt.Fprintln(f, getAppendCode(k, "t.K", "kp"))
			fmt.Fprintln(f, getAppendCode(v, "t.V", "vp"))
			fmt.Fprintln(f, "}")
			fmt.Fprintln(f, "return b, checkMapN(n)")
			fmt.Fprintln(f, "}")
			fmt.Fprintln(f, "")
		}
	}

	fileb, err := format.Source(f.Bytes())
	if err != nil {
		t.Log(codeWithLine(f.Bytes()))
		t.Fatal(err)
	}
	err = os.WriteFile(filename, fileb, 0o644)
	if err != nil {
		t.Fatal(err)
	}
	t.Logf("generated: %s", filename)
}

这种法子我也是首见,确实是妙,如果是使用一个单独的辅组包专门用于生成所需的代码,也不是不行,就是看着别扭,毕竟frugal是一个库,终端用户是导入这个库然后使用,并不是一个cli可执行程序,潜意识里如果这个是一个go功能库,就不应该或者最好不要存在有main函数。

 Kitex: Thrift-HTTP 映射的 IDL 规范 在go库包中包含main包的方式 

Comments