ssh.client 可复用,但 ssh.session 必须每次新建,否则命令静默失败或连接中断;hostkeycallback 必填,生产环境须用 ssh.knownhosts;session.run() 比 output() 更利于错误排查;stdout/stderr 需分离读取并配 goroutine + wait();超时控制依赖 context.withtimeout;跳板机需嵌套 dialer 实现双跳。

ssh.Client 复用但 ssh.Session 必须每次新建,否则命令静默失败或连接中断
Go 语言里没有“SSH 执行工具”这种开箱即用的模块,所有远程命令执行都得靠 golang.org/x/crypto/ssh 手动搭链路。它轻、稳、可控,但错一步就卡死或丢输出——不是语法难,是流程和边界条件容易漏。
HostKeyCallback 不填,连都连不上
这是最常卡住的第一步:ssh.ClientConfig 的 HostKeyCallback 是强制非空字段,不设就编译报错。别指望默认行为。
-
ssh.InsecureIgnoreHostKey()只能用于本地调试,CI/CD 或生产环境等于主动放弃主机身份校验 - 线上必须用
ssh.KnownHosts(knownHostsFile),路径要自己拼全,比如os.UserHomeDir() + "/.ssh/known_hosts",不能写~/.ssh/known_hosts - 错误现象
ssh: handshake failed: ssh: unable to authenticate,90% 是这个回调拒绝了服务端公钥,不是密码或密钥错了——建议先打印hostKey的类型和 fingerprint 做比对
session.Run() 和 session.Output() 都是阻塞调用,但错误处理逻辑不同
session.Run("ls") 和 session.Output("ls") 看似一样,实际差异影响调试和容错能力。
-
session.Run()返回error,需显式检查;非零退出码会返回*ssh.ExitError,可用.ExitStatus()提取码值 -
session.Output()内部封装了Run()+io.ReadAll(stdout),掩盖了错误来源:是命令失败?还是读取超时?还是管道没关干净? - 别用
Output()替代Run()做关键命令,尤其涉及权限、路径、状态判断时
stdout/stderr 分离读取必须配 goroutine + Wait() 顺序
想拿到结构化日志、实时解析输出(如 tail)、或区分错误流,就不能用 CombinedOutput()。
- 必须按顺序调:
session.StdoutPipe()→session.StderrPipe()→session.Start("cmd")→ 启 goroutine 读 pipe →session.Wait() -
session.Wait()必须在所有读操作完成后调,否则可能 EOF 提前或数据截断 - 即使只关心 stdout,stderr 也得读(哪怕丢弃),否则某些 shell(如 zsh、带 strict mode 的 bash)会 hang 住
- 超时控制只能靠
context.WithTimeout,ClientConfig.Timeout只管握手阶段,对命令执行完全无效
跳板机(Bastion)链式连接不是加个中间 host 就行
双跳不是“先连 A,再从 A 连 B”,而是要用 ssh.Client 作为 transport 创建嵌套 dialer。
- 第一跳(Bastion)连通后,不能直接拿它的
net.Conn去 dial 第二跳;得用ssh.Dial的net.Conn参数传入已建立的 SSH 连接 - 典型做法:bastionClient.NewSession().StdinPipe() 作为第二跳 dial 的底层 transport
- 每跳都要独立配置
HostKeyCallback和认证方式,密钥或密码不能跨跳复用 - 链路中任一环节没
Close(),就会泄漏 goroutine 和文件描述符,压测时容易 fd 耗尽
session.Close()、每次 context.Cancel()、每处 HostKeyCallback 的落地方式,都得贴着运维现场去调。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











