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

更新日期: 2024-11-19 阅读次数: 182 字数: 1871 分类: golang

上周基于 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 聊聊, 查看更多联系方式