场景描述

起因

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

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

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

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

编译争端

初始代码如下:

  1. tools.go
    // 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二进制文件时,会提示:
    # 编译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如下:
      //  +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
    }
  1. 将原来逻辑改成如下:

    • windows.go
      // 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 ""
      }
  2. 光修改了上面的逻辑也不行,因为编译的时候代码依然会执行,此时会报如下错误

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

    • 编译时考虑使用 定义接口的方式, 针对不同操作系统使用不同的 结构体,然后通过结构实现接口的方式来使其两种操作系统方法来指向同一个接口
    • 使用 接口字典的方式,实现策略模式,不再使用显示的 if 判断语法来做显示判断,这样可以避免编译时显示加载因操作系统带来的编译冲突
    • 使用init() 方式初始化 接口实现
    • 最终代码如下
      /* 实现接口目录如下
      ├── used_ports
      │ ├── linux.go
      │ ├── used_ports.go
      │ └── windows.go
      */
    • used_ports.go
      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
      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
      //  +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
      ...
      
      // 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. 不同结构体只要实现了同一个接口的所有方法,那么可以使用 字典接口来实现代码层面的控制

使用interface化解一场因操作系统不同导致的编译问题的更多相关文章

  1. [转]9个offer,12家公司,35场面试,从微软到谷歌,应届计算机毕业生的2012求职之路

    1,简介 毕业答辩搞定,总算可以闲一段时间,把这段求职经历写出来,也作为之前三个半月的求职的回顾. 首先说说我拿到的offer情况: 微软,3面->终面,搞定 百度,3面->终面,口头of ...

  2. 9个offer,12家公司,35场面试,从微软到谷歌,应届计算机毕业生的2012求职之路

    1,简介 毕业答辩搞定,总算可以闲一段时间,把这段求职经历写出来,也作为之前三个半月的求职的回顾. 首先说说我拿到的offer情况: 微软,3面->终面,搞定 百度,3面->终面,口头of ...

  3. (转)9个offer,12家公司,35场面试,从微软到谷歌,应届计算机毕业生的2012求职之路

    原文:http://www.cnblogs.com/figure9/archive/2013/01/09/2853649.html 1,简介 毕业答辩搞定,总算可以闲一段时间,把这段求职经历写出来,也 ...

  4. 转:9个offer,12家公司,35场面试 从微软到谷歌,应届计算机毕业生的2012求职之路 !!!

    1,简介 毕业答辩搞定,总算可以闲一段时间,把这段求职经历写出来,也作为之前三个半月的求职的回顾. 首先说说我拿到的offer情况: 微软,3面->终面,搞定 百度,3面->终面,口头of ...

  5. 我的求职之路:9个offer,12家公司,35场面试,最终谷歌【转载】

    作者:Luc(写于2012年) 一.简介 毕业答辩搞定,总算可以闲一段时间,把这段求职经历写出来,也作为之前三个半月的求职的回顾. 首先说说我拿到的offer情况: 微软,3面->终面,搞定 百 ...

  6. 智能驾驶操作系统OS

    智能驾驶操作系统OS 自动驾驶操作系统是一个流程化.复杂的综合系统,设计到众多流程和领域.首先,分为不同的层,包括:感知层.认知层.决策规划层.控制层和执行层几个层面. 自动驾驶操作系统是一个流程化. ...

  7. Device.js – 快速检测平台、操作系统和方向信息

    在 Web 项目中,有时候我们需要根据程序运行的环境采取特定操作.Device.js 是一个很小的 JavaScript 库,它简化了编写和平台,操作系统或浏览器相关的条件 CSS 或 JavaScr ...

  8. Golang 之 interface接口全面理解

    什么是interface 在面向对象编程中,可以这么说:“接口定义了对象的行为”, 那么具体的实现行为就取决于对象了. 在Go中,接口是一组方法签名(声明的是一组方法的集合).当一个类型为接口中的所有 ...

  9. GNU/Linux操作系统总览

    计算机科学本科的专业课包括高等数学.离散数学.模拟电子技术.数字电子技术.微机原理.汇编语言原理.高级程序语言.操作系统原理.高级编译原理.嵌入式原理.网络原理.计算机组成与结构等诸多科目.GNU计算 ...

  10. c#Winform程序调用app.config文件配置数据库连接字符串 SQL Server文章目录 浅谈SQL Server中统计对于查询的影响 有关索引的DMV SQL Server中的执行引擎入门 【译】表变量和临时表的比较 对于表列数据类型选择的一点思考 SQL Server复制入门(一)----复制简介 操作系统中的进程与线程

    c#Winform程序调用app.config文件配置数据库连接字符串 你新建winform项目的时候,会有一个app.config的配置文件,写在里面的<connectionStrings n ...

随机推荐

  1. 【我与openGauss的故事】如何管理数据库安全(第一部分)

    前言 2021 年 6 月 10 日国家颁布数据安全法对我们国家来说具有重大意义 信息安全法 梳理几点重要意义: (一) 对数据的有效监管实现了有法可依,填补了数据安全保护立法的空白,完善了网络空间安 ...

  2. 推荐两款HTTP请求Mock利器

    1.背景 在日常测试过程中或者研发开发过程中,目前接口暂时没有开发完成,测试人员又要提前介入接口测试中,测试人员不仅仅只是简单的编写测试用例,也可以通过一些mock的方法进行来提前根据接口测试的情况进 ...

  3. SQL 转置计算

    转置即旋转数据表的横纵方向,常用来改变数据布局,以便用新的角度观察.有些转置算法比较简单,比如行转列.列转行.双向转置:有些算法变化较多,比如动态转置.转置时跨行计算.关联转置等.这些转置算法对日常工 ...

  4. Xilinx USB JTAG两种JTGA-HS3和Platfrom下载器速度对比

    下面测试速度,以一个V7的配置文件为例子.文件大小如下,27MB.特别是对于有点规模的项目配置文件都是很大的.总不能是点灯项目. 选择普通的下载器,Platform Cable USB.这种下载器是基 ...

  5. WPF随笔收录-RestSharp下载文件406问题

    一.前言 在项目开发过程中,涉及到通过http下载文件的需求,最近遇到一个406问题,由于第一次接触这个问题,也被问题卡了好久,在网上风暴了很久才找到解决办法: 二.解决方法 解决的办法就是在requ ...

  6. vue-cli4.0 (vue3.0 的脚手架)

    前言: 这个搭建脚手架的话实际是我们创建一个新项目的第一步,当然,现在脚手架4.0都出来了,经过使用后发现跟我们之前的3.0使用方法是答题一样的,其中用vue-cli3.0来搭建我们的项目的话又分为两 ...

  7. Java面试题:细数ThreadLocal大坑,内存泄露本可避免

    一.背景ThreadLocal是Java中用于解决多线程共享变量导致的线程安全问题的一种机制.它为每个线程分配一个独立的变量副本,从而避免了线程间的数据竞争.这个我们从上一篇文章<Java面试题 ...

  8. 力扣657(java & python)-机器人能否返回原点(简单)

    题目: 在二维平面上,有一个机器人从原点 (0, 0) 开始.给出它的移动顺序,判断这个机器人在完成移动后是否在 (0, 0) 处结束. 移动顺序由字符串 moves 表示.字符 move[i] 表示 ...

  9. Spark如何对源端数据做切分?

    简介: 典型的Spark作业读取位于OSS的Parquet外表时,源端的并发度(task/partition)如何确定?特别是在做TPCH测试时有一些疑问,如源端扫描文件的并发度是如何确定的?是否一个 ...

  10. 多任务多目标CTR预估技术

    ​简介: 多目标(Multi Objective Learning)是MTL中的一种.在业务场景中,经常面临既要又要的多目标问题.而多个目标常常会有冲突.如何使多个目标同时得到提升,是多任务多目标在真 ...