使用interface化解一场因操作系统不同导致的编译问题

场景描述

起因

因项目需求,需要编写一个agent, 需支持Linux和Windows操作系统。 Agent里面有一个功能需要获取到服务器上所有已经被占用的端口。

**实现方式:**针对不同的操作系统,实现方式有所不同

  • linux: 使用服务器自带的 netstat 指令,然后使用 os/exec 库来调用 shell脚本实现
  • windows: windows系统不同在于,使用 exec.Command指令后,需要调用 syscall.SysProcAttrsyscall.LoadDLL, 而这两个方法是windows系统下的专用库。

问题: 这里会出出现一个问题,虽然程序在编译的时候可以通过GOOS来区分编译到指定的操作系统的二进制包, 但是在编译过程中,编译器会进行代码检查,也会加载windows的代码逻辑。

编译争端

初始代码如下:

  1. tools.go

    golang 复制代码
    // get address
    func getAddress(addr string) string {
        var address string
        if strings.Contains(addr, "tcp") {
            address = strings.TrimRight(addr, "tcp")
        } else {
            address = strings.TrimRight(addr, "udp")
        }
        return address
    }
    
    // CollectServerUsedPorts, collect all of the ports that have been used
    func CollectServerUsedPorts(platform string) string {
        var (
            platformLower = strings.ToLower(platform)
            cmd           *exec.Cmd
            err           error
            cmdOutPut     []byte
        )
    
        if platformLower == "linux" {
            // 执行 shell 指令, 获取tcp协议占用的端口
            getUsedPortsCmd := `netstat -tln | awk '{print $4}' | awk -F: '{print $NF}' | egrep -o '[0-9]+' | sort -n | uniq | paste -s -d ","`
            cmd = exec.Command("bash", "-c", getUsedPortsCmd)
    
        } else if platformLower == "windows" {
            // 执行 powershell指令获取已经占用的端口号
            getUsedPortsCmd := SelectScriptByWindowsVersion()
    
            cmd = exec.Command("powershell", "-Command", getUsedPortsCmd)
            cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
    
        } else {
            cmd = nil
        }
    
        if cmd != nil {
            if cmdOutPut, err = cmd.Output(); err != nil {
                log.Errorf("err to execute command %s,  %s", cmd.String(), err.Error())
                return ""
            }
            return strings.Trim(string(cmdOutPut), "\n")
        }
        return ""
    }
    
    func SelectScriptByWindowsVersion() string {
        var getUsedPortsCmd string
        version, err := getWindowsVersion()
        if err != nil {
            log.Errorf("无法获取Windows版本信息: %s", err.Error())
            return ""
        }
    
        // if system version is lower than windows 8
        if version < 6.2 {
            log.Warnf("Windows 版本低于 Windows 8")
            getUsedPortsCmd = `(netstat -an | Select-String 'LISTENING' | ForEach-Object { $_ -replace '\s+', ' ' } | ForEach-Object { ($_ -split ' ')[2] } | Where-Object {$_ -match '\d'} | ForEach-Object {[int]($_ -split ':')[-1]} | Sort-Object | Get-Unique ) -join ",".Replace("r","")`
        } else {
            getUsedPortsCmd = `((Get-NetTCPConnection | Where-Object {$_.State -eq 'Listen'} | Select-Object -ExpandProperty LocalPort) | Sort-Object {[int]$_} | Get-Unique) -join ",".Replace("r","")`
        }
    
        return getUsedPortsCmd
    }
    
    func getWindowsVersion() (float64, error) {
    
        mod, err := syscall.LoadDLL("kernel32.dll")
        if err != nil {
            return 0, err
        }
        defer func() {
            _ = mod.Release()
        }()
    
        proc, err := mod.FindProc("GetVersion")
        if err != nil {
            return 0, err
        }
    
        version, _, _ := proc.Call()
        majorVersion := byte(version)
        minorVersion := byte(version >> 8)
    
        return float64(majorVersion) + float64(minorVersion)/10, nil
    }
  2. 上面代码编译成windows没问题,但是编译linux二进制文件时,会提示:

    sh 复制代码
    # 编译linux二进制文件
    go build -ldflags "-linkmode external -extldflags '-static'" -tags musl -o  main main.go
    
    # 错误输出如下
    windows.go:31:22: undefined: syscall.LoadDLL
    windows.go:56:41: unknown field 'HideWindow' in struct literal of type syscall.SysProcAttr
    
    # 错误原因
    - 内置库syscall,在linux编译时,其syscall.SysProcAttr 结构体并没有`HideWindow`字段;
    - linux 下也没有 syscall.LoadDLL方法
    - 编译和代码执行逻辑不一样,虽然代码有检查系统服务器类型的逻辑,但是编译时需要加载代码中的每一行代码逻辑,
    - 将其编译成汇编,然后再交给计算机执行,所以会出现编译错误

矛盾化解

Go语言在编译时除了有对整个项目编译的 参数控制 , 如 参数GOOS=windows表示编译成widnwos系统下的二进制文件。 但是这个参数只能控制项目级别的, 对于上面这种情况,需要控制文件级别的编译, 当然 Go也是支持的,在提取出 windows 逻辑的代码为独立文件,在文件开头使用 // + build windows 语法。修改如下:

  1. used_ports/windows.go如下:

    golang 复制代码
      //  +build windows
    func SelectScriptByWindowsVersion() string {
        var getUsedPortsCmd string
        version, err := getWindowsVersion()
        if err != nil {
            log.Errorf("无法获取Windows版本信息: %s", err.Error())
            return ""
        }
    
        // if system version is lower than windows 8
        if version < 6.2 {
            log.Warnf("Windows 版本低于 Windows 8")
            getUsedPortsCmd = `(netstat -an | Select-String 'LISTENING' | ForEach-Object { $_ -replace '\s+', ' ' } | ForEach-Object { ($_ -split ' ')[2] } | Where-Object {$_ -match '\d'} | ForEach-Object {[int]($_ -split ':')[-1]} | Sort-Object | Get-Unique ) -join ",".Replace("r","")`
        } else {
            getUsedPortsCmd = `((Get-NetTCPConnection | Where-Object {$_.State -eq 'Listen'} | Select-Object -ExpandProperty LocalPort) | Sort-Object {[int]$_} | Get-Unique) -join ",".Replace("r","")`
        }
    
        return getUsedPortsCmd
    }
    
    func getWindowsVersion() (float64, error) {
    
        mod, err := syscall.LoadDLL("kernel32.dll")
        if err != nil {
            return 0, err
        }
        defer func() {
            _ = mod.Release()
        }()
    
        proc, err := mod.FindProc("GetVersion")
        if err != nil {
            return 0, err
        }
    
        version, _, _ := proc.Call()
        majorVersion := byte(version)
        minorVersion := byte(version >> 8)
    
        return float64(majorVersion) + float64(minorVersion)/10, nil
    }
  2. 将原来逻辑改成如下:

    • windows.go

      golang 复制代码
      // CollectServerUsedPorts, collect all of the ports that have been used
      func CollectServerUsedPorts(platform string) string {
          var (
              platformLower = strings.ToLower(platform)
              cmd           *exec.Cmd
              err           error
              cmdOutPut     []byte
          )
      
          if platformLower == "linux" {
              // 执行 shell 指令, 获取tcp协议占用的端口
              getUsedPortsCmd := `netstat -tln | awk '{print $4}' | awk -F: '{print $NF}' | egrep -o '[0-9]+' | sort -n | uniq | paste -s -d ","`
              cmd = exec.Command("bash", "-c", getUsedPortsCmd)
          
          } else if platformLower == "windows" {
              // todo: 需要优化,通过接口映射避免编译的问题
              // Linux 编译时需要隐藏下面代码,
              getUsedPortsCmd := used_ports.CollectWindowsUsedPorts()
              cmd = exec.Command("powershell", "-Command", getUsedPortsCmd)
              cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
      
          } else {
              cmd = nil
          }
      
          if cmd != nil {
              if cmdOutPut, err = cmd.Output(); err != nil {
                  log.Errorf("err to execute command %s,  %s", cmd.String(), err.Error())
                  return ""
              }
              return strings.Trim(string(cmdOutPut), "\n")
          }
          return ""
      }
  3. 光修改了上面的逻辑也不行,因为编译的时候代码依然会执行,此时会报如下错误

    sh 复制代码
    tools.go:121:15: undefined: used_ports.CollectWindowsUsedPorts
    
    # 这是因为虽然linux编译时,不会编译 windows.go的文件,同时会导致 模块下的
    #CollectWindowsUsedPorts 方法不存在
  4. 最终修复方式如下

    • 编译时考虑使用 定义接口的方式, 针对不同操作系统使用不同的 结构体,然后通过结构实现接口的方式来使其两种操作系统方法来指向同一个接口

    • 使用 接口字典的方式,实现策略模式,不再使用显示的 if 判断语法来做显示判断,这样可以避免编译时显示加载因操作系统带来的编译冲突

    • 使用init() 方式初始化 接口实现

    • 最终代码如下

      golang 复制代码
      /* 实现接口目录如下
      ├── used_ports
      │   ├── linux.go
      │   ├── used_ports.go
      │   └── windows.go
      */
    • used_ports.go

      golang 复制代码
      package used_ports
      
      import "os/exec"
      
      type UsedPortCollector interface {
          CollectHaveUsedPorts() *exec.Cmd
      }
      
      var UsedPortCollectorMap = make(map[string]UsedPortCollector, 2)
      
      func Register(platformOS string, collector UsedPortCollector) {
          if _, ok := Find(platformOS); ok {
              return
          }
      
          UsedPortCollectorMap[platformOS] = collector
      }
      
      func Find(platformOS string) (UsedPortCollector, bool) {
      
          c, ok := UsedPortCollectorMap[platformOS]
          return c, ok
      }
    • linux.go

      golang 复制代码
      package used_ports
        
      import (
          "os/exec"
      )
        
      func init() {
          Register("linux", newLinuxUsedPorts())
      }
        
      type linuxPortCollectorImpl struct{}
        
      func (w linuxPortCollectorImpl) CollectHaveUsedPorts() *exec.Cmd {
          // 执行 shell 指令, 获取tcp协议占用的端口
          getUsedPortsCmd := `netstat -tln | awk '{print $4}' | awk -F: '{print $NF}' | egrep -o '[0-9]+' | sort -n | uniq | paste -s -d ","`
          cmd := exec.Command("bash", "-c", getUsedPortsCmd)
          return cmd
      }
        
         // newLinuxUsedPorts 返回Windows系统下的端口收集器实例
      func newLinuxUsedPorts() UsedPortCollector {
          return linuxPortCollectorImpl{}
      }
      • windows.go
      golang 复制代码
      //  +build windows
      
      package used_ports
      
      import (
          "os/exec"
          "syscall"
      )
      
      func init() {
          Register("windows", newWindowsCollector())
      }
      
      // 结构体
      type windowsPortCollectorImpl struct{}
      
      // 实现接口方法
      func (w windowsPortCollectorImpl) CollectHaveUsedPorts() *exec.Cmd {
          // 执行 powershell指令获取已经占用的端口号
          getUsedPortsCmd := selectScriptByWindowsVersion()
      
          cmd := exec.Command("powershell", "-Command", getUsedPortsCmd)
          cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
      
          return cmd
      }
      
      // newWindowsCollector 返回Windows系统下的端口收集器实例,算是工厂方法
      func newWindowsCollector() UsedPortCollector {
          return windowsPortCollectorImpl{}
      }
      
      func selectScriptByWindowsVersion() string {
          var getUsedPortsCmd string
          version, err := getWindowsVersion()
          if err != nil {
              return ""
          }
      
          // if system version is lower than windows 8
          if version < 6.2 {
              getUsedPortsCmd = `(netstat -an | Select-String 'LISTENING' | ForEach-Object { $_ -replace '\s+', ' ' } | ForEach-Object { ($_ -split ' ')[2] } | Where-Object {$_ -match '\d'} | ForEach-Object {[int]($_ -split ':')[-1]} | Sort-Object | Get-Unique ) -join ",".Replace("r","")`
          } else {
              getUsedPortsCmd = `((Get-NetTCPConnection | Where-Object {$_.State -eq 'Listen'} | Select-Object -ExpandProperty LocalPort) | Sort-Object {[int]$_} | Get-Unique) -join ",".Replace("r","")`
          }
      
          return getUsedPortsCmd
      }
      
      func getWindowsVersion() (float64, error) {
      
          mod, err := syscall.LoadDLL("kernel32.dll")
          if err != nil {
              return 0, err
          }
          defer func() {
              _ = mod.Release()
          }()
      
          proc, err := mod.FindProc("GetVersion")
          if err != nil {
              return 0, err
          }
      
          version, _, _ := proc.Call()
          majorVersion := byte(version)
          minorVersion := byte(version >> 8)
      
          return float64(majorVersion) + float64(minorVersion)/10, nil
      }
    • tools.go

      golang 复制代码
      ...
      
      // CollectServerUsedPorts, collect all of the ports that have been used
      func CollectServerUsedPorts(platform string) string {
          var (
              platformLower = strings.ToLower(platform)
              cmd           *exec.Cmd
              err           error
              cmdOutPut     []byte
          )
         
         // 策略方法,获取操作系统对应的实例(接口)  
          if portCollector, ok := used_ports.Find(platformLower); ok {
              cmd = portCollector.CollectHaveUsedPorts()
          }
      
          if cmd != nil {
              if cmdOutPut, err = cmd.Output(); err != nil {
                  log.Errorf("err to execute command %s,  %s", cmd.String(), err.Error())
                  return ""
              }
              return strings.Trim(string(cmdOutPut), "\n")
          }
      
          return ""
      }

总结

  1. 程序级别的控制可以在 编译时使用 GOOS=windows 来区分编译成对应操作系统的二进制文件
  2. 文件级别的控制可以在文件头上使用 // + build windows进行控制
  3. 代码级别的控制,可以是使用 结构体映射接口的方式进行区分
  4. init()初始化方法的使用
  5. 不同结构体只要实现了同一个接口的所有方法,那么可以使用 字典接口来实现代码层面的控制
相关推荐
意法半导体STM3217 天前
【官方原创】如何为STM32CubeMX2配置Visual Studio Code配置方案
vscode·stm32·单片机·嵌入式硬件·策略模式·stm32cubemx·嵌入式开发
山东点狮信息科技有限公司18 天前
企业级 MES 制造执行系统架构设计与实践
spring cloud·性能优化·系统架构·策略模式·点狮
zzqssliu18 天前
基于策略模式与责任链的代购商品多源采集架构实战
架构·策略模式
mxpan19 天前
macOS 13+ 上使用 macFUSE + NTFS-3G 读写 NTFS 移动硬盘技术说明
macos·策略模式
折哥的程序人生 · 物流技术专研19 天前
Java 23 种设计模式:从踩坑到精通 | 番外:编排器+策略模式在多平台电子面单中的实战(含性能压测)
设计模式·策略模式·代码重构·java设计模式·编排器·电子面单·从踩坑到精通
忧云20 天前
2026年最新 Cursor 国内使用 DeepSeek API等各模型使用完整教程
ai编程·策略模式·cursor·byok·cursor使用国内大模型
AIex-YH20 天前
三域贯通11/12:生物制造的“死亡之谷“,CDMO 是桥还是船?
运维·制造·策略模式
回忆2012初秋21 天前
【Nginx】原理、配置与运维实战(2)
运维·nginx·策略模式
怎么没有名字注册了啊21 天前
macOS 基于 CSDN GitCode + Homebrew Tap 发布 Qt .app 二进制程序通用教程(homebrew 安装自己的软件)
策略模式·homebrew·formula·ruhy
坏小虎22 天前
macOS 安装 Ghostty 终端完整教程:环境、依赖与美化配置
macos·策略模式