如何在 Go 二进制文件中嵌入并执行 Bash 脚本

go 1.16+ 支持 `embed` 包,可将 bash 脚本以字符串形式编译进二进制;配合 `exec.command("bash")` 并设置 `stdin`,即可直接执行,无需外部文件依赖,完美支持交叉编译。

在 Go 应用中动态执行 Shell 脚本是常见需求(如初始化环境、调试辅助、CI 工具封装等),但传统方式需分发脚本文件,破坏单一二进制优势,且易因路径或权限问题失败。Go 1.16 引入的 embed 包为此提供了优雅解法:将脚本内容静态嵌入编译后的二进制中,并通过标准输入(stdin)交由 bash 或 sh 解释执行

✅ 基础实现:嵌入 + 执行

只需两步:

  1. 使用 //go:embed 指令声明脚本文件(如 script.sh);
  2. 创建 exec.Command("bash"),并将嵌入的脚本内容作为 strings.NewReader(script) 赋给 c.Stdin。
package main

import (
    "embed"
    "fmt"
    "os/exec"
    "strings"
)

//go:embed script.sh
var script string // 类型为 string,自动读取文件 UTF-8 内容

func main() {
    cmd := exec.Command("bash")
    cmd.Stdin = strings.NewReader(script)

    output, err := cmd.Output()
    if err != nil {
        fmt.Printf("执行失败: %v\n", err)
        return
    }
    fmt.Println(string(output))
}

⚠️ 注意事项:

  • embed 仅支持 string 和 []byte 类型变量;若需二进制脚本(如含非 UTF-8 字符),请改用 []byte + bytes.NewReader();
  • 确保目标系统已安装 bash(或改用 sh 提高兼容性);
  • 脚本中使用 $0, $1, $@ 等参数时,需显式传参(见下文“带参执行”);
  • script.sh 必须位于当前包目录或子目录中,且不能是隐藏文件(以 . 开头)或 testdata/ 目录下的文件。

? 进阶技巧:向嵌入脚本传递参数

Bash 支持 -s 标志从 stdin 读取脚本,并将后续参数作为 , , … $@ 传入:

package main

import (
    "fmt"
    "os/exec"
    "strings"
)

func main() {
    // -s: 从 stdin 读脚本;-: 占位符(表示脚本结束位置);后续为 $1, $2...
    cmd := exec.Command("bash", "-s", "-", "hello", "world")

    cmd.Stdin = strings.NewReader(`
echo "参数个数: $#"
echo "第一个参数: $1"
echo "第二个参数: $2"
echo "全部参数: $@"
`)

    out, err := cmd.Output()
    if err != nil {
        fmt.Printf("执行出错: %v\n", err)
        return
    }
    fmt.Print(string(out))
}

输出示例:

参数个数: 2
第一个参数: hello
第二个参数: world
全部参数: hello world

? 验证与最佳实践

  • 交叉编译安全:embed 在构建时完成内容注入,不依赖运行时文件系统,GOOS=linux GOARCH=arm64 go build 可生成纯静态 ARM64 二进制;
  • 调试建议:开发期可用 fmt.Printf("嵌入脚本:\n%s\n", script) 输出验证内容是否正确加载;
  • 安全提醒:避免拼接用户输入到嵌入脚本中(防止命令注入),如需动态逻辑,请改用 Go 原生实现或严格参数化调用;
  • 替代方案对比:相比 text/template 渲染或 os.ReadFile,embed 零 I/O、零依赖、启动即用,是嵌入式/CLI 工具的首选。

通过 embed + exec 组合,你既能享受 Go 单体二进制的部署便利,又能复用成熟的 Shell 生态——无需妥协,开箱即用。