Golang AST 解析 struct 字段,自动生成 CRUD 代码

文章目录

    上周基于 cobra 实现了一个 golang 的命令行工具, 参考:golang 快速开发命令行工具的神器 cobra & cobra cli,实现了一键生成 go gin 后台,及 react ant design 前端的 CRUD 工具。
    大大提升了枯燥的 CRUD 劳作效率。并在两个项目上试水成功。
    但是,还有一点不够完美,就是目前的 ant design 前端部分,只是个界面架子。
    具体的编辑字段,还得手动一个个添加。这周又接到了一个无数 CRUD 的搬砖项目,我觉得有必要把这部分功能加上了。
    这样才能无愧于我的“搬砖之王”的称号。

    功能需求

    即,使用 golang 解析一个 golang 的包含 struct 的 model 文件,自动解析出每个字段的名称,及类型。
    然后自动生成:

    • react ant design 前端字段编辑界面,及列表展示界面
    • MySQL 创建表的 SQL
    • 自动填充 gorm 的可更新字段列表

    生成命令

    使用 cobra cli 添加一个新命令:

    > cobra-cli add parseStruct
    

    当然,如果是要通过 go generate 集成到项目中,并不需要 cobra 这类命令行工具。

    解析 go 源码文件

    用 AI 生成了一个代码架子,并稍作修改:

    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())
    				}
    			}
    		}
    	}
    }
    

    示例 model 代码

    假设,我在 models 目录下有个 device.go 的 go 文件。
    定义了设备信息的结构体,用于 gorm 的数据库操作:

    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"
    }
    

    解析结果

    运行命令,得到的解析结果如下:

    > 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"`}, 注释: 收藏总数
    

    可以看到,字段名称,类型,及 tag 和注释都能正确的解析出来了。

    后续

    后面,就可以逐一处理每个字段,针对不同类型,生成不同的前端 ant design 组件代码了。

    什么是 AST

    这里用到了三个库:

    • go/parser:用于解析 Go 源代码并生成 AST。
    • go/token:用于管理源代码的位置和标记。
    • go/ast:用于表示和操作 AST。

    AST, 英文全程是 Abstract Syntax Tree, 即抽象语法树。

    抽象语法树是源代码的一种抽象表示形式,它以树状结构来展现程序的语法结构,将代码中的各种语法元素(如语句、表达式、类型定义等)转化为节点,节点之间通过父子关系等连接,能够更清晰地体现代码的逻辑和语法构成,而忽略掉诸如空格、括号等具体的语法细节(即词法细节)。

    查看 go 的 ast.go 实现代码,可以看到,其定义了一些常见的语法元素:

    // 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)
    }
    

    例如,常用的 swagger 库 swaggo 就是基于 AST 解析结果,然后再分析注释代码,生成 swagger 操作界面:

    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
    }
    

    参考

    关于作者 🌱

    我是来自山东烟台的一名开发者,有感兴趣的话题,或者软件开发需求,欢迎加微信 zhongwei 聊聊,或者关注我的个人公众号“大象工具”, 查看更多联系方式