Golang AST parses struct fields to automatically generate CRUD code

Last week I implemented a command-line tool for golang based on cobra, cf:golang Rapid development of command line tools for the gods cobra & cobra cliThis is a CRUD tool that realizes one-click generation of go gin backend and react ant design frontend. It greatly improves the efficiency of the boring CRUD labor. And in two projects to test the waters successfully. However, there is a little less than perfect, is the current ant design front-end part, just an interface shelf. Specific edit fields, still have to manually add one by one. This week, I got another bricklaying project with countless CRUDs, and I felt the need to add this part of the functionality. This way I can live up to my title of “King of Bricks”.

functional requirement

I.e., use golang to parse a golang model file containing a struct, and automatically parse out the name and type of each field. Then it generates them automatically:

  • react ant design front-end field editing interface, and list display interface
  • MySQL SQL for Creating Tables
  • Auto-populate gorm’s list of updatable fields

Generate command

Add a new command using cobra cli:

> cobra-cli add parseStruct 

Of course, you don’t need a command-line tool like cobra if you want to integrate it into your project via go generate.

Parsing go source files

A code shelf was generated with AI and slightly modified:

package cmd 
 
import ( 
	"fmt" 
	"go/ast" 
	"go/parser" 
	"go/token" 
 
	"github.com/spf13/cobra" 
) 
 
// parseStructCmd represents the parseStruct command 
var parseStructCmd = &cobra.Command{ 
	Use:     "parseStruct", 
	Short:   "解析 model 文件中的 struct 字段,生成建表 SQL 及 antd pro 字段, 及可 update 字段列表", 
	Args:    cobra.ExactArgs(1), // 参数为 model 文件路径 
	Example: "go_snip parseStruct models/device.go", 
	Run:     parseStruct, 
} 
 
func init() { 
	rootCmd.AddCommand(parseStructCmd) 
} 
 
func parseStruct(cmd *cobra.Command, args []string) { 
	filePath := args[0] 
	fset := token.NewFileSet() 
	// 解析Go文件 
	file, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments) 
	if err != nil { 
		fmt.Printf("解析文件失败:%v\n", err) 
		return 
	} 
 
	// 遍历文件中的所有声明 
	for _, decl := range file.Decls { 
		// 检查声明是否是结构体类型声明 
		genDecl, ok := decl.(*ast.GenDecl) 
		if ok && genDecl.Tok == token.TYPE && len(genDecl.Specs) > 0 { 
			typeSpec := genDecl.Specs[0].(*ast.TypeSpec) 
			structType, ok := typeSpec.Type.(*ast.StructType) 
			if ok { 
				// 输出结构体名称 
				fmt.Printf("结构体名称:%s\n", typeSpec.Name.Name) 
				// 遍历结构体的字段 
				for _, field := range structType.Fields.List { 
					fmt.Printf("名称:%s, 类型:%s, tag: %v, 注释: %s \n", 
						field.Names[0].Name, field.Type, field.Tag, field.Comment.Text()) 
				} 
			} 
		} 
	} 
} 

Example model code

Suppose, I have a go file device.go in the models directory. It defines a structure with device information for gorm’s database operations:

package models 
 
import "time" 
 
type Device struct { 
	ID           uint 
	CreatedAt    time.Time 
	UpdatedAt    time.Time 
	Name         string // 设备名称 
	Model        string // 型号 
	Manufacturer string // 生产厂家 
	Address      string // 地址 
	Admin        string // 负责人姓名 
	Tel          string // 联系电话 
	Images       string // 设备照片。多张,地址使用英文逗号分隔 
	Attachments  string // 附件。支持多个附近,地址使用英文逗号分隔 
	TotalCollect int    `json:"total"` // 收藏总数 
} 
 
func (Device) TableName() string { 
	return "device" 
} 

parsing result

Running the command yields the following parsing results:

> go run main.go parseStruct <some_project>/models/device.go 
 
结构体名称:Device 
名称:ID, 类型:uint, tag: <nil>, 注释: 
名称:CreatedAt, 类型:&{time Time}, tag: <nil>, 注释: 
名称:UpdatedAt, 类型:&{time Time}, tag: <nil>, 注释: 
名称:Name, 类型:string, tag: <nil>, 注释: 设备名称 
名称:Model, 类型:string, tag: <nil>, 注释: 型号 
名称:Manufacturer, 类型:string, tag: <nil>, 注释: 生产厂家 
名称:Address, 类型:string, tag: <nil>, 注释: 地址 
名称:Admin, 类型:string, tag: <nil>, 注释: 负责人姓名 
名称:Tel, 类型:string, tag: <nil>, 注释: 联系电话 
名称:Images, 类型:string, tag: <nil>, 注释: 设备照片。多张,地址使用英文逗号分隔 
名称:Attachments, 类型:string, tag: <nil>, 注释: 附件。支持多个附近,地址使用英文逗号分隔 
名称:TotalCollect, 类型:int, tag: &{518 STRING `json:"total"`}, 注释: 收藏总数 

As you can see, the field names, types, and tags and comments are parsed correctly.

(dialect) remarry

Later, you can handle each field one by one and generate different front-end ant design component code for different types.

What is AST

Three libraries are used here:

  • go/parser: used to parse Go source code and generate ASTs.
  • go/token: location and token for managing source code.
  • go/ast: used to represent and manipulate ASTs.

AST, the whole English is Abstract Syntax Tree, that is, Abstract Syntax Tree.

Abstract syntax tree is an abstract representation of the source code, which shows the syntactic structure of the program in a tree-like structure, transforming various syntactic elements in the code (e.g., statements, expressions, type definitions, etc.) into nodes, and connecting nodes to each other through parent-child relationships and so on, which can more clearly reflect the logical and syntactic composition of the code, while ignoring specific syntactic details such as spaces, parentheses, and so on (i.e., lexical details).

Looking at the code for go’s ast.go implementation, you can see that it defines some common syntax elements:

// All node types implement the Node interface. 
type Node interface { 
	Pos() token.Pos // position of first character belonging to the node 
	End() token.Pos // position of first character immediately after the node 
} 
 
// All expression nodes implement the Expr interface. 
type Expr interface { 
	Node 
	exprNode() 
} 
 
// All statement nodes implement the Stmt interface. 
type Stmt interface { 
	Node 
	stmtNode() 
} 
 
// All declaration nodes implement the Decl interface. 
type Decl interface { 
	Node 
	declNode() 
} 
 
// Comments 
type Comment struct { 
	Slash token.Pos // position of "/" starting the comment 
	Text  string    // comment text (excluding '\n' for //-style comments) 
} 

For example, the popular swagger library swaggo is based on parsing ASTs and then analyzing the annotated code to generate the swagger interface:

https://github.com/swaggo/swag/blob/master/parser.go

import goparser "go/parser" 
// ParseGeneralAPIInfo parses general api info for given mainAPIFile path. 
func (parser *Parser) ParseGeneralAPIInfo(mainAPIFile string) error { 
	fileTree, err := goparser.ParseFile(token.NewFileSet(), mainAPIFile, nil, goparser.ParseComments) 
	if err != nil { 
		return fmt.Errorf("cannot parse source files %s: %s", mainAPIFile, err) 
	} 
 
	parser.swagger.Swagger = "2.0" 
 
	for _, comment := range fileTree.Comments { 
		comments := strings.Split(comment.Text(), "\n") 
		if !isGeneralAPIComment(comments) { 
			continue 
		} 
 
		err = parseGeneralAPIInfo(parser, comments) 
		if err != nil { 
			return err 
		} 
	} 
 
	return nil 
} 

consultation