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