golang tree 辅助操作

本文介绍了一种树形数据结构的封装方法,包括树的创建、遍历及复制功能,并提供Go语言实现示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在项目开发过程中,发现需要很多树的数据结构,所以封装了一个树的接口,方便操作。

树的生成:

比如树的节点结构定义大概如下:

type struct Node {
	Name string
	Children []Node
	CalcRule string // 自定义属性
}
Name 指节点名字,Children是包含的子节点的数组,还有其他一些字段,就是叶子节点的一些属性了,比如这里的CalcRule

现在有一些节点的数据和他们对应的树的路径,比如:

A: {Name: "node1",  CalcRule: "add"},对应的中间路径是"a" -> "b1" -> "c3";

B: {Name: "node2",  CalcRule: "minus"},对应的中间路径是"a" -> "b1";

C: {Name: "node3",  CalcRule: "mult"},对应的中间路径是"a" -> "b2" -> "c4";

D: {Name: "node4",  CalcRule: "add"},对应的中间路径是"a" -> "b1" -> "c3";

E: {Name: "node5",  CalcRule: "none"},对应的中间路径是"a" -> "b1" -> "c3";

按照这些路径生成好的树的结构图如下:


使用了接口创建这颗树的代码非常简单,如下所示:

func createTree() *Node {
	// 路径数组,每个路径需要包含叶子节点的名称,但是不需要包含根节点的名字
	pathArr := [][]string{
		[]string{b1", "c3", "node1"},
		[]string{b1", "node2"},
		[]string{b2", "c4", "node3"},
		[]string{b1", "c3", "node4"},
		[]string{b1", "c3", "node5"},
	}

	// 每个叶子节点的信息,在叶子节点产生之后赋值使用
	nodeArr := []Node{
		Node{CalcRule: "add"},
		Node{CalcRule: "minus"},
		Node{CalcRule: "mult"},
		Node{CalcRule: "add"},
		Node{CalcRule: "none"},
	}

	// 使用根节点信息初始化整棵树
	rootTree := &Node{
		Name: "a", // 根节点的名称放在这里
	}

	for k, v := range pathArr {
		// 通过路径信息和自定义的创建节点函数生成树的分支,返回的是叶子节点
		leaf := tree.CreateByPath(rootTree, pathArr, rootTree.CreateNode)

		// 增加叶子节点的信息
		leaf.(*Node).CalcRule = nodeArr[k].CalcRule
	}

	// 返回生成好的树
	return rootTree
}

通过tree.CreateByPath函数生成树,参数中rootTree.CreateNode是Node结构的成员函数。为了使用tree.CreateByPath函数,Node结构必须实现NormalTree(自己起的名字)的接口,还需要实现其他几个成员函数,如下所示:

// 满足接口的函数:获取到子节点数组,返回的数组元素必须要实现NormalTree的接口
// 因为children数组的元素是Node,而*Node才实现了NormalTree接口,所以需要将[]Node转换为[]*Node
func (self *Node) GetChildren() []interface{} {
	if nil == self.Children || 0 == len(self.Children) {
		return nil
	}

	arr := make([]interface{}, len(self.Children))
	for k := range self.Children {
		arr[k] = &self.Children[k]
	}

	return arr
}

// 满足接口的函数:增加子节点,其中使用了自定义的create函数。返回结果是满足NormalTree接口的子节点
func (self *Node) AddChild(node map[string]interface{}, create func(map[string]interface{}) interface{}) interface{} {
	child := create(node).(Node)
	self.Children = append(self.Children, child)

	return &self.Children[len(self.Children)-1]
}

// 满足接口的函数:获取节点名字
func (self *Node) GetName() string {
	return self.Name
}

// 在AddChild中使用的create函数,参数data为固定的map:{"value": 节点名称}。返回结果是新的节点
func (self *Node) CreateNode(data map[string]interface{}) interface{} {
	node := Node{
		Name:    data["value"].(string),
	}

	return node
}

下面是tree接口文件tree.go的代码:

package tree

type NormalTree interface {
	GetChildren() []interface{}
	AddChild(map[string]interface{}, func(map[string]interface{}) interface{}) interface{}
	GetName() string
}

func CreateByPath(tree NormalTree, path []string, creatFunc func(map[string]interface{}) interface{}) NormalTree {
	for _, v := range path {
		index := -1
		children := tree.GetChildren()
		for k1, v1 := range children {
			vData := v1.(NormalTree)
			if vData.GetName() == v {
				index = k1
				break
			}
		}

		if -1 == index {
			index = len(children)
			tree.AddChild(map[string]interface{}{"value": v}, creatFunc)
			children = tree.GetChildren()
		}

		tree = children[index].(NormalTree)
	}

	return tree
}

从路径数组生成树的工作就完成了。


有时候我也需要从遍历生成的树,每次写代码用广度优先搜素或者深度优先搜素也很麻烦。所以加了一个将树转换为路径链表的功能。

按照上面生成好的树,转换回来就如下所示:

[]Node{{node2, minus} -> {b1} ->{a},  {node1, add} -> {c3} -> {b1} -> {a}, ......} // 用伪代码,大家理解意思就行。

为了满足这个操作,Node结构还需要增加一个成员和实现另外一个成员函数:

type struct Node {
    Name string
    Children []Node
    Parent *Node // 增加指向父节点的指针
    CalcRule string // 自定义属性
}

func (self *Node) SetParent(parent interface{}) {
	self.Parent = parent.(*Node)
}
tree.go文件中接口定义也需要增加这个函数定义:

SetParent(interface{})
做这个操作的tree.go中函数如下:

func GetReversePath(tree NormalTree) []NormalTree {
	arr := []NormalTree{tree}
	result := make([]NormalTree, 0)

	for len(arr) > 0 {
		next := make([]NormalTree, 0)

		for k, v := range arr {
			children := v.GetChildren()
			if nil != children && len(children) > 0 {
				for k1, v1 := range children {
					v1Data := v1.(NormalTree)
					children[k1].(NormalTree).SetParent(arr[k])
					next = append(next, v1Data)
				}
			} else {
				result = append(result, v)
			}
		}

		arr = next
	}

	return result
}


最后,在加一个树的拷贝函数,可以将一个实现了NormalTree接口的结构A的树拷贝为路径相同的实现了NormalTree的结构B的树。因为这个使用的比较少,就不细说了,把tree.go中的函数列下即可:

type NodeContent interface {
	Decode() map[string]interface{}
}

unc Copy(src NormalTree, dst NormalTree, creatFunc func(map[string]interface{}) interface{}, level int) {
	srcArr := []NormalTree{src}
	dstArr := []NormalTree{dst}

	currentLevel := 1
	for len(srcArr) > 0 && (level <= 0 || currentLevel < level) {
		nextSrcArr := make([]NormalTree, 0)
		nextDstArr := make([]NormalTree, 0)

		for k, v := range srcArr {
			srcChildren := v.GetChildren()
			if nil == srcChildren || 0 == len(srcChildren) {
				continue
			}

			for _, v1 := range srcChildren {
				dstChild := dstArr[k].AddChild(v1.(NodeContent).Decode(), creatFunc)
				if nil == dstChild {
					continue
				}

				v1Data := v1.(NormalTree)
				dstData := dstChild.(NormalTree)

				nextSrcArr = append(nextSrcArr, v1Data)
				nextDstArr = append(nextDstArr, dstData)
			}

		}

		srcArr = nextSrcArr
		dstArr = nextDstArr

		currentLevel += 1
	}
}




### 抽象语法树(AST)在 Go 语言中的应用 抽象语法树(Abstract Syntax Tree,简称 AST),是一种源代码语法结构的抽象表示方式[^1]。对于编程语言而言,AST 是编译过程中的重要中间产物之一,在此过程中,原始代码被解析成一种更易于处理的形式。 #### 使用 `go/ast` 包创建和操作 AST Go 提供了一个名为 `go/ast` 的标准库包专门用来生成和操作 AST 结构[^2]。这个包包含了多种类型定义以及辅助函数,允许开发者轻松地构建、遍历甚至修改程序的 AST 表达形式。下面是一个简单的例子展示了如何利用 `go/parser` 和 `go/ast` 来读取并打印给定 Go 文件的内容: ```go package main import ( "fmt" "go/parser" "go/token" "os" ) func main() { fset := token.NewFileSet() file, err := parser.ParseFile(fset, "example.go", nil, 0) if err != nil { fmt.Fprintf(os.Stderr, "parsing file: %v\n", err) os.Exit(1) } ast.Print(fset, file) } ``` 这段代码首先初始化一个新的文件集 (`token.FileSet`) 并调用 `parser.ParseFile()` 函数来加载指定路径下的 Go 源码文件到内存中作为 AST 节点对象;最后通过 `ast.Print()` 方法输出整个 AST 到控制台以便查看其内部结构。 #### 修改现有 AST 实现自定义逻辑 当涉及到对已有的 AST 进行更改时,则可能需要用到更多高级特性,比如访问者模式或是递归下降解析器等技术手段。这里给出一个基于命令行工具 go generate 自动化脚本的例子,其中涉及到了对特定格式注释行(`//go:generate ...`)的操作[^3]: ```go type Generator struct { r io.Reader } func (g *Generator) run() (ok bool) { input := bufio.NewReader(g.r) for { var buf []byte buf, err = input.ReadSlice('\n') if err != nil { if err == io.EOF && isGoGenerate(buf) { err = io.ErrUnexpectedEOF } break } if !isGoGenerate(buf) { continue } g.setEnv() words := g.split(string(buf)) g.exec(words) } return true } ``` 上述片段是从官方文档简化而来的一个版本,实际应用场景可能会更加复杂一些。它实现了基本的功能——识别特殊的注解语句,并据此触发相应的动作。 #### 加载外部依赖项的信息 有时候还需要获取关于导入模块的具体细节,这可以通过调整 swag 中使用的 loader 工具实现[^4]。例如,要查找某个包名对应的实际名称,可以这样做: ```go func (pkgDefs *PackagesDefinitions) parseImportName(importPath string) string { conf := loader.Config{ ParserMode: goparser.PackageClauseOnly, } conf.Import(importPath) loaderProgram, err := conf.Load() if err != nil { return "" } for _, info := range loaderProgram.AllPackages { pkgPath := strings.TrimPrefix(info.Pkg.Path(), "vendor/") if pkgPath == importPath { return info.Pkg.Name() } } return "" } ``` 这种方法能够有效地帮助理解项目间的相互关系及其各自的命名空间布局情况。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值