go结构体扫描

1.java类扫描工具的使用场景

在使用java开发应用的时候,我们可以使用类扫描器,扫描所有已加载的文件,对符合目标的class进行过滤。典型地,spring基于此种方式,扫描带有指定注解的类进行实例初始化与自动注入。

例如,jforgame框架大量使用类扫描工具,进行消息注册,路由实例化等等。例如,所有的消息,只要拥有Message注解,便会自动注册到客户端与服务器的消息工厂里。如下:

java 复制代码
@MessageMeta(cmd = 55555)
public class ReqLogin implements Message {

    private long playerId;

    public long getPlayerId() {
        return playerId;
    }

    public void setPlayerId(long playerId) {
        this.playerId = playerId;
    }
}

2.使用go扫描结构体

2.1手动注册结构体的不便之处

如果没有结构体扫描工具,当我们需要实现同样的逻辑,例如,注册所有的通信协议。我们不得不采用以下的方式。

Go 复制代码
network.RegisterMessage(protos.CmdHeroReqRecruit, &protos.ReqHeroRecruit{})
network.RegisterMessage(protos.CmdHeroResRecruit, &protos.ResHeroRecruit{})
network.RegisterMessage(protos.CmdHeroPushAllHero, &protos.PushAllHeroInfo{})

network.RegisterMessage(protos.CmdHeroReqLevelUp, &protos.ReqHeroLevelUp{})
network.RegisterMessage(protos.CmdHeroResLevelUp, &protos.ResHeroLevelUp{})
network.RegisterMessage(protos.CmdHeroPushAdd, &protos.PushHeroAdd{})
network.RegisterMessage(protos.CmdHeroPushAttrChange, &protos.PushHeroAttrChange{})

每个模块,对需要手动注册所有的通信协议,非常麻烦。

为此,我们探索一种可以自动扫描结果体的方式。

2.2.使用ast解析源文件

go不是传统的oop语言,网上查询,也没有找到一种可以在运行期,查找已注册的结构体的API。为此,我们不得不采用直接解析源代码文件的方式。例如,使用正则的方式,扫描指定目录的所有的文件,将属于struct定义的所有结构体进行统计预处理。遗憾的是,每个人写代码的风格差异性比较大,使用正则的方式,解析的难度非常大。

幸运的是, go自带的ast工具库,可以很好的解决这个问题。

Go语言的AST工具库概述

Go语言的ast(Abstract Syntax Tree,抽象语法树)工具库是标准库go/ast的一部分,用于解析和分析Go源代码的结构。该库提供了对Go代码的语法树表示,允许开发者通过程序化的方式操作和分析代码结构,常用于代码生成、静态分析、格式化工具等场景。

go/ast库能够将Go源代码解析为抽象语法树,每个节点代表代码中的元素(如函数、变量、表达式等)。例如,ast.File表示一个Go文件,ast.FuncDecl表示函数声明。

使用ast解析源代码文件

Go 复制代码
// parseGoFile 通用AST解析
func (b *BaseGenerator) parseGoFile(filePath string) ([]structInfo, error) {
	fset := token.NewFileSet()
	node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
	if err != nil {
		return nil, err
	}

	cm := ast.NewCommentMap(fset, node, node.Comments)
	var structInfos []structInfo

	ast.Inspect(node, func(n ast.Node) bool {
		ts, ok := n.(*ast.TypeSpec)
		if !ok {
			return true
		}

		structType, ok := ts.Type.(*ast.StructType)
		if !ok {
			return true
		}

		structName := ts.Name.Name
		structLine := fset.Position(ts.Pos()).Line
		var fields []structField

		if structType.Fields != nil {
			for _, field := range structType.Fields.List {
				var fieldName string
                if len(field.Names) > 0 {
                    fieldName = field.Names[0].Name
                } else {
                    continue
                }
                if fieldName == "_" {
                    continue
                }

				fieldType := b.getFieldTypeStr(field.Type)
				fieldComment := b.getCommentText(field.Comment)
				jsonTag := ""
				if field.Tag != nil {
					tagStr := strings.Trim(field.Tag.Value, "`")
					jsonTag = b.extractJsonTag(tagStr)
				}

				fields = append(fields, structField{
					Name:    fieldName,
					Type:    fieldType,
					Comment: fieldComment,
					JsonTag: jsonTag,
				})
			}
		}

		// 提取结构体注释(按行号过滤)
		var structComment string
		commentGroups := cm.Filter(ts).Comments()
		for _, cg := range commentGroups {
			for _, c := range cg.List {
				commentLine := fset.Position(c.Pos()).Line
				if commentLine > structLine {
					continue
				}

				text := strings.TrimSpace(c.Text)
				if strings.HasPrefix(text, "//go:") {
					continue
				}
				text = strings.TrimPrefix(text, "//")
				text = strings.TrimPrefix(text, "/*")
				text = strings.TrimSuffix(text, "*/")
				text = strings.TrimSpace(text)
				if text != "" {
					if structComment != "" {
						structComment += "\n"
					}
					structComment += text
				}
			}
		}

		if structComment == "" {
			structComment = b.getCommentText(ts.Doc)
			if structComment == "" {
				structComment = b.getCommentText(ts.Comment)
			}
		}

		structInfos = append(structInfos, structInfo{
			Name:    structName,
			Comment: structComment,
			Fields:  fields,
		})

		return true
	})

	return structInfos, nil
}

对指定的文件目录,解析得到所有关于通信消息struct的元信息。

Go 复制代码
// structField 存储单个结构体字段信息
type structField struct {
	Name    string // 字段名(如 Channel)
	Type    string // Go 类型(如 int)
	Comment string // 注释(如 发送频道:1个人 2世界)
	JsonTag string // JSON Tag(如 channel)
}

// structInfo 存储单个结构体信息
type structInfo struct {
	Name    string        // 结构体名(如 ReqChat)
	Comment string        // 结构体注释(如 聊天请求)
	Fields  []structField // 字段列表
}

聊聊几行代码,就能自动扫描所有人结构体,非常方便。

2.3.生产环境扫描"曲线救国"

开发阶段,使用该工具,确实实现了预期目标,但遗憾的是,由于我们采用的是,基于源代码解析的方式。当程序部署到生产环境,由于所有的源代码已经被编译打包,已经没有了源代码文件,这种方式,一下子失去了解析的基础,宣告失败。

聪明的朋友,可能想到一些解决问题的技巧。笔者想到的是,在开发阶段,将所有已扫描的信息收集起来,反过来翻译成go源代码,如此在打包的时候,就能将所有已解析好的数据被go程序所识别,翻译出的目标代码如下所示:

Go 复制代码
package protos

type ReqHeroRecruit struct {
	_     struct{} `cmd_ref:"CmdHeroReqRecruit" type:"req"`
	Times int32    `json:"times"`
}

type ResHeroRecruit struct {
	_           struct{}      `cmd_ref:"CmdHeroResRecruit" type:"res"`
	Code        int32         `json:"code"`
	RewardInfos []*RewardInfo `json:"rewardInfos"`
}

type PushAllHeroInfo struct {
	_     struct{}    `cmd_ref:"CmdHeroPushAllHero" type:"push"`
	Heros []*HeroInfo `json:"heros"`
}

type AttrInfo struct {
	AttrType string `json:"attrType"`
	Value    int32  `json:"value"`
}

type HeroInfo struct {
	Id       int32      `json:"id"`
	Level    int32      `json:"level"`
	Position int32      `json:"position"`
	Stage    int32      `json:"stage"`
	Exp      int32      `json:"exp"`
	Hp       int32      `json:"hp"`
	Attrs    []AttrInfo `json:"attrs"`
	Fight    int32      `json:"fight"`
}

type ReqHeroLevelUp struct {
	_       struct{} `cmd_ref:"CmdHeroReqLevelUp" type:"req"`
	HeroId  int32    `json:"heroId"`
	ToLevel int32    `json:"toLevel"`
}

这些代码是通过ast解析源文件生成的"二次源代码",有点类似于"元编程"。我们只需要在开发环境,先启动一次,自动生成上面的源代码文件,然后第二次启动,便能将以上的文件作为源文件,由go程序加载。此种方式,也实现了生成环境的部署需求。

思想成型了,代码也就应运而生,比较简单,如下所示:

Go 复制代码
func (b *BaseGenerator) GenerateRegisterFromTags(goDir string, outputFile string, msgConsts map[string]int) error {
    if msgConsts == nil || len(msgConsts) == 0 {
        msgConsts = b.buildMsgConstMap(goDir + "\\" + "message.go")
    }
    files, err := os.ReadDir(goDir)
    if err != nil {
        return fmt.Errorf("读取Go目录失败:%w", err)
    }

    type entry struct{ Type string; Cmd int; FileName string ;}
    entries := make([]entry, 0, 64)
    var buf bytes.Buffer
    buf.WriteString("package protos\n\n")
    buf.WriteString("import (\n\t\"io/github/gforgame/network\"\n)\n\n")
    buf.WriteString("func init() {\n")

    fset := token.NewFileSet()
    for _, file := range files {
        if file.IsDir() || !strings.HasSuffix(file.Name(), ".go") {
            continue
        }
        filePath := goDir + "\\" + file.Name()
        node, err := parser.ParseFile(fset, filePath, nil, 0)
        if err != nil {
            continue
        }

		
        ast.Inspect(node, func(n ast.Node) bool {
            ts, ok := n.(*ast.TypeSpec)
            if !ok {
                return true
            }
            st, ok := ts.Type.(*ast.StructType)
            if !ok {
                return true
            }
            typeName := ts.Name.Name
            if st.Fields != nil {
                for _, fld := range st.Fields.List {
                    if fld.Tag == nil {
                        continue
                    }
                    tagStr := strings.Trim(fld.Tag.Value, "`")
                    tags := b.parseAllTags(tagStr)
                    cmd := 0
                    found := false
                    if ref, ok := tags["cmd_ref"]; ok {
                        if v, ok2 := msgConsts[ref]; ok2 {
                            cmd = v
                            found = true
                        }
                    } else if s, ok := tags["cmd"]; ok {
                        if v, err := strconv.Atoi(s); err == nil {
                            cmd = v
                            found = true
                        }
                    }
                    if found {
                        entries = append(entries, entry{Type: typeName, Cmd: cmd, FileName: file.Name()})
                        break
                    }
                }
            }
            return true
        })
    }


    grouped := make(map[string][]entry)
    for _, e := range entries {
        grouped[e.FileName] = append(grouped[e.FileName], e)
    }
    for fileName, list := range grouped {
        buf.WriteString(fmt.Sprintf("\t// ----from %s----\n", fileName))
        for _, e := range list {
            buf.WriteString(fmt.Sprintf("\tnetwork.RegisterMessage(%d, &%s{})\n", e.Cmd, e.Type))
        }
        buf.WriteString("\n")
    }
    buf.WriteString("}\n")

    if err := os.WriteFile(outputFile, buf.Bytes(), 0644); err != nil {
        return fmt.Errorf("写入文件失败:%w", err)
    }
    return nil
}

详细的代码,请移步:

go游戏服务器

实际的使用方式,还将解析的协议元信息,进一步翻译成客户端需要的协议源文件,包含typescript或csharp版本。

当然,解决go自动扫描结构体的方式,可能还有更好的实现方式,受笔者水平所限。欢迎评论区交流,不胜感激~~

相关推荐
分***82 小时前
MAME街机模拟器下载 支持全街机游戏 多系统适配 中文设置教程
游戏·mame街机模拟器·拳皇模拟器·三国战记模拟器·合金弹头模拟器
会员源码网3 小时前
游戏行业专属支付通道搭建 – 聚合支付系统支持当面付与三方支付
游戏·源代码管理
呆呆敲代码的小Y3 小时前
【Unity 实用工具篇】| UX Tool 工具 快速上手使用,提高日常开发效率
游戏·unity·游戏引擎·游戏程序·ux
汪小成12 小时前
Go 项目结构总是写乱?这个 50 行代码的 Demo 教你标准姿势
后端·go
skywalk816314 小时前
wow文件处理trinitycore的文件处理
开发语言·游戏
youngee1117 小时前
hot100-64跳跃游戏
算法·游戏
CodeCaptain20 小时前
CocosCreator3.8.x 解析Tiled1.4.x【瓦片图层、对象图层、图像图层、组图层】的核心原理
经验分享·游戏·typescript·cocos2d
mg6681 天前
0基础开发学习python工具_____用 Python 从零写一个贪吃蛇游戏:完整实现 + 打包成 .exe(附源码)
python·游戏·pygame·python开发
CodeCaptain1 天前
Cocos Creator 3.8.x 读取 Tiled 1.4.x 原理分析
经验分享·游戏