使用interface化解一场因操作系统不同导致的编译问题
扫描二维码随身看资讯
使用手机 二维码应用 扫描右侧二维码,您可以
1. 在手机上细细品读~
2. 分享给您的微信好友或朋友圈~
场景描述
起因 :
因项目需求,需要编写一个agent, 需支持Linux和Windows操作系统。 Agent里面有一个功能需要获取到服务器上所有已经被占用的端口。
实现方式: 针对不同的操作系统,实现方式有所不同
-
linux: 使用服务器自带的
netstat
指令,然后使用os/exec
库来调用 shell脚本实现 -
windows: windows系统不同在于,使用 exec.Command指令后,需要调用
syscall.SysProcAttr
和syscall.LoadDLL
, 而这两个方法是windows系统下的专用库。
问题:
这里会出出现一个问题,虽然程序在编译的时候可以通过
GOOS
来区分编译到指定的操作系统的二进制包, 但是在编译过程中,编译器会进行代码检查,也会加载windows的代码逻辑。
编译争端
初始代码如下:
-
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 }
-
上面代码编译成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: unKnow+n 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
语法。修改如下:
-
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 }
-
将原来逻辑改成如下:
-
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 "" }
-
-
光修改了上面的逻辑也不行,因为编译的时候代码依然会执行,此时会报如下错误
tools.go:121:15: undefined: used_ports.CollectWindowsUsedPorts # 这是因为虽然linux编译时,不会编译 windows.go的文件,同时会导致 模块下的 #CollectWindowsUsedPorts 方法不存在
-
最终修复方式如下
- 编译时考虑使用 定义接口的方式, 针对不同操作系统使用不同的 结构体,然后通过结构实现接口的方式来使其两种操作系统方法来指向同一个接口
-
使用 接口字典的方式,实现策略模式,不再使用显示的
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 "" }
总结
-
程序级别的控制可以在 编译时使用
GOOS=windows
来区分编译成对应操作系统的二进制文件 -
文件级别的控制可以在文件头上使用
// + build windows
进行控制 - 代码级别的控制,可以是使用 结构体映射接口的方式进行区分
-
init()
初始化方法的使用 - 不同结构体只要实现了同一个接口的所有方法,那么可以使用 字典接口来实现代码层面的控制
英雄连城满V版下载 v1.6.0 安卓版
鸣潮测试版免费下载 v1.0.0 安卓版
全民打螺丝红包版下载 v1.0.1 安卓版
明日之后 官方正版
三国将无双送充值永抽版下载 v3.0.0 安卓版
嘎吱最新版
王者荣耀星之破晓体验服 v1.203.255 安卓版
为你读诗最新版
迷雾大陆正版
老爹奶酪店 中文版无广告
王国保卫战4无限道具版下载 v1.3.5 安卓版
mtr港铁
周三不忠夜V2模组低配版下载 v2.0.0 安卓版
鸣潮手游官方正版下载安装2024 v1.0.0 最新版
- 软件模块化原理:用户配置窗体的类库方案
- Keycloak中的跨域资源共享(CORS)处理
- 解密Prompt系列29. LLM Agent之真实世界海量API解决方案:ToolLLM & AnyTool
- 可视化学习:使用极坐标参数方程和SDF绘制有趣的图案
- Avalonia中的线性渐变画刷LinearGradientBrush
- Spring Boot中@Value注解的使用与替代方案
- AI大模型算法工程师的发展趋势及热门开源项目
- 微服务架构下的服务注册中心
- 缓存模式和设计模式在系统中的应用
- 深入了解Channel类:.NET CORE 3.0的新特性
- 最新Salesforce功能更新介绍
- C 语言中的 sscanf 详解
- 1
加查之花 正版
- 2
爪女孩 最新版
- 3
企鹅岛 官方正版中文版
- 4
捕鱼大世界 无限金币版
- 5
情商天花板 2024最新版
- 6
球球英雄 手游
- 7
内蒙打大a真人版
- 8
烦人的村民 手机版
- 9
跳跃之王手游
- 10
蛋仔派对 国服版本