https://github.com/jfeliu007/goplantuml/是一款工具可以解析golang源码,生成plantuml类图,它的原理是通过源码静态分析,提取结构体,接口,方法,包等属性,然后按照uml语法生成puml文件,效果如下。

golang数据库分析实例(golang源码分析goplantuml)(1)

首先看下如何使用:

go get github.com/jfeliu007/goplantuml/parser go install github.com/jfeliu007/goplantuml/cmd/goplantuml@latest goplantuml $GOPATH/src/github.com/jfeliu007/goplantuml/parser > ClassDiagram.puml // Generates a file ClassDiagram.puml with the previous specifications

然后我们就可以借助vscode plantuml插件生成上面的类图。熟悉完使用后我们来分析下它的源码。它的入口函数位于:cmd/goplantuml/main.go

func main() { recursive := flag.bool("recursive", false, "walk all directories recursively") ignore := flag.String("ignore", "", "comma separated list of folders to ignore") showAggregations := flag.Bool("show-aggregations", false, "renders public aggregations even when -hide-connections is used (do not render by default)") hideFields := flag.Bool("hide-fields", false, "hides fields") renderingOptions := map[goplantuml.RenderingOption]interface{}{ legend, err := getLegend(renderingOptions) dirs, err := getDirectories() dirAbs, err := filepath.Abs(dir) ignoredDirectories, err := getIgnoredDirectories(*ignore) result, err := goplantuml.NewClassDiagram(dirs, ignoredDirectories, *recursive) result.SetRenderingOptions(renderingOptions) rendered := result.Render() writer, err = os.Create(*output) fmt.Fprint(writer, rendered)

解析完参数之后,将所有选项放入map,然后获取源码目录,以及忽略的目录,接着进行源码解析,提取生成plantuml需要的元数据信息后,调用渲染函数生成plantuml文件,最后输出。如果制定了标签,也会调用下面函数获取标签。

func getLegend(ro map[goplantuml.RenderingOption]interface{}) (string, error) {

它的核心解析函数定义在parser/class_parser.go

func NewClassDiagram(directoryPaths []string, ignoreDirectories []string, recursive bool) (*ClassParser, error) { return NewClassDiagramWithOptions(options)

func NewClassDiagramWithOptions(options *ClassDiagramOptions) (*ClassParser, error) { classParser := &ClassParser{ renderingOptions: &RenderingOptions{ structure: make(map[string]map[string]*struct), allInterfaces: make(map[string]struct{}), allStructs: make(map[string]Struct{}), allImports: make(map[string]string), allAliases: make(map[string]*Alias), allRenamedStructs: make(map[string]map[string]string), for _, dir := range options.IgnoredDirectories { ignoreDirectoryMap[dir] = struct{}{} for _, directoryPath := range options.Directories { if options.Recursive { err := afero.Walk(options.FileSystem, directoryPath, func(path string, info os.fileInfo, err error) error { classParser.parseDirectory(path) classParser.parseDirectory(path) for s := range classParser.allStructs { st := classParser.getStruct(s) if st != nil { for i := range classParser.allInterfaces { inter := classParser.getStruct(i) if st.ImplementsInterface(inter) { st.AddToExtends(i) } } } } classParser.SetRenderingOptions(options.RenderingOptions)

可以看到在准备完可选参数后,定义了一系列map,从名字到结构体信息映射,从名字到接口信息映射。用来存放提取的结构体信息,然后调用 afero.Walk,遍历所有目录和子目录。完成需要信息的提取。该函数是著名的spf13定义的一个文件接口包的一个函数:github.com/spf13/afero

在每个目录内都会调用解析函数进行解析,解析函数如下:

func (p *ClassParser) parseDirectory(directoryPath string) error { fs := token.NewFileSet() result, err := parser.ParseDir(fs, directoryPath, nil, 0) for _, v := range result { p.parsePackage(v)

它调用了golang源码的解析器,解析每个目录下的.go文件,返回包名到抽象语法树的映射。

func ParseDir(fset *token.FileSet, path string, filter func(fs.FileInfo) bool, mode Mode) (pkgs map[string]*ast.Package, first error) {

得到抽象语法树以后,就会遍历所有抽象语法树,解析每个包内的信息。

func (p *ClassParser) parsePackage(node ast.Node) { pack := node.(*ast.Package) p.currentPackageName = pack.Name for fileName := range pack.Files { sortedFiles = append(sortedFiles, fileName) for _, fileName := range sortedFiles { if !strings.HasSuffix(fileName, "_test.go") { f := pack.Files[fileName] for _, d := range f.Imports { p.parseImports(d) } for _, d := range f.Decls { p.parseFiledeclarations(d) } } }

在每个包内依次递归解析它的下一层结构的信息:

func (p *ClassParser) parseImports(impt *ast.ImportSpec) { p.allImports[impt.Name.Name] = s

解析过程中,提取了两类信息:

func (p *ClassParser) parseFileDeclarations(node ast.Decl) { switch decl := node.(type) { case *ast.GenDecl: p.handleGenDecl(decl) case *ast.FuncDecl: p.handleFuncDecl(decl) }

其中GenDecl包括下面四类信息:

// token.IMPORT *ImportSpec // token.CONST *ValueSpec // token.TYPE *typeSpec // token.VAR *ValueSpec

func (p *ClassParser) handleGenDecl(decl *ast.GenDecl) { for _, spec := range decl.Specs { p.processSpec(spec) }

对每个类型的内部每一部分也会进行处理:

func (p *ClassParser) processSpec(spec ast.Spec) { switch v := spec.(type) { case *ast.TypeSpec: typeName = v.Name.Name switch c := v.Type.(type) { case *ast.StructType: declarationType = "class" handleGenDecStructType(p, typeName, c) case *ast.InterfaceType: declarationType = "interface" handleGenDecInterfaceType(p, typeName, c) default: basicType, _ := getFieldType(getBasicType(c), p.allImports) aliasType, _ := getFieldType(c, p.allImports) aliasType = replacePackageConstant(aliasType, "") if !isPrimitiveString(typeName) { typeName = fmt.Sprintf("%s.%s", p.currentPackageName, typeName) } packageName := p.currentPackageName if isPrimitiveString(basicType) { packageName = builtinPackageName } alias = getNewAlias(fmt.Sprintf("%s.%s", packageName, aliasType), p.currentPackageName, typeName) } default: p.getOrCreateStruct(typeName).Type = declarationType switch declarationType { case "interface": p.allInterfaces[fullName] = struct{}{} case "class": p.allStructs[fullName] = struct{}{} case "alias": p.allAliases[typeName] = alias if strings.Count(alias.Name, ".") > 1 { pack := strings.SplitN(alias.Name, ".", 2) if _, ok := p.allRenamedStructs[pack[0]]; !ok { p.allRenamedStructs[pack[0]] = map[string]string{} } renamedClass := generateRenamedStructName(pack[1]) p.allRenamedStructs[pack[0]][renamedClass] = pack[1] } }

对于结构体类型,会纯粹在Struct的结构体里:

func handleGenDecStructType(p *ClassParser, typeName string, c *ast.StructType) { for _, f := range c.Fields.List { p.getOrCreateStruct(typeName).AddField(f, p.allImports) } }

func (p *ClassParser) getOrCreateStruct(name string) *Struct { result = &Struct{ PackageName: p.currentPackageName, Functions: make([]*Function, 0), Fields: make([]*Field, 0), Type: "", Composition: make(map[string]struct{}, 0), Extends: make(map[string]struct{}, 0), Aggregations: make(map[string]struct{}, 0), privateAggregations: make(map[string]struct{}, 0), }

interface的处理是类似的:

func handleGenDecInterfaceType(p *ClassParser, typeName string, c *ast.InterfaceType) { for _, f := range c.Methods.List { switch t := f.Type.(type) { case *ast.FuncType: p.getOrCreateStruct(typeName).AddMethod(f, p.allImports) break case *ast.Ident: f, _ := getFieldType(t, p.allImports) st := p.getOrCreateStruct(typeName) f = replacePackageConstant(f, st.PackageName) st.AddToComposition(f) break } }

函数的处理:

func (p *ClassParser) handleFuncDecl(decl *ast.FuncDecl) { theType, _ := getFieldType(decl.Recv.List[0].Type, p.allImports) theType = replacePackageConstant(theType, "") structure := p.getOrCreateStruct(theType) structure.AddMethod(&ast.Field{ Names: []*ast.Ident{decl.Name}, Doc: decl.Doc, Type: decl.Type, Tag: nil, Comment: nil, }, p.allImports)

func (p *ClassParser) getStruct(structName string) *Struct {

解析完成后设置渲染选项,为最终渲染成plantuml做准备:

func (p *ClassParser) SetRenderingOptions(ro map[RenderingOption]interface{}) error { for option, val := range ro { switch option { case RenderAggregations:

其中解析过程中用到了go语言本性的包

"go/ast" "go/parser" "go/token"

渲染的过程,就是将解析得到的元数据信息,渲染成plantuml语法

func (p *ClassParser) Render() string { str.WriteLineWithDepth(0, "@startuml") str.WriteLineWithDepth(0, fmt.Sprintf(`title %s`, p.renderingOptions.Title)) str.WriteLineWithDepth(0, "legend") str.WriteLineWithDepth(0, note) str.WriteLineWithDepth(0, "end legend") for pack := range p.structure { packages = append(packages, pack) for _, pack := range packages { structures := p.structure[pack] p.renderStructures(pack, structures, str) if p.renderingOptions.Aliases { p.renderAliases(str) str.WriteLineWithDepth(0, "@enduml")

对于每个具体结构也是递归进行渲染的和解析过程是类似的

func (p *ClassParser) renderStructures(pack string, structures map[string]*Struct, str *LineStringBuilder) { str.WriteLineWithDepth(0, fmt.Sprintf(`namespace %s {`, pack)) for _, name := range names { structure := structures[name] p.renderStructure(structure, pack, name, str, composition, extends, aggregations) for _, tempName := range orderedRenamedStructs { name := p.allRenamedStructs[pack][tempName] str.WriteLineWithDepth(1, fmt.Sprintf(`class "%s" as %s {`, name, tempName)) if p.renderingOptions.Compositions { str.WriteLineWithDepth(0, composition.String()) } if p.renderingOptions.Implementations { str.WriteLineWithDepth(0, extends.String()) } if p.renderingOptions.Aggregations { str.WriteLineWithDepth(0, aggregations.String()) }

结构体的渲染

func (p *ClassParser) renderStructure(structure *Struct, pack string, name string, str *LineStringBuilder, composition *LineStringBuilder, extends *LineStringBuilder, aggregations *LineStringBuilder) { switch structure.Type { case "class": sType = "<< (S,Aquamarine) >>" case "alias": sType = "<< (T, #FF7700) >> " renderStructureType = "class" } p.renderStructFields(structure, privateFields, publicFields) p.renderStructMethods(structure, privateMethods, publicMethods) p.renderCompositions(structure, name, composition) p.renderExtends(structure, name, extends) p.renderAggregations(structure, name, aggregations)

其中渲染的目标就是string.Builder

type LineStringBuilder struct { strings.Builder }

如果有别名会解析到:parser/alias.go

type Alias struct { Name string PackageName string AliasOf string }

结构体被解析到:parser/struct.go

type Struct struct { PackageName string Functions []*Function Fields []*Field Type string Composition map[string]struct{} Extends map[string]struct{} Aggregations map[string]struct{} PrivateAggregations map[string]struct{} }

判断一个结构体是否实现了接口,就是判断结构体的方法签名和接口所有的函数列表是否一致

func (st *Struct) ImplementsInterface(inter *Struct) bool { for _, f1 := range inter.Functions { for _, f2 := range st.Functions { if f1.SignturesAreEqual(f2) {

func (st *Struct) AddToExtends(fType string) { st.Extends[fType] = struct{}{}

函数的信息描述位于:parser/function.go

type Function struct { Name string Parameters []*Field ReturnValues []string PackageName string FullNameReturnValues []string }

判断签名是否一致,会判断输入和返回信息:

func (f *Function) SignturesAreEqual(function *Function) bool { result = result && (len(f.Parameters) == len(function.Parameters))

结构体字段信息描述位于:parser/field.go

type Field struct { Name string Type string FullType string }

总结起来,这类工具大同小异,都是解析源码获取元数据信息,然后将元数据信息翻译成目标语言或者协议

,