diff --git a/dixhttp/README.md b/dixhttp/README.md index adf7cd9..3e679f7 100644 --- a/dixhttp/README.md +++ b/dixhttp/README.md @@ -12,7 +12,11 @@ This module provides an HTTP server to visualize dependency relationships in the - 🔄 **Bidirectional Dependency Tracking** - Show both upstream (dependencies) and downstream (dependents) - 📏 **Depth Control** - Limit dependency graph display levels (1-5 or all) - 🎨 **Multiple Layouts** - Support hierarchical and force-directed layouts +- 🧩 **Group Rules (Prefix Aggregation)** - Aggregate nodes by package/prefix rules +- 🔎 **Prefix Filter** - Show only nodes/providers matching a prefix +- 🧭 **Group Subgraph** - View a group's internal + upstream/downstream dependencies - 📡 **RESTful API** - Provide JSON format dependency data +- 🧩 **Mermaid Export/Preview** - Generate Mermaid flowcharts for current graph (respects grouping/filtering) ## Quick Start @@ -54,6 +58,18 @@ func main() { Open browser and visit `http://localhost:8080` to view the dependency graph. +### Base Path / Prefix + +If you need to mount the UI and API under a path prefix (e.g. behind a gateway), use `WithBasePath`: + +```go +server := dixhttp.NewServerWithOptions( + (*dixinternal.Dix)(di), + dixhttp.WithBasePath("/dix"), +) +// Visit http://localhost:8080/dix/ +``` + ## UI Layout ``` @@ -143,6 +159,52 @@ Search `UserService` with different depths: - Depth 1: `Database ← UserService → Handler` - Depth 2: `Config ← Database ← UserService → Handler` +### 🧩 Group Rules (Prefix Aggregation) + +You can aggregate nodes by **package or prefix** using group rules. Rules can be configured: + +- **In UI** (group list) +- **From backend** via `RegisterGroupRules` (recommended for production) + +Backend registration: + +```go +import "github.com/pubgo/dix/v2/dixhttp" + +dixhttp.RegisterGroupRules( + dixhttp.GroupRule{ + Name: "service", + Prefixes: []string{ + "github.com/acme/app/service", + "github.com/acme/app/internal/service", + }, + }, + dixhttp.GroupRule{ + Name: "router", + Prefixes: []string{"github.com/acme/app/router"}, + }, +) +``` + +The UI will auto-load `/api/group-rules` if local rules are empty. + +### 🔎 Prefix Filter + +The toolbar provides a **Prefix Filter** field. It filters the current graph to show only nodes/providers +whose package/type/function name contains the given prefix. This works in: + +- Providers/Types view +- Type-focused dependency view +- Group subgraph view + +### 🧭 Group Subgraph + +Click a virtual group node to open the **group detail panel**, then click **View group graph** to see: + +- Internal nodes +- Upstream & downstream dependencies +- Depth control applied from the toolbar + ### 📦 Package Grouping Left panel features: @@ -161,6 +223,15 @@ Left panel features: | **Scroll Zoom** | Zoom in/out graph | | **Click Type in Details** | Jump to view that type's dependencies | +## Mermaid Support + +The toolbar includes a **Mermaid** button. It generates a Mermaid `flowchart` from the **current graph view** (including grouping, depth, and prefix filters), opens a preview modal, and lets you copy the Mermaid source. + +**Typical usage**: +1. Adjust view / grouping / filters. +2. Click **Mermaid**. +3. Copy the generated Mermaid text or use the preview. + ## API Endpoints ### GET `/` @@ -200,8 +271,11 @@ Returns dependency data, supports package filtering { "id": "provider_*main.ServiceA_0", "output_type": "*main.ServiceA", + "output_pkg": "github.com/example/app/service", "function_name": "main.NewServiceA", - "input_types": ["*main.Config"] + "function_pkg": "github.com/example/app", + "input_types": ["*main.Config"], + "input_pkgs": ["github.com/example/app/config"] } ], "objects": [...], @@ -226,6 +300,15 @@ Returns dependency chain for specified type {"from": "*db.Database", "to": "*service.UserService", "type": "dependency"} ] } + +### GET `/api/group-rules` +Returns backend-registered group rules (used as UI defaults) + +```json +[ + {"name": "service", "prefixes": ["github.com/acme/app/service"]} +] +``` ``` ## Tech Stack diff --git a/dixhttp/README_zh.md b/dixhttp/README_zh.md index e3e709b..228133f 100644 --- a/dixhttp/README_zh.md +++ b/dixhttp/README_zh.md @@ -12,7 +12,11 @@ - 🔄 **双向依赖追踪** - 同时展示依赖(上游)和被依赖(下游)关系 - 📏 **深度控制** - 限制依赖图的展示层级(1-5级或全部) - 🎨 **多种布局** - 支持层级布局和力导向布局 +- 🧩 **分组清单(前缀聚合)** - 通过包路径/前缀聚合节点 +- 🔎 **前缀过滤** - 只显示匹配前缀的节点/Provider +- 🧭 **组内子图** - 查看组内及上下游依赖 - 📡 **RESTful API** - 提供 JSON 格式的依赖关系数据 +- 🧩 **Mermaid 预览/导出** - 将当前图生成 Mermaid 流程图(支持分组/过滤) ## 快速开始 @@ -54,6 +58,18 @@ func main() { 打开浏览器访问 `http://localhost:8080` 即可查看依赖关系图。 +### 配置访问前缀 + +如果需要将页面和 API 挂载到一个前缀路径(例如网关转发),可以使用 `WithBasePath`: + +```go +server := dixhttp.NewServerWithOptions( + (*dixinternal.Dix)(di), + dixhttp.WithBasePath("/dix"), +) +// 访问 http://localhost:8080/dix/ +``` + ## 界面布局 ``` @@ -143,6 +159,51 @@ func main() { - 深度 1: `Database ← UserService → Handler` - 深度 2: `Config ← Database ← UserService → Handler` +### 🧩 分组清单(前缀聚合) + +支持通过**包路径/前缀**聚合节点,规则来源: + +- **前端分组清单** +- **后端全局注册**(推荐生产使用) + +后端注册示例: + +```go +import "github.com/pubgo/dix/v2/dixhttp" + +dixhttp.RegisterGroupRules( + dixhttp.GroupRule{ + Name: "service", + Prefixes: []string{ + "github.com/acme/app/service", + "github.com/acme/app/internal/service", + }, + }, + dixhttp.GroupRule{ + Name: "router", + Prefixes: []string{"github.com/acme/app/router"}, + }, +) +``` + +当本地未配置分组清单时,前端会自动加载 `/api/group-rules`。 + +### 🔎 前缀过滤 + +工具栏提供“前缀过滤”,可按包路径/类型名/函数名过滤当前图: + +- Providers/类型视图 +- 类型依赖视图 +- 组内依赖视图 + +### 🧭 组内子图 + +点击虚拟组节点 → 详情面板 → “查看组内依赖图”,可以看到: + +- 组内节点 +- 上下游依赖 +- 受工具栏“深度”控制 + ### 📦 按包分组 左侧面板功能: @@ -161,6 +222,15 @@ func main() { | **滚轮缩放** | 放大/缩小图形 | | **点击详情中的类型** | 跳转查看该类型的依赖 | +## Mermaid 支持 + +工具栏新增 **Mermaid** 按钮,会基于**当前视图**生成 Mermaid `flowchart`,并弹出预览窗口,支持一键复制源码。 + +**典型用法**: +1. 调整视图/分组/过滤条件。 +2. 点击 **Mermaid**。 +3. 复制生成的 Mermaid 文本或直接预览。 + ## API 端点 ### GET `/` @@ -200,8 +270,11 @@ func main() { { "id": "provider_*main.ServiceA_0", "output_type": "*main.ServiceA", + "output_pkg": "github.com/example/app/service", "function_name": "main.NewServiceA", - "input_types": ["*main.Config"] + "function_pkg": "github.com/example/app", + "input_types": ["*main.Config"], + "input_pkgs": ["github.com/example/app/config"] } ], "objects": [...], @@ -226,6 +299,15 @@ func main() { {"from": "*db.Database", "to": "*service.UserService", "type": "dependency"} ] } + +### GET `/api/group-rules` +返回后端注册的分组清单(前端默认配置) + +```json +[ + {"name": "service", "prefixes": ["github.com/acme/app/service"]} +] +``` ``` ## 技术栈 diff --git a/dixhttp/server.go b/dixhttp/server.go index 5ef068e..eeaf73a 100644 --- a/dixhttp/server.go +++ b/dixhttp/server.go @@ -7,6 +7,7 @@ import ( "net/http" "strconv" "strings" + "sync" "github.com/pubgo/dix/v2/dixinternal" ) @@ -18,13 +19,92 @@ var htmlTemplate string type Server struct { dix *dixinternal.Dix mux *http.ServeMux + // basePath is an optional URL prefix (no trailing slash). Example: "/dix" + basePath string +} + +// GroupRule defines a group name with prefix list for aggregation. +type GroupRule struct { + Name string `json:"name"` + Prefixes []string `json:"prefixes"` +} + +var ( + groupRulesMu sync.RWMutex + groupRules []GroupRule +) + +// RegisterGroupRules registers global group rules for visualization. +// This can be called by business code to predefine group rules. +func RegisterGroupRules(rules ...GroupRule) { + groupRulesMu.Lock() + defer groupRulesMu.Unlock() + groupRules = sanitizeGroupRules(rules) +} + +func getGroupRules() []GroupRule { + groupRulesMu.RLock() + defer groupRulesMu.RUnlock() + if len(groupRules) == 0 { + return nil + } + result := make([]GroupRule, 0, len(groupRules)) + for _, r := range groupRules { + result = append(result, GroupRule{Name: r.Name, Prefixes: append([]string{}, r.Prefixes...)}) + } + return result +} + +func sanitizeGroupRules(rules []GroupRule) []GroupRule { + var result []GroupRule + seen := make(map[string]bool) + for _, r := range rules { + name := strings.TrimSpace(r.Name) + if name == "" || seen[name] { + continue + } + seen[name] = true + var prefixes []string + prefixSeen := make(map[string]bool) + for _, p := range r.Prefixes { + pp := strings.TrimSpace(p) + if pp == "" || prefixSeen[pp] { + continue + } + prefixSeen[pp] = true + prefixes = append(prefixes, pp) + } + result = append(result, GroupRule{Name: name, Prefixes: prefixes}) + } + return result } // NewServer creates a new HTTP server for dependency visualization func NewServer(dix *dixinternal.Dix) *Server { + return NewServerWithOptions(dix) +} + +// ServerOption customizes the HTTP server behavior. +type ServerOption func(*Server) + +// WithBasePath sets an optional URL prefix for all routes. Example: "/dix". +func WithBasePath(basePath string) ServerOption { + return func(s *Server) { + s.basePath = normalizeBasePath(basePath) + } +} + +// NewServerWithOptions creates a new HTTP server with options. +func NewServerWithOptions(dix *dixinternal.Dix, opts ...ServerOption) *Server { s := &Server{ - dix: dix, - mux: http.NewServeMux(), + dix: dix, + mux: http.NewServeMux(), + basePath: "", + } + for _, opt := range opts { + if opt != nil { + opt(s) + } } s.setupRoutes() return s @@ -32,12 +112,27 @@ func NewServer(dix *dixinternal.Dix) *Server { // setupRoutes configures all HTTP routes func (s *Server) setupRoutes() { - s.mux.HandleFunc("/", s.HandleIndex) - s.mux.HandleFunc("/api/dependencies", s.HandleDependencies) - s.mux.HandleFunc("/api/stats", s.HandleStats) - s.mux.HandleFunc("/api/packages", s.HandlePackages) - s.mux.HandleFunc("/api/package/", s.HandlePackageDetails) - s.mux.HandleFunc("/api/type/", s.HandleTypeDetails) + base := s.basePath + indexPath := "/" + if base != "" { + indexPath = base + "/" + // Redirect /base -> /base/ + s.mux.HandleFunc(base, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == base { + http.Redirect(w, r, indexPath, http.StatusMovedPermanently) + return + } + http.NotFound(w, r) + }) + } + + s.mux.HandleFunc(indexPath, s.HandleIndex) + s.mux.HandleFunc(base+"/api/dependencies", s.HandleDependencies) + s.mux.HandleFunc(base+"/api/stats", s.HandleStats) + s.mux.HandleFunc(base+"/api/packages", s.HandlePackages) + s.mux.HandleFunc(base+"/api/package/", s.HandlePackageDetails) + s.mux.HandleFunc(base+"/api/type/", s.HandleTypeDetails) + s.mux.HandleFunc(base+"/api/group-rules", s.HandleGroupRules) } // ServeHTTP implements http.Handler interface @@ -54,7 +149,8 @@ func (s *Server) ListenAndServe(addr string) error { func (s *Server) HandleIndex(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) - fmt.Fprint(w, htmlTemplate) + html := strings.ReplaceAll(htmlTemplate, "__DIX_BASE_PATH__", s.basePath) + fmt.Fprint(w, html) } // HandleStats returns summary statistics @@ -298,6 +394,15 @@ func (s *Server) HandleDependencies(w http.ResponseWriter, r *http.Request) { writeJSON(w, data) } +// HandleGroupRules returns global group rules for visualization. +func (s *Server) HandleGroupRules(w http.ResponseWriter, r *http.Request) { + rules := getGroupRules() + if rules == nil { + rules = []GroupRule{} + } + writeJSON(w, rules) +} + // extractDependencyData extracts structured data from the Dix container func (s *Server) extractDependencyData(pkgFilter string, limit int) *DependencyData { data := &DependencyData{ @@ -329,8 +434,11 @@ func (s *Server) extractDependencyData(pkgFilter string, limit int) *DependencyD providerInfo := ProviderInfo{ ID: providerID, OutputType: detail.OutputType, + OutputPkg: detail.OutputPkg, FunctionName: detail.FunctionName, + FunctionPkg: detail.FunctionPkg, InputTypes: detail.InputTypes, + InputPkgs: detail.InputPkgs, } // Add edges from input types to provider output @@ -434,8 +542,11 @@ type DependencyData struct { type ProviderInfo struct { ID string `json:"id"` OutputType string `json:"output_type"` + OutputPkg string `json:"output_pkg"` FunctionName string `json:"function_name"` + FunctionPkg string `json:"function_pkg"` InputTypes []string `json:"input_types"` + InputPkgs []string `json:"input_pkgs"` } // ObjectInfo contains information about an object instance @@ -501,3 +612,15 @@ func writeJSON(w http.ResponseWriter, data any) { http.Error(w, fmt.Sprintf("Failed to encode JSON: %v", err), http.StatusInternalServerError) } } + +func normalizeBasePath(basePath string) string { + base := strings.TrimSpace(basePath) + if base == "" || base == "/" { + return "" + } + if !strings.HasPrefix(base, "/") { + base = "/" + base + } + base = strings.TrimRight(base, "/") + return base +} diff --git a/dixhttp/template.html b/dixhttp/template.html index fc4efcb..6a43b10 100644 --- a/dixhttp/template.html +++ b/dixhttp/template.html @@ -7,6 +7,7 @@ + diff --git a/dixinternal/api.go b/dixinternal/api.go index 6b88242..08b5fa9 100644 --- a/dixinternal/api.go +++ b/dixinternal/api.go @@ -3,6 +3,7 @@ package dixinternal import ( "errors" "reflect" + "strings" ) // New Dix new @@ -95,8 +96,11 @@ func (dix *Dix) GetObjects() map[reflect.Type]map[string][]reflect.Value { // ProviderDetails contains detailed information about a provider type ProviderDetails struct { OutputType string + OutputPkg string FunctionName string + FunctionPkg string InputTypes []string + InputPkgs []string } // GetProviderDetails returns detailed information about all providers @@ -106,15 +110,66 @@ func (dix *Dix) GetProviderDetails() []ProviderDetails { for _, providerFn := range providerList { fnName := GetFnName(providerFn.fn) var inputTypes []string + var inputPkgs []string + seen := make(map[string]bool) for _, input := range providerFn.inputList { - inputTypes = append(inputTypes, input.typ.String()) + if input.isStruct || input.typ.Kind() == reflect.Struct { + for _, in := range getProvideAllInputs(input.typ) { + name := in.typ.String() + if name == "" || seen[name] { + continue + } + seen[name] = true + inputTypes = append(inputTypes, name) + inputPkgs = append(inputPkgs, resolveTypePkgPath(in.typ)) + } + continue + } + + name := input.typ.String() + if name == "" || seen[name] { + continue + } + seen[name] = true + inputTypes = append(inputTypes, name) + inputPkgs = append(inputPkgs, resolveTypePkgPath(input.typ)) } details = append(details, ProviderDetails{ OutputType: outputType.String(), + OutputPkg: resolveTypePkgPath(outputType), FunctionName: fnName, + FunctionPkg: resolveFuncPkgPath(fnName), InputTypes: inputTypes, + InputPkgs: inputPkgs, }) } } return details } + +func resolveTypePkgPath(typ reflect.Type) string { + if typ == nil { + return "" + } + for typ.Kind() == reflect.Ptr || typ.Kind() == reflect.Slice || typ.Kind() == reflect.Array { + typ = typ.Elem() + if typ == nil { + return "" + } + } + if typ.Kind() == reflect.Map { + return resolveTypePkgPath(typ.Elem()) + } + return typ.PkgPath() +} + +func resolveFuncPkgPath(fnName string) string { + name := strings.TrimSpace(fnName) + if name == "" { + return "" + } + if idx := strings.LastIndex(name, "."); idx > 0 { + return name[:idx] + } + return "" +} diff --git a/dixrender/renderer.go b/dixrender/renderer.go deleted file mode 100644 index 2fbcfcc..0000000 --- a/dixrender/renderer.go +++ /dev/null @@ -1,144 +0,0 @@ -package dixrender - -import ( - "bytes" - "fmt" - "sort" - "strings" -) - -// DotRenderer implements DOT format graph rendering -type DotRenderer struct { - Buf *bytes.Buffer // Exported for testing - indent string - cache map[string]string -} - -func NewDotRenderer() *DotRenderer { - return &DotRenderer{ - Buf: &bytes.Buffer{}, - indent: "", - cache: make(map[string]string), - } -} - -// Writef writes a formatted string to the renderer buffer -func (d *DotRenderer) Writef(format string, args ...any) { - _, _ = fmt.Fprintf(d.Buf, d.indent+format+"\n", args...) -} - -func (d *DotRenderer) writef(format string, args ...any) { - d.Writef(format, args...) -} - -// escapeDotString escapes special characters for DOT format -func escapeDotString(s string) string { - s = strings.ReplaceAll(s, "\\", "\\\\") - s = strings.ReplaceAll(s, "\"", "\\\"") - s = strings.ReplaceAll(s, "\n", "\\n") - s = strings.ReplaceAll(s, "\r", "\\r") - s = strings.ReplaceAll(s, "\t", "\\t") - return s -} - -func (d *DotRenderer) RenderNode(name string, attrs map[string]string) { - escapedName := escapeDotString(name) - d.writef(`"%s" [label="%s"%s]`, escapedName, escapedName, d.formatAttrs(attrs)) -} - -func (d *DotRenderer) RenderEdge(from, to string, attrs map[string]string) { - d.writef(`"%s" -> "%s" %s`, escapeDotString(from), escapeDotString(to), d.formatAttrs(attrs)) -} - -func (d *DotRenderer) BeginSubgraph(name, label string) { - d.writef("subgraph %s {", escapeDotString(name)) - d.indent += "\t" - d.writef("label=\"%s\"", escapeDotString(label)) -} - -func (d *DotRenderer) EndSubgraph() { - d.indent = d.indent[:len(d.indent)-1] - d.writef("}") -} - -func (d *DotRenderer) String() string { - return d.Buf.String() -} - -// FormatAttrs formats attributes map into DOT format string -func (d *DotRenderer) FormatAttrs(attrs map[string]string) string { - return d.formatAttrs(attrs) -} - -func (d *DotRenderer) formatAttrs(attrs map[string]string) string { - if len(attrs) == 0 { - return "" - } - - // Sort keys to ensure consistent ordering - keys := make([]string, 0, len(attrs)) - for k := range attrs { - keys = append(keys, k) - } - sort.Strings(keys) - - var result bytes.Buffer - result.WriteString(" [") - first := true - for _, k := range keys { - if !first { - result.WriteString(",") - } - first = false - v := attrs[k] - fmt.Fprintf(&result, "%s=\"%s\"", k, v) - } - result.WriteString("]") - return result.String() -} - -// Graph represents dependency graphs in DOT format -type Graph struct { - Objects string `json:"objects"` - Providers string `json:"providers"` - ProviderTypes string `json:"provider_types"` -} - -// GraphOptions holds configuration options for graph rendering -type GraphOptions struct { - // MaxDepth limits the depth of dependencies to show (0 = unlimited) - MaxDepth int - - // GroupByPackage enables grouping nodes by package - GroupByPackage bool - - // ShowStructFields controls whether to show struct field dependencies - ShowStructFields bool - - // FilterPackages allows filtering by specific packages - FilterPackages []string -} - -// NewGraphOptions creates GraphOptions with sensible defaults -func NewGraphOptions() *GraphOptions { - return &GraphOptions{ - MaxDepth: 0, // Unlimited by default - GroupByPackage: true, - ShowStructFields: false, - FilterPackages: []string{}, - } -} - -// ShouldIncludeType checks if a type should be included in the graph based on filters -func (opts *GraphOptions) ShouldIncludeType(typ string) bool { - if len(opts.FilterPackages) == 0 { - return true - } - - for _, pkg := range opts.FilterPackages { - if strings.Contains(typ, pkg) { - return true - } - } - return false -} diff --git a/example/cycle/main.go b/example/cycle/main.go index dfe6973..9c9b9a5 100644 --- a/example/cycle/main.go +++ b/example/cycle/main.go @@ -12,9 +12,6 @@ func main() { fmt.Printf("panic: %v\n", r) } }() - defer func() { - fmt.Println(dixglobal.Graph()) - }() type ( A struct{} diff --git a/example/go.mod b/example/go.mod index f8099a0..e8e20ac 100644 --- a/example/go.mod +++ b/example/go.mod @@ -4,10 +4,6 @@ go 1.24.0 replace github.com/pubgo/dix/v2 => ../ -require github.com/pubgo/dix/v2 v2.0.0-alpha.3 +require github.com/pubgo/dix/v2 v2.0.0-00010101000000-000000000000 -require ( - github.com/lmittmann/tint v1.1.2 // indirect - github.com/samber/lo v1.52.0 // indirect - golang.org/x/text v0.31.0 // indirect -) +require github.com/lmittmann/tint v1.1.2 // indirect diff --git a/example/go.sum b/example/go.sum index 802974a..ca8ef76 100644 --- a/example/go.sum +++ b/example/go.sum @@ -1,6 +1,2 @@ github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= -github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= -github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= diff --git a/example/handler/main.go b/example/handler/main.go index 9a004ac..b1a3eab 100644 --- a/example/handler/main.go +++ b/example/handler/main.go @@ -24,10 +24,6 @@ func main() { } }() - defer func() { - fmt.Println(dixglobal.Graph()) - }() - dixglobal.Provide(func() *log.Logger { return log.New(os.Stderr, "example: ", log.LstdFlags|log.Lshortfile) }) @@ -47,8 +43,6 @@ func main() { } }) - fmt.Println(dixglobal.Graph()) - dixglobal.Inject(func(r *Redis, l *log.Logger, rr map[string]*Redis) { l.Println("invoke redis") fmt.Println("invoke:", r.name) diff --git a/example/struct-out/main.go b/example/struct-out/main.go index ed7b3ed..c11015d 100644 --- a/example/struct-out/main.go +++ b/example/struct-out/main.go @@ -50,7 +50,6 @@ func main() { err = fmt.Errorf("panic: %v", r) } fmt.Printf("panic: %v\n", err) - fmt.Println(dixglobal.Graph()) // Original behavior from recovery.Exit's func } }() @@ -97,6 +96,4 @@ func main() { fmt.Println(dm) fmt.Println(d5) }) - - fmt.Println(dixglobal.Graph()) }