Go用crypto/ssh远程执行命令需手动创建session、处理I/O管道并调用Wait(),漏掉任一环节易卡死或无输出;client.Run()仅适用于短命命令,复用连接须每次NewSession并Close。

Go 用 crypto/ssh 远程执行命令,不是“连上就跑”,而是得手动处理连接、会话、stdin/stdout/stderr 和错误传播——漏掉任一环节,命令看似执行了,实际可能卡死、超时或根本没输出。
ssh.Client.Dial 后必须显式调用 ssh.NewSession
很多人以为 ssh.Client 连上就能直接执行,其实它只管 TCP + SSH 协议层;真正跑命令的是 ssh.Session。不新建 session,client.Run() 或 client.Start() 都会 panic 或返回 ssh: subsystem request failed。
-
client.Run("ls -l")是便捷封装,但底层仍会临时创建 session 并阻塞等待退出 —— 它适合单条短命命令,不适用于需要读取实时输出或交互的场景 - 需复用连接时,应复用
*ssh.Client,但每次执行命令前都得调用client.NewSession(),用完记得session.Close() - session 不是 goroutine-safe 的,多个协程并发用同一个 session 会导致
write tcp: use of closed network connection
执行带输出的命令必须显式启动并读取 stdout/stderr
session.Run() 会阻塞直到命令结束,但不会返回任何输出;想拿到结果,得用 session.StdoutPipe() 和 session.StderrPipe(),再配合 session.Start() + session.Wait()。
- 顺序不能错:先调
session.StdoutPipe(),再session.Start(),否则 pipe 返回 nil - 即使命令秒退,也要等
session.Wait()才能确保 stdout 被完整写入,否则io.ReadAll()可能提前 EOF - stderr 默认不合并到 stdout,如需统一捕获,可设
session.Stderr = session.Stdout,但要注意日志混杂问题
session, _ := client.NewSession()
defer session.Close()
stdout, _ := session.StdoutPipe()
session.Start("ps aux | grep go")
session.Wait() // 必须等这里,否则 stdout 可能没写完
out, _ := io.ReadAll(stdout)
认证失败常见于 key 格式或权限配置不匹配
报错 ssh: handshake failed: ssh: unable to authenticate, attempted methods [none publickey] 或 permission denied (publickey),基本不是密码错了,而是 key 加载或服务端配置出了问题。
立即学习“go语言免费学习笔记(深入)”;
- 私钥必须是 PEM 编码的 RSA/ECDSA/ED25519,OpenSSH 新格式(
-----BEGIN OPENSSH PRIVATE KEY-----)默认不支持,需用ssh-keygen -p -m PEM -f id_rsa转换 - 用
ssh.PublicKeys认证时,ssh.ParsePrivateKey()错误常被忽略,务必检查返回 err - 服务端
/etc/ssh/sshd_config中PubkeyAuthentication yes和AuthorizedKeysFile路径必须生效,且~/.ssh/authorized_keys权限不能大于 600
长时命令或交互式操作容易因 SSH KeepAlive 缺失而断连
默认 SSH 连接空闲 30 秒左右就会被中间设备(防火墙、负载均衡)静默断开,表现为 read tcp: i/o timeout 或直接 EOF,尤其在执行 tail -f、apt update 等耗时操作时。
- 客户端需开启 keepalive:在
ssh.ClientConfig的HostKeyCallback后添加SetKeepAlive(30 * time.Second) - 更可靠的方式是配置服务端:修改
/etc/ssh/sshd_config,加TCPKeepAlive yes、ClientAliveInterval 60、ClientAliveCountMax 3 - 对非阻塞流式读取(如实时日志),建议用
session.Shell()+session.StdinPipe()替代Start(),并自行管理心跳和重连逻辑
最常被跳过的一步是 session.Close() —— 它不只是释放内存,还负责发送 SSH channel-close 消息,否则远端进程可能残留、文件描述符泄漏,多次执行后触发 too many open files 或连接拒绝。


















