• golang[ssa & callgraph] 获取调用图实战


    最近在拆分一个旧服务,需要从几十万行代码中,按业务功能拆分出对应代码,并部署新服务;然而,面对这种巨型服务,代码调用错综复杂,纯人力拆分需要耗费很多时间;基于此,这里借助golang自带callgraph调用图能力,帮我们找到需要拆出的代码;

    1. package main
    2. import (
    3. "fmt"
    4. "io/ioutil"
    5. "path/filepath"
    6. "sort"
    7. "strings"
    8. "github.com/pkg/errors"
    9. "golang.org/x/tools/go/packages"
    10. "golang.org/x/tools/go/ssa/ssautil"
    11. "golang.org/x/tools/go/callgraph"
    12. "golang.org/x/tools/go/pointer"
    13. )
    14. // getProjectUsedCall 获取项目使用中的调用方法
    15. func getProjectUsedCall(projectPath string) ([]string, error) {
    16. projectModule, err := parseProjectModule(projectPath)
    17. if err != nil {
    18. return nil, errors.Wrap(err, "parseProjectModule fail")
    19. }
    20. log.Debugf("projectModule: %+v", projectModule)
    21. callMap, err := parseProjectCallMap(projectPath)
    22. if err != nil {
    23. return nil, errors.Wrap(err, "parseProjectCallMap fail")
    24. }
    25. log.Debugf("callMap: %+v", callMap)
    26. srcCall := fmt.Sprintf("%v.main", projectModule)
    27. isDeleteEdgeFunc := func(caller, callee string) bool {
    28. // 非本项目调用
    29. if !strings.Contains(caller, projectModule) || !strings.Contains(callee, projectModule) {
    30. return true
    31. }
    32. // 非初始化调用
    33. if isInitCall(caller) || isInitCall(callee) {
    34. return true
    35. }
    36. // 非自我调用
    37. if caller == callee {
    38. return true
    39. }
    40. return false
    41. }
    42. // 过滤不需要的边
    43. for caller, callees := range callMap {
    44. for callee := range callees {
    45. if isDeleteEdgeFunc(caller, callee) {
    46. delete(callees, callee)
    47. }
    48. }
    49. if len(callees) == 0 {
    50. delete(callMap, caller)
    51. }
    52. }
    53. // 广度搜索图
    54. for {
    55. srcCallees := callMap[srcCall]
    56. srcSize := len(srcCallees)
    57. for srcCallee := range srcCallees {
    58. for nextCallee := range callMap[srcCallee] {
    59. callMap[srcCall][nextCallee] = true
    60. }
    61. }
    62. if srcSize == len(callMap[srcCall]) {
    63. break
    64. }
    65. }
    66. // 调用源涉及到的所有方法
    67. var callees []string
    68. for c := range callMap[srcCall] {
    69. callees = append(callees, c)
    70. }
    71. sort.Strings(callees)
    72. return callees, nil
    73. }
    74. // parseProjectCallMap 解析项目调用图
    75. func parseProjectCallMap(projectPath string) (map[string]map[string]bool, error) {
    76. projectModule, err := parseProjectModule(projectPath)
    77. if err != nil {
    78. return nil, errors.Wrap(err, "parseProjectModule fail")
    79. }
    80. log.Debugf("projectModule: %+v", projectModule)
    81. result, err := analyzeProject(projectPath)
    82. if err != nil {
    83. return nil, errors.Wrap(err, "analyzeProject fail")
    84. }
    85. log.Debugf("analyzeProject: %+v", result)
    86. // 遍历调用链路
    87. var callMap = make(map[string]map[string]bool)
    88. visitFunc := func(edge *callgraph.Edge) error {
    89. if edge == nil {
    90. return nil
    91. }
    92. // 解析调用者和被调用者
    93. caller, callee, err := parseCallEdge(edge)
    94. if err != nil {
    95. return errors.Wrap(err, "parseCallEdge fail")
    96. }
    97. // 记录调用关系
    98. if callMap[caller] == nil {
    99. callMap[caller] = make(map[string]bool)
    100. }
    101. callMap[caller][callee] = true
    102. return nil
    103. }
    104. err = callgraph.GraphVisitEdges(result.CallGraph, visitFunc)
    105. if err != nil {
    106. return nil, errors.Wrap(err, "GraphVisitEdges fail")
    107. }
    108. return callMap, nil
    109. }
    110. func parseProjectModule(projectPath string) (string, error) {
    111. modFilename := filepath.Join(projectPath, "go.mod")
    112. content, err := ioutil.ReadFile(modFilename)
    113. if err != nil {
    114. return "", errors.Wrap(err, "ioutil.ReadFile fail")
    115. }
    116. lines := strings.Split(string(content), "\n")
    117. module := strings.TrimPrefix(lines[0], "module ")
    118. module = strings.TrimSpace(module)
    119. return module, nil
    120. }
    121. func analyzeProject(projectPath string) (*pointer.Result, error) {
    122. // 生成Go Packages
    123. pkgs, err := packages.Load(&packages.Config{
    124. Mode: packages.LoadAllSyntax,
    125. Dir: projectPath,
    126. })
    127. if err != nil {
    128. return nil, errors.Wrap(err, "packages.Load fail")
    129. }
    130. log.Debugf("pkgs: %+v", pkgs)
    131. // 生成ssa 构建编译
    132. prog, ssaPkgs := ssautil.AllPackages(pkgs, 0)
    133. prog.Build()
    134. log.Debugf("ssaPkgs: %+v", ssaPkgs)
    135. // 使用pointer生成调用链路
    136. return pointer.Analyze(&pointer.Config{
    137. Mains: ssaPkgs,
    138. BuildCallGraph: true,
    139. })
    140. }
    141. func parseCallEdge(edge *callgraph.Edge) (string, string, error) {
    142. const callArrow = "-->"
    143. edgeStr := fmt.Sprintf("%+v", edge)
    144. strArray := strings.Split(edgeStr, callArrow)
    145. if len(strArray) != 2 {
    146. return "", "", fmt.Errorf("invalid format: %v", edgeStr)
    147. }
    148. callerNodeStr, calleeNodeStr := strArray[0], strArray[1]
    149. caller, callee := getCallRoute(callerNodeStr), getCallRoute(calleeNodeStr)
    150. return caller, callee, nil
    151. }
    152. func getCallRoute(nodeStr string) string {
    153. nodeStr = strings.TrimSpace(nodeStr)
    154. if strings.Contains(nodeStr, ":") {
    155. nodeStr = nodeStr[strings.Index(nodeStr, ":")+1:]
    156. }
    157. nodeStr = strings.ReplaceAll(nodeStr, "*", "")
    158. nodeStr = strings.ReplaceAll(nodeStr, "(", "")
    159. nodeStr = strings.ReplaceAll(nodeStr, ")", "")
    160. nodeStr = strings.ReplaceAll(nodeStr, "<", "")
    161. nodeStr = strings.ReplaceAll(nodeStr, ">", "")
    162. if strings.Contains(nodeStr, "$") {
    163. nodeStr = nodeStr[:strings.Index(nodeStr, "$")]
    164. }
    165. if strings.Contains(nodeStr, "#") {
    166. nodeStr = nodeStr[:strings.Index(nodeStr, "#")]
    167. }
    168. return strings.TrimSpace(nodeStr)
    169. }
    170. func isInitCall(call string) bool {
    171. return strings.HasSuffix(call, ".init")
    172. }

  • 相关阅读:
    【笔录】TVP技术沙龙:寻宝AI时代
    高精度乘法模板(fft)
    3分钟学会设计模式 -- 单例模式
    聊一聊redis奇葩数据类型与集群知识
    规则引擎Drools在贷后催收业务中的应用
    3如何搭建组件库的样式工程之button-scss
    JAVA-分页查询
    [Emeuelc]DC模拟器Flycast按键设置相关研究
    Virtualbox中对SD卡进行格式化和分区
    React antd Table点击下一页后selectedRows丢失之前页选择内容的问题
  • 原文地址:https://blog.csdn.net/qq_28969139/article/details/134522265