hostkeycallback必须显式设置,否则ssh.dial()直接panic;生产环境用ssh.knownhosts()校验主机密钥,session需每次新建并defer关闭,run()可获取退出码而output()掩盖错误细节。

HostKeyCallback 必须显式设置,否则连不上
不填 HostKeyCallback,ssh.Dial() 直接 panic,不是运行时错,是编译期就报字段未初始化。别指望它有默认值。
本地调试可用 ssh.InsecureIgnoreHostKey(),但 CI/CD 或容器里必须换掉——它等于跳过主机身份校验,攻击者可中间人劫持。
生产环境推荐用 ssh.KnownHosts(knownHostsFile),注意路径要拼全:os.UserHomeDir() + "/.ssh/known_hosts",不能写 ~/.ssh/known_hosts(Go 不展开波浪号)。
常见错误现象:ssh: handshake failed: ssh: unable to authenticate,90% 是这个回调拒绝了服务端公钥,不是密码或密钥错了。建议在回调里加日志打印 hostKey.Type() 和 ssh.FingerprintSHA256(hostKey) 做比对。
每次命令都得新建 *ssh.Session,复用会静默失败
*ssh.Session 不是连接池资源,也不是线程安全对象。它是一次性会话,复用会导致 write tcp: broken pipe、命令不执行、或 stdout/stderr 丢数据。
哪怕只连一台机器跑 10 条命令,也得调 10 次 client.NewSession()。不要试图缓存 session 或设成 struct 字段。
错误写法示例:
type SSHRunner struct {
client *ssh.Client
session *ssh.Session // ❌ 危险!
}
func (r *SSHRunner) Run(cmd string) error {
return r.session.Run(cmd) // 多次调用大概率卡死或静默失败
}
正确做法是把 session 创建和销毁放在单次方法内,用 defer session.Close() 收尾。
session.Run() 和 session.Output() 的错误处理差异很大
session.Run("ls") 返回 error,且能区分退出码:如果命令返回非零码,error 类型是 *ssh.ExitError,可用 .ExitStatus() 提取具体码值。
session.Output("ls") 内部封装了 Run() + io.ReadAll(stdout),但掩盖了错误来源:是命令本身失败?还是读取超时?还是管道没关干净?
关键命令别用 Output():
- 需要判断
ls /tmp是否存在目录?用Run()+ 检查ExitStatus() - 要解析
df -h输出里的百分比?先Run()确保成功,再读stdout - 只做简单探活(如
echo ok)且不关心退出码,Output()可省几行代码
stdout/stderr 分离读取必须配 goroutine + Wait() 顺序
想区分标准输出和错误流、实时捕获日志、或做结构化解析,就不能用 CombinedOutput()。
必须按固定顺序操作:
- 先调
session.StdoutPipe()和session.StderrPipe() - 再
session.Start("cmd") - 启两个 goroutine 分别读两个 pipe(哪怕 stderr 只丢弃也要读)
- 最后
session.Wait()—— 必须在所有读操作完成后调,否则可能提前 EOF 或截断
漏读 stderr 是高频坑:某些 shell(如 zsh、带 strict mode 的 bash)会因 stderr 缓冲区满而 hang 住,导致 Wait() 永远不返回。
超时控制只能靠 context.WithTimeout():ClientConfig.Timeout 只管握手阶段,对命令执行完全无效。没加 context,session.Run() 卡死无解。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











