2025-07-12 08:36:18
本文永久链接 – https://tonybai.com/2025/07/12/insanely-productive-in-go
大家好,我是Tony Bai。
在软件开发的世界里,我们总被灌输一种观念:选项越多,工具越强,生产力就越高。于是,我们追求功能最全的框架、最灵活的配置、以及最新潮的库。
但最近,在 Reddit 的 r/golang 社区,一篇名为《我感觉用 Go 的效率高得离谱》(I feel insanely productive in Go) 的帖子引发了近百条热议。一位曾坚信 TypeScript 和 Python 是“快语言”的开发者,在亲手尝试 Go 之后,发出了“真香”的感叹。
他发现,之前在 Node.js 生态中,光是技术选型——选择哪个运行时 (Bun? Deno?)、哪个 Web 框架 (Express? Fastify?)、哪个 ORM (Prisma? Drizzle?)——就足以耗费他整整一周的时间。他称之为“分析瘫痪” (Analysis Paralysis)。
而在 Go 中,他一天之内就搭建起了项目,开始编写业务逻辑。
这个故事并非孤例,它触动了无数从其他语言生态“迁徙”而来的开发者的心弦。它揭示了 Go 语言一个常常被误解,却又极其强大的超能力:正是那些看似“无聊”的、更少的选项,才赋予了我们惊人的生产力。
为什么选项更少反而更快?因为 Go 的设计哲学从一开始就在极力避免“分析瘫痪”,为开发者提供一条清晰、低阻力的“默认路径”。
Reddit 上的高赞评论一针见血:“在 Go 中,你不需要从一个框架开始,标准库已经提供了你需要的大部分东西。”
想写一个 Web 服务?net/http 就是你的起点。想操作数据库?database/sql 就在那里。想处理 JSON?encoding/json 已为你备好。
这些标准库不仅功能强大、性能卓越,更重要的是,它们是 Go 团队维护的、最稳定、最符合 Go 哲学的实现。这意味着,当你遇到问题时,你面对的是整个 Go 社区的集体智慧,而不是某个特定框架的小圈子。
当然,标准库并非万能。但当你需要第三方库时,你会发现 Go 的生态也与众不同。这里没有像 Java Spring 或 JavaScript React 那样“统治一切”的庞大框架)。
取而代之的,是一个由无数“小而美”的、可组合的库构成的生态系统。比如,你需要一个更强大的路由?chi 或 gorilla/mux 可以无缝地与标准库的 http.Handler 配合。你需要一个配置库?Viper 可以专注于做好这一件事。
这种模式的好处是显而易见的:你只引入你需要的,你的项目不会被一个臃肿的、你只用了 10% 功能的框架所绑架。
这种生态哲学,引出了一个更深层次的问题:你到底是一个“语言开发者”,还是一个“框架开发者”?
在许多其他生态中,框架的存在感甚至超过了语言本身。
* 一个 Java 工程师的简历上,写着“精通 Spring Boot”,这比“精通 Java”本身可能更具分量。
* 一个前端工程师,很可能对 React 的生命周期了如指掌,却对 JavaScript 的原生事件循环感到陌生。
这是因为,那些庞大的框架往往会重新定义语言的工作方式,引入大量“黑魔法”般的抽象和依赖注入。你写的是框架的 API,遵循的是框架的范式。你的技能,与这个框架深度绑定。一旦需要更换框架,或者脱离框架工作,你可能会发现自己几乎要重新学习一门“新语言”。
而 Go 社区,自始至终都在走一条“纯粹之路”。
这里的目标,永远是成为一个更好的 Go 开发者。因为标准库的强大和生态的“小工具”特性,无论你在哪个公司、哪个项目,你所依赖的核心思维和工具集都是一致的。你学到的 context 包的用法、interface 的设计模式、goroutine 的并发模型,这些知识具有极高的可移植性。
你不是在学习一个框架的“方言”,而是在掌握一门通用语言的“普通话”。这不仅提升了你个人的职业安全感,也极大地保障了项目的长期可维护性。
Go 的生产力优势,根植于其看似“固执”和“无聊”的约束之中。
它通过一个强大的标准库和一套约定俗成的惯例,为你铺设了一条清晰的道路,让你免于在无穷无尽的选择中耗尽心力。
它通过一个由小工具组成的、可组合的生态,让你专注于学习语言本身,而不是被某个庞大的框架所束缚,从而保护了你最宝贵的资产——你的知识和技能。
最终,Go 通过减少不必要的外部认知负荷,将你最宝贵的资源——注意力——解放出来,让你能真正地聚焦于业务逻辑,聚焦于创造价值。
这或许就是为什么,那么多开发者在体验过 Go 的“少即是多”之后,再也回不去了。因为他们发现,真正的自由与效率,恰恰来自于“恰到-好处”的约束。
资料链接:https://www.reddit.com/r/golang/comments/1lx52vz/insanely_productive_in_go_rethinking_everything/
你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?
继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!
我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。
目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。
© 2025, bigwhite. 版权所有.
2025-07-11 08:41:39
本文永久链接 – https://tonybai.com/2025/07/11/net-http-pprof-v2
大家好,我是Tony Bai。
Go 语言的性能诊断利器 net/http/pprof 即将迎来一次意义深远的变革。一项编号为 #74544 的新提案建议引入一个全新的 net/http/pprof/v2 包,旨在从根本上解决当前版本因“默认注册”行为带来的安全隐患。该提案不仅重塑了 pprof 端点的注册方式,还计划引入对 Go 1.25 飞行记录器(Flight Recorder)的支持、动态 CPU 采样率控制等一系列新功能。本文将深入解读该提案的核心内容、API 变化及其对 Go 开发者生态的潜在影响。
net/http/pprof 包是 Go 生产环境调试的基石,拥有超过 31,000 个公开包引用:
开发者只需匿名导入 _ “net/http/pprof”,即可在 DefaultServeMux 上自动注册 /debug/pprof/ 下的所有诊断端点。这种“零成本”的便利性,在内部服务中广受欢迎。
然而,正是这种“自动注册”的特性,成为了一个严重的安全隐患。对于面向公众的服务,开发者很容易因疏忽而将这些包含敏感运行时数据(如执行追踪、内存堆栈、Goroutine 信息等)的端点暴露在公网上,造成严重的数据泄露风险。提案作者 mknyszek 指出,许多大型项目都曾因此遭遇安全问题,不得不紧急修复。社区中,如 #46307 和 #42834 等 issue 也早已指出了这一设计缺陷。
此外,当前 net/http/pprof 包的维护也相对滞后,一些来自社区(如 DataDog)的合理功能增强提案(如 #71213、#66679)积压已久。提案认为,正是因为现有包存在根本性问题,导致团队不愿意在其上继续投入,从而阻碍了其发展。
为了彻底解决上述问题,提案的核心是创建一个全新的 net/http/pprof/v2 包,并引入一系列新功能以鼓励开发者迁移。
v2 包最大的变化是移除了 init 函数中的自动注册逻辑。匿名导入 net/http/pprof/v2 将不会产生任何副作用。取而代之的是,开发者需要显式地将 pprof 端点注册到指定的 *http.ServeMux 上。
为了简化这一过程,提案新增了一个便捷函数 RegisterHandlers:
// 将所有 pprof 处理器注册到指定的 mux,路径前缀为 /debug/pprof
func RegisterHandlers(mux *http.ServeMux)
对开发者的影响:
这意味着开发者将完全控制 pprof 端点的暴露范围。例如,可以轻松地创建一个只在内网端口监听的 ServeMux 来注册 pprof 处理器,而主服务则可以安全地暴露在公网,从而彻底杜绝意外泄露的风险。
// 生产环境推荐实践
func main() {
// 主服务 Mux,面向公网
mainMux := http.NewServeMux()
mainMux.HandleFunc("/", handlePublicRequest)
go http.ListenAndServe(":8080", mainMux)
// 诊断服务 Mux,仅监听本地回环地址
debugMux := http.NewServeMux()
pprof.RegisterHandlers(debugMux) // 使用 v2 的手动注册
log.Println("Serving pprof routes on http://localhost:6060/debug/pprof")
log.Fatal(http.ListenAndServe("localhost:6060", debugMux))
}
为了提供更强大的动态诊断能力,提案建议为 Go 1.25 中引入的飞行记录器 (Flight Recorder) 新增三个专属的 HTTP 端点:
对开发者的影响:
这将允许运维人员或外部监控系统在不重启服务、不进行完整 trace 的情况下,根据外部信号(如 CPU 告警)动态地抓取系统“事发现场”的短时追踪数据,极大地提升了线上问题排查的效率和灵活性。
提案还采纳了社区的建议,对现有的 cpu 和 trace 端点进行了增强:
提案也引发了一些讨论。例如,prattmic 建议 RegisterHandlers 应该允许用户自定义路径前缀,而不仅仅是硬编码的 /debug/pprof/。提案作者 mknyszek 则认为,提供一个标准、无需思考的默认路径是简化使用的关键,对于高度定制的场景,用户可以逐一注册 handler。
关于直接修改 v1 包的行为,提案认为这会破坏成千上万个现有项目的兼容性,风险过高。因此,引入一个全新的 v2 包,并通过 go vet 等工具引导用户迁移,是更为稳妥的路径。
net/http/pprof/v2 提案是一次意义重大的演进。它以安全为先的设计理念,修正了 Go 语言中最广为人知的“便利性陷阱”之一。通过强制开发者显式注册,它从根本上提升了 Go 应用的安全性。
更令人兴奋的是,提案并未止步于此。它积极地将飞行记录器、动态采样率等现代化诊断功能引入 pprof,使其不再仅仅是一个被动的数据采集工具,而是向一个动态、可交互的诊断平台迈进。
虽然这可能意味着开发者需要对现有项目进行少量代码修改,但换来的是更安全、更强大的诊断能力。我们有理由相信,这项提案一旦被接受并实现,将为 Go 语言的生产环境可观测性和问题排查能力带来一次质的飞跃。我们期待在未来的 Go 版本中看到这个 v2 包的到来。
你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?
继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!
我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。
目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。
© 2025, bigwhite. 版权所有.
2025-07-10 21:33:11
本文永久链接 – https://tonybai.com/2025/07/10/stop-building-ai-agents
大家好,我是Tony Bai。
如果你正在开发 AI 应用,你很可能听说过、尝试过,甚至正在挣扎于构建一个“AI Agent”。
我们都看过那些令人心潮澎湃的 Demo:一个 AI Agent 被赋予一个目标,然后它就能自主地规划、调用工具、浏览网页、编写代码,最终完成任务。于是,我们纷纷投身其中,搭建记忆系统、定义工具、编写角色背景……感觉就像在创造一个真正的数字生命,充满了力量和进步感。
但现实往往是残酷的。正如资深 AI 教育者 Hugo Bowne-Anderson 在他那篇引爆讨论的文章《Stop Building AI Agents》中描述的,他曾用 CrewAI 构建了一个“研究小组”:三个 Agent、五个工具,纸面上完美,实践中一塌糊涂。
这是一个“美丽的计划,以壮观的方式分崩离析”。这个故事听起来熟悉吗?
Hugo 一针见血地指出:问题的根源,可能不是你的实现细节,而是你从一开始就选择去构建一个 Agent。
要理解为什么 Agent 如此脆弱,我们必须先弄清它的定义。一个 LLM 应用通常具备四个特性:
1. 记忆 (Memory): 让 LLM 记住过去的交互。
2. 信息检索 (Information Retrieval): 通过 RAG 等方式为 LLM 提供上下文。
3. 工具使用 (Tool Usage): 赋予 LLM 调用函数和 API 的能力。
4. 工作流控制 (Workflow Control): 让 LLM 的输出来决定下一步使用哪个工具以及何时使用。
这第四点,正是“Agent”的定义,也是问题的核心!
当我们构建一个 Agent 时,我们实际上是把系统的控制权交给了 LLM。我们希望它能像一个自主的决策者一样,动态地编排整个工作流程。
但这就像是让一个充满创造力、才华横溢但情绪不定的艺术家去担任整个交响乐团的指挥。他可能会即兴发挥出惊人的乐章,但更可能的是,他会忘记看乐谱,让整个演奏陷入混乱。
大多数 Agent 系统崩溃,不是因为功能太少,而是因为复杂度太高、控制权失控。
Hugo 用一张简单的决策图告诉我们,在绝大多数场景下,我们需要的根本不是 Agent。
那么,如果不是 Agent,我们应该构建什么?
答案是:用更简单的、由你(开发者)的代码来控制流程的工作流模式。 下面这 5 个模式,源自 Anthropic 的研究,并由 Hugo 在实践中验证,足以解决 90% 的真实世界问题。
用例: 根据领英资料,撰写个性化的推广邮件。
这是一个典型的顺序任务。你先用一个 LLM 调用将非结构化的个人资料文本,转换为结构化的数据(姓名、公司、职位),然后再用第二个 LLM 调用,基于这些结构化数据和公司背景,生成一封定制邮件。
用例: 从一份简历中,同时提取多个部分的信息。
当你想一次性处理多个独立的子任务时,并行化是最佳选择。你可以定义多个并行的任务,如提取工作经历、提取技能列表、提取教育背景,然后让它们同时运行,最后汇总结果。
用例: 一个客户支持工具,根据用户问题类型分发到不同的处理流程。
路由模式就像一个智能交换机。你先用一个 LLM 或简单的逻辑来对输入进行分类(例如,这是“账单问题”还是“技术问题”),然后将请求“路由”到相应的专有处理函数或工作流中。控制权一旦交出,就不再收回。
用例: 一个需要将任务动态分解成多步的邮件生成器。
这看起来像路由,但有一个关键区别:控制权始终在“编排器”手中。编排器(可以是 LLM 或你的代码)负责做决策和协调,而“工作者”(通常是具体的函数)负责执行。例如,编排器先调用 LLM 将目标公司分类为“科技”或“非科技”,然后选择一个专门的“科技邮件工作者”或“非科技邮件工作者”来撰写邮件,并管理整个流程的始终。
用例: 优化一封营销邮件的语气和结构,以满足特定标准。
当你对输出质量有极高要求时,这个模式非常有用。一个“生成器”LLM 先生成初始内容,然后一个“评估器”LLM 对其进行打分。如果分数不达标,“评估器”会提供反馈,然后“生成器”根据反馈进行优化,如此循环,直到满足质量要求或达到重试上限。
读到这里,你可能会问,Agent 是否就一无是处?并非如此。Hugo 指出,Agent 在一类特定场景中表现出色:当有一个敏锐的人类在环中(Human-in-the-Loop)时。
在这些场景中,Agent 是一个创造力的放大器,而非一个自主的工人。它适用于不稳定的、探索性的工作,而非需要稳定可靠的自动化流程。
AI Agent 的概念被过度炒作和滥用。在大多数真实世界的应用中,我们并不需要一个拥有自主意识、能动态控制一切的复杂系统。
我们需要的,是更清晰、更简单、更可控的工作流结构。上述 5 种模式,为我们提供了强大的武器库。它们提醒我们软件工程的第一原则:从简单开始,逐步增加复杂性,并始终将控制权留在最可靠的地方——你自己的代码里。
所以,下一次当你准备构建下一个 LLM 应用时,请先停下来问自己:我真的需要一个 Agent 吗?还是一个简单的“提示词链”或“路由器”就足够了?
这个问题的答案,可能会为你节省下数周甚至数月的调试时间。
资料地址:https://decodingml.substack.com/p/stop-building-ai-agents
你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?
继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!
我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。
目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。
© 2025, bigwhite. 版权所有.
2025-07-10 08:56:42
本文永久链接 – https://tonybai.com/2025/07/10/mcp-official-go-sdk
大家好,我是Tony Bai。
随着大型语言模型(LLM)的能力边界不断扩展,“function calling”或“tool use”已成为释放其潜力的关键。MCP(Model Context Protocol)正是为此而生,它定义了一套标准的、与模型无关的通信规范,使得任何应用都能以“工具”的形式被 LLM 调用。
长期以来,mcp官方都没有发布go-sdk,Go社区也一直在使用像mark3labs/mcp-go这样的流行的第三方库。直到Google Go团队安排专人协助mcp组织进行了Go SDK的设计。
7月初,该Go SDK正式以modelcontextprotocol/go-sdk仓库的形式对外开源发布,这是Go 语言在这一浪潮中的一个里程碑事件。它的意义远超一个普通的库:
简而言之,它不仅仅是一个工具,更是 Go 语言与 AI 模型世界之间的一座标准化桥梁。
MCP 协议设计了灵活的通信方式,以适应不同的部署场景。官方 Go SDK 对此提供了出色的支持。主要包括以下几种类型:
标准输入/输出 (Stdio):这是最简单的模式,客户端通过启动一个子进程(MCP Server),并通过其 stdin 和 stdout 进行 JSON-RPC 通信。这种模式非常适合本地工具、CLI 插件或 Sidecar 模型的场景。我们将使用此模式构建基础工具服务和文件系统服务。
HTTP 流式传输 (Streamable HTTP):这是 MCP 规范中最新、最推荐的 HTTP 模式。它通过一系列的 GET 和 POST 请求实现了一个可恢复的、无状态的会话管理机制,非常适合构建可扩展、高可用的网络服务。我们将使用此模式构建多路复用 HTTP 服务。
服务器发送事件 (SSE):这是早期 MCP 规范中的一种 HTTP 模式,在社区版 SDK 中较为常见。官方 SDK 也提供了 SSEHandler 以支持这种模式,但新的 StreamableHTTPHandler 功能更强大,是未来的方向。
尽管我们在此不深入探讨其完整的设计文档,但理解以下几个核心概念对于后续的实践至关重要:
Server 定义了“能做什么”,而 Session 则是“正在与谁通信”的实例。这种解耦设计为构建灵活、可扩展的服务提供了基础。
现在,让我们动手构建几个实用的 MCP 服务,来体验官方 SDK 的强大功能。
这是最经典的“Hello, World”场景,通过 stdio 运行,用于展示如何定义一个简单的工具。
完整代码:greeter/main.go
// mcp-go-sdk/greeter/main.go
package main
import (
"context"
"fmt"
"log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// HiParams 定义了工具的输入参数,强类型保证
type HiParams struct {
Name string json:"name"
}
// SayHi 是工具的具体实现
func SayHi(ctx context.Context, _ *mcp.ServerSession, params *mcp.CallToolParamsFor[HiParams]) (*mcp.CallToolResultFor[any], error) {
resultText := fmt.Sprintf("Hi %s, welcome to the Go MCP world!", params.Arguments.Name)
return &mcp.CallToolResultFor[any]{
Content: []mcp.Content{
&mcp.TextContent{Text: resultText},
},
}, nil
}
func main() {
// 1. 创建 Server 实例
server := mcp.NewServer("greeter-server", "1.0.0", nil)
// 2. 添加工具
// NewServerTool 利用泛型和反射自动生成输入 schema
server.AddTools(
mcp.NewServerTool("greet", "Say hi to someone", SayHi),
)
// 3. 通过 StdioTransport 运行服务,它会监听标准输入/输出
log.Println("Greeter server running over stdio...")
if err := server.Run(context.Background(), mcp.NewStdioTransport()); err != nil {
log.Fatalf("Server run failed: %v", err)
}
}
在不依赖任何特殊客户端的情况下,我们可以通过管道向这个基于 stdio 的服务发送一系列原生的 JSON-RPC 消息,来模拟完整的客户端握手和工具调用流程。
步骤一:运行服务并发送请求序列
打开你的终端,执行以下命令。这行命令会使用 printf 来确保每个 JSON 对象都以换行符分隔,模拟一个完整的会话流程:
1. 发送 initialize 请求,启动会话。
2. 发送 initialized 通知,确认会话建立。
3. 发送 tools/call 请求,调用 greet 工具。
在greeter目录下执行下面命令:
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"test-cli","version":"0.1"},"protocolVersion":"2025-03-26"}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"greet","arguments":{"name":"Go MCP Enthusiast"}}}' \
| go run main.go
预期输出:
服务会处理这三个消息,并对两个有 ID 的请求(initialize 和 tools/call)作出响应。你将看到两个 JSON-RPC 响应对象被打印到标准输出(顺序可能会因并发处理而不同,但内容是固定的):
2025/07/08 17:05:46 Greeter server running over stdio...
{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"completions":{},"logging":{},"prompts":{"listChanged":true},"resources":{"listChanged":true},"tools":{"listChanged":true}},"protocolVersion":"2025-03-26","serverInfo":{"name":"greeter-server","version":"1.0.0"}}}
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"Hi Go MCP Enthusiast, welcome to the Go MCP world!"}]}}
看到这两个响应,证明我们的 Greeter 服务已经成功地完成了握手并正确响应了工具调用。
这个场景也通过 stdio 运行,展示了如何通过 Resource 机制,安全地向 LLM 暴露本地文件系统的读写能力。
// fileserver/main.go
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func main() {
server := mcp.NewServer("filesystem-server", "1.0.0", nil)
pwd, err := os.Getwd()
if err != nil {
log.Fatalf("Failed to get current directory: %v", err)
}
log.Printf("File server serving from directory: %s", pwd)
// 使用我们自己实现的 File Handler
handler := createFileHandler(pwd)
// 添加一个虚构的资源,用于列出目录内容
server.AddResources(&mcp.ServerResource{
Resource: &mcp.Resource{
URI: "mcp://fs/list",
Name: "list_files",
Description: "List all non-directory files in the current directory.",
},
Handler: listDirectoryHandler(pwd),
})
// 添加一个资源模板,用于读取指定的文件
server.AddResourceTemplates(&mcp.ServerResourceTemplate{
ResourceTemplate: &mcp.ResourceTemplate{
Name: "read_file",
URITemplate: "file:///{+filename}",
Description: "Read a specific file from the directory. 'filename' is the relative path to the file.",
},
Handler: handler,
})
log.Println("File system server running over stdio...")
if err := server.Run(context.Background(), mcp.NewStdioTransport()); err != nil {
log.Fatalf("Server run failed: %v", err)
}
}
// createFileHandler 是一个简化的、用于演示的 ResourceHandler 工厂函数。
func createFileHandler(baseDir string) mcp.ResourceHandler {
return func(ctx context.Context, ss *mcp.ServerSession, params *mcp.ReadResourceParams) (*mcp.ReadResourceResult, error) {
// 注意:在生产环境中,这里必须调用 ss.ListRoots() 来获取客户端授权的
// 根目录,并进行严格的安全检查。
// 为了让这个入门示例能用简单的管道命令验证,我们暂时省略了这个双向调用。
requestedPath := filepath.Join(baseDir, filepath.FromSlash(params.URI[len("file:///"):]))
data, err := os.ReadFile(requestedPath)
if err != nil {
if os.IsNotExist(err) {
return nil, mcp.ResourceNotFoundError(params.URI)
}
return nil, fmt.Errorf("failed to read file: %w", err)
}
return &mcp.ReadResourceResult{
Contents: []*mcp.ResourceContents{
{URI: params.URI, MIMEType: "text/plain", Text: string(data)},
},
}, nil
}
}
// listDirectoryHandler 是一个自定义的 ResourceHandler,用于实现列出目录的功能
func listDirectoryHandler(dir string) mcp.ResourceHandler {
return func(ctx context.Context, ss *mcp.ServerSession, params *mcp.ReadResourceParams) (*mcp.ReadResourceResult, error) {
// 同样,为简化本地验证,暂时省略对 ss.ListRoots() 的调用。
entries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("failed to read directory: %w", err)
}
var fileList string
for _, e := range entries {
if !e.IsDir() {
fileList += e.Name() + "\n"
}
}
if fileList == "" {
fileList = "(The directory is empty or contains no files)"
}
return &mcp.ReadResourceResult{
Contents: []*mcp.ResourceContents{
{URI: params.URI, MIMEType: "text/plain", Text: fileList},
},
}, nil
}
}
文件服务同样需要完整的握手流程。我们将用与上面类似的方式来验证其功能。
步骤一:准备测试文件
首先,在你的项目根目录下创建一个简单的文本文件。
echo "Hello from the File System MCP Server!" > my-test-file.txt
步骤二:验证“列出文件”功能
我们发送包含 initialize、initialized 和 resources/read 的请求序列。
在fileserver下执行下面命令:
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"test-cli","version":"0.1"},"protocolVersion":"2025-03-26"}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}' \
'{"jsonrpc":"2.0","id":2,"method":"resources/read","params":{"uri":"mcp://fs/list"}}' \
| go run main.go
预期输出:
你将看到 initialize 的响应,以及 resources/read 的响应,后者包含了目录文件列表。
2025/07/08 18:13:47 File system server running over stdio...
{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"completions":{},"logging":{},"prompts":{"listChanged":true},"resources":{"listChanged":true},"tools":{"listChanged":true}},"protocolVersion":"2025-03-26","serverInfo":{"name":"filesystem-server","version":"1.0.0"}}}
{"jsonrpc":"2.0","id":2,"result":{"contents":[{"uri":"mcp://fs/list","mimeType":"text/plain","text":"go.mod\ngo.sum\nmain.go\nmy-test-file.txt\n"}]}}
步骤三:验证“读取文件”功能
现在,我们发送请求序列来读取 my-test-file.txt 的内容。
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"test-cli","version":"0.1"},"protocolVersion":"2025-03-26"}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}' \
'{"jsonrpc":"2.0","id":3,"method":"resources/read","params":{"uri":"file:///my-test-file.txt"}}' \
| go run main.go
预期输出:
除了 initialize 的响应外,你将看到包含文件内容的 resources/read 响应。
2025/07/08 18:15:12 File server serving from directory: /Users/tonybai/go/src/github.com/bigwhite/experiments/mcp-go-sdk/fileserver
2025/07/08 18:15:12 File system server running over stdio...
{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"completions":{},"logging":{},"prompts":{"listChanged":true},"resources":{"listChanged":true},"tools":{"listChanged":true}},"protocolVersion":"2025-03-26","serverInfo":{"name":"filesystem-server","version":"1.0.0"}}}
{"jsonrpc":"2.0","id":3,"result":{"contents":[{"uri":"file:///my-test-file.txt","mimeType":"text/plain","text":"Hello from the File System MCP Server\n"}]}}
步骤四:清理
测试完成后,可以删除测试文件。
rm my-test-file.txt
这个场景展示了如何使用 StreamableHTTPHandler 在单个 HTTP 端点上提供多个不同的 MCP 服务。
完整代码:httpserver/main.go
package main
import (
"context"
"fmt"
"log"
"net/http"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// HiParams 和 SayHi 函数与场景一相同
type HiParams struct{ Name string json:"name" }
func SayHi(ctx context.Context, _ *mcp.ServerSession, params *mcp.CallToolParamsFor[HiParams]) (*mcp.CallToolResultFor[any], error) {
resultText := fmt.Sprintf("Hi %s, this response is from the HTTP server!", params.Arguments.Name)
return &mcp.CallToolResultFor[any]{
Content: []mcp.Content{&mcp.TextContent{Text: resultText}},
}, nil
}
// AddParams 和 Add 工具的实现
type AddParams struct{ A, B int }
func Add(_ context.Context, _ *mcp.ServerSession, params *mcp.CallToolParamsFor[AddParams]) (*mcp.CallToolResultFor[any], error) {
result := params.Arguments.A + params.Arguments.B
return &mcp.CallToolResultFor[any]{
Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("The sum is: %d", result)}},
}, nil
}
func main() {
// 1. 创建 Greeter 服务实例
greeterServer := mcp.NewServer("greeter-service", "1.0", nil)
greeterServer.AddTools(mcp.NewServerTool("greet", "Say hi", SayHi))
// 2. 创建 Math 服务实例
mathServer := mcp.NewServer("math-service", "1.0", nil)
mathServer.AddTools(mcp.NewServerTool("add", "Add two integers", Add))
// 3. 创建 StreamableHTTPHandler
handler := mcp.NewStreamableHTTPHandler(func(request *http.Request) *mcp.Server {
log.Printf("Routing request for URL: %s\n", request.URL.Path)
switch request.URL.Path {
case "/greeter":
return greeterServer
case "/math":
return mathServer
default:
return nil // 返回 nil 将导致 404 Not Found
}
}, nil)
// 4. 启动标准的 Go HTTP 服务器
addr := ":8080"
log.Printf("Multi-service MCP server listening at http://localhost%s\n", addr)
if err := http.ListenAndServe(addr, handler); err != nil {
log.Fatalf("HTTP server failed: %v", err)
}
}
与基于 stdio 的简单服务不同,验证 Streamable HTTP 服务使用 curl 等工具会非常繁琐。这是因为 MCP 是一个有状态的协议,要求客户端在发送工具调用之前,必须先完成一个包含 initialize 请求和 initialized 通知的多步“握手”流程来建立会话。
一个简单的 curl 命令无法管理这种有状态的交互。因此,最理想的验证方式是使用一个真正的 MCP 客户端。我们将在下一节构建这样一个客户端——agent,然后用集成了大模型的它来统一验证我们创建的所有三个服务,包括这个 HTTP 服务。
在前面的章节中,我们成功构建了三种不同类型的 MCP 服务。现在,是时候将它们与 AI 大模型(以 DeepSeek 为例)集成,构建一个能够调度这些mcp server工具的智能 Agent 了。
一个常见的思路可能是创建一个通用的命令行工具(CLI)来调用这些服务,然后让我们的 Go Agent 程序去执行这个 CLI。然而,既然我们的 Agent 本身就是用 Go 编写的,一个更优雅、更高效、更符合 Go 语言习惯的方式是:让 Agent 程序直接导入 modelcontextprotocol/go-sdk,将自己作为原生的 MCP 客户端来与服务通信。
这种方法避免了不必要的进程开销和数据序列化,使得整个系统更加内聚和高性能。接下来,我们将编写这样一个 Go Agent。
这个程序将承担所有角色:它既是与 DeepSeek 模型对话的主循环,也是调用我们 MCP 服务的客户端。
准备工作:
完整代码:agent/main.go
// agent/main.go
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"os/exec"
"strings"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/openai/openai-go"
"github.com/openai/openai-go/option"
)
// serverConfig 结构体用于管理不同 MCP 服务的连接信息
type serverConfig struct {
ServerCmd string // 用于 stdio 服务
HTTPAddr string // 用于 http 服务
}
// toolRegistry 映射对 LLM 友好的工具别名到其服务配置
var toolRegistry = map[string]serverConfig{
"greet": {ServerCmd: "go run ../greeter/main.go"},
"add": {HTTPAddr: "http://localhost:8080/math"},
"list_files": {ServerCmd: "go run ../fileserver/main.go"},
"read_file": {ServerCmd: "go run ../fileserver/main.go"},
}
// invokeMCPTool 是 Agent 的核心函数,负责直接与 MCP 服务通信
func invokeMCPTool(toolAlias string, arguments map[string]interface{}) (string, error) {
config, ok := toolRegistry[toolAlias]
if !ok {
return "", fmt.Errorf("unknown tool alias: %s", toolAlias)
}
// 1. 将 LLM 友好的别名和参数,转换为真正的 MCP 请求
mcpToolName := toolAlias
mcpArguments := arguments
if toolAlias == "list_files" {
mcpToolName = "resources/read"
mcpArguments = map[string]interface{}{"uri": "mcp://fs/list"}
} else if toolAlias == "read_file" {
mcpToolName = "resources/read"
if filename, ok := arguments["filename"].(string); ok {
mcpArguments = map[string]interface{}{"uri": "file:///" + filename}
} else {
return "", fmt.Errorf("tool 'read_file' requires a 'filename' argument")
}
}
// 2. 创建 MCP 客户端实例
client := mcp.NewClient("go-agent", "1.0", nil)
// 3. 根据配置选择并创建 Transport
var transport mcp.Transport
if config.ServerCmd != "" {
cmdParts := strings.Fields(config.ServerCmd)
transport = mcp.NewCommandTransport(exec.Command(cmdParts[0], cmdParts[1:]...))
} else {
transport = mcp.NewStreamableClientTransport(config.HTTPAddr, nil)
}
// 4. 授权客户端访问本地文件系统(仅对文件服务调用有效)
client.AddRoots(&mcp.Root{URI: "file://./"})
// 5. 连接到服务器,建立会话
ctx := context.Background()
session, err := client.Connect(ctx, transport)
if err != nil {
return "", fmt.Errorf("failed to connect to MCP server for tool %s: %w", toolAlias, err)
}
defer session.Close() // 每次调用都是一个独立的会话,确保关闭
// 6. 执行调用并处理结果
var resultText string
if mcpToolName == "resources/read" {
res, err := session.ReadResource(ctx, &mcp.ReadResourceParams{
URI: mcpArguments["uri"].(string),
})
if err != nil {
return "", fmt.Errorf("ReadResource failed: %w", err)
}
var sb strings.Builder
for _, c := range res.Contents {
sb.WriteString(c.Text)
}
resultText = sb.String()
} else {
res, err := session.CallTool(ctx, &mcp.CallToolParams{
Name: mcpToolName,
Arguments: mcpArguments,
})
if err != nil {
return "", fmt.Errorf("CallTool failed: %w", err)
}
if res.IsError {
return "", fmt.Errorf("tool execution failed: %s", res.Content[0].(*mcp.TextContent).Text)
}
resultText = res.Content[0].(*mcp.TextContent).Text
}
return resultText, nil
}
func main() {
apiKey := os.Getenv("DEEPSEEK_API_KEY")
if apiKey == "" {
log.Fatal("DEEPSEEK_API_KEY environment variable not set.")
}
client := openai.NewClient(
option.WithAPIKey(apiKey),
option.WithBaseURL("https://api.deepseek.com/v1"),
)
// 为所有工具使用合法的名称,特别是为 resources/read 创建别名
tools := []openai.ChatCompletionToolParam{
{
Function: openai.FunctionDefinitionParam{
Name: "greet",
Description: openai.String("Say hi to someone."),
Parameters: openai.FunctionParameters{
"type": "object", "properties": map[string]interface{}{"name": map[string]string{"type": "string", "description": "Name of the person to greet"}}, "required": []string{"name"},
},
},
},
{
Function: openai.FunctionDefinitionParam{
Name: "add",
Description: openai.String("Add two integers."),
Parameters: openai.FunctionParameters{
"type": "object", "properties": map[string]interface{}{"A": map[string]string{"type": "integer"}, "B": map[string]string{"type": "integer"}}, "required": []string{"A", "B"},
},
},
},
{
Function: openai.FunctionDefinitionParam{
Name: "list_files",
Description: openai.String("List all non-directory files in the current project directory."),
Parameters: openai.FunctionParameters{"type": "object", "properties": map[string]interface{}{}},
},
},
{
Function: openai.FunctionDefinitionParam{
Name: "read_file",
Description: openai.String("Read the content of a specific file."),
Parameters: openai.FunctionParameters{
"type": "object", "properties": map[string]interface{}{"filename": map[string]string{"type": "string", "description": "The name of the file to read."}}, "required": []string{"filename"},
},
},
},
}
messages := []openai.ChatCompletionMessageParamUnion{
openai.SystemMessage("You are a helpful assistant with access to local tools. You must call tools by using the tool_calls response format. Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."),
openai.UserMessage("Hi, can you greet my friend Alex, add 5 and 7, and then list the files in my project?"),
}
ctx := context.Background()
for i := 0; i < 5; i++ {
log.Println("--- Sending request to DeepSeek ---")
resp, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{Model: "deepseek-chat", Messages: messages, Tools: tools})
if err != nil {
log.Fatalf("ChatCompletion error: %v\n", err)
}
if len(resp.Choices) == 0 {
log.Fatal("No choices returned from API")
}
msg := resp.Choices[0].Message
messages = append(messages, msg.ToParam())
if msg.ToolCalls != nil {
for _, toolCall := range msg.ToolCalls {
functionName := toolCall.Function.Name
var arguments map[string]interface{}
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &arguments); err != nil {
log.Fatalf("Failed to unmarshal function arguments: %v", err)
}
log.Printf("--- LLM wants to call tool: %s with args: %v ---\n", functionName, arguments)
// 直接调用我们的 Go 函数,该函数内建了 MCP 客户端逻辑
toolResult, err := invokeMCPTool(functionName, arguments)
if err != nil {
log.Printf("Tool call failed: %v\n", err)
toolResult = fmt.Sprintf("Error executing tool: %v", err)
}
log.Printf("--- Tool result: ---\n%s\n---------------------\n", toolResult)
messages = append(messages, openai.ToolMessage(toolResult, toolCall.ID))
}
continue
}
log.Println("--- Final response from LLM ---")
log.Println(msg.Content)
return
}
log.Println("Reached max conversation turns.")
}
注:上述代码使用了OpenAI的function calling api,不过即便不用function calling api,通过prompt依然可以实现mcp server接口的调用(需要自行解析response),大家可以自行实现一下。
现在,我们的 agent 程序已经是一个功能齐全的、内建了 MCP 客户端的智能体。让我们来验证它的工作流程。
启动 httpserver:
agent 会通过 HTTP 调用 math 服务,所以我们必须先在后台运行它。
go run ./httpserver/main.go & HTTP_PID=$!
创建测试文件:
为文件服务准备一个可供读取的文件。
echo "This file will be read by our Go AI Agent." > agent-test.txt
运行 agent 程序:
确保你的 DEEPSEEK_API_KEY 已经设置。
go run ./agent/main.go
预期的输出流程:
你的终端将清晰地展示 AI Agent 的思考和行动链。它直接在内部与各个 MCP 服务进行高效的 Go-to-Go通信。
$DEEPSEEK_API_KEY=<your_deepseek_api_key> go run main.go
2025/07/08 19:17:42 --- Sending request to DeepSeek ---
2025/07/08 19:17:53 --- LLM wants to call tool: greet with args: map[name:Alex] ---
2025/07/08 19:17:53 --- Tool result: ---
Hi Alex, welcome to the Go MCP world!
---------------------
2025/07/08 19:17:53 --- LLM wants to call tool: add with args: map[A:5 B:7] ---
2025/07/08 19:17:53 --- Tool result: ---
The sum is: 12
---------------------
2025/07/08 19:17:53 --- LLM wants to call tool: list_files with args: map[] ---
2025/07/08 19:17:53 --- Tool result: ---
go.mod
go.sum
main.go
---------------------
2025/07/08 19:17:53 --- Sending request to DeepSeek ---
2025/07/08 19:18:07 --- Final response from LLM ---
2025/07/08 19:18:07 Here's what you asked for:
1. **Greeting for Alex**: Hi Alex, welcome to the Go MCP world!
2. **Addition of 5 and 7**: The sum is 12.
3. **Files in your project**:
- go.mod
- go.sum
- main.go
Let me know if you'd like to do anything else!
最后,做一下清理工作:
kill $HTTP_PID
rm agent-test.txt
通过本次从零到一的实践,我们不仅学习了如何使用 modelcontextprotocol/go-sdk 构建支持不同通信协议的 MCP 服务,更重要的是,我们探索并实现了将 Go Agent 程序直接作为原生 MCP 客户端的实践。
这种直接通过库调用的内聚架构,相比于通过外部 CLI 工具进行解耦的方式,充分发挥了 Go 语言的优势:
modelcontextprotocol/go-sdk 不仅仅是一个协议的实现,它更像一个宣言:Go 语言凭借其出色的并发模型、强大的类型系统和简洁的工程哲学,完全有能力成为构建下一代高性能、高可靠性 AI Agent 和工具化应用的首选后端语言。
虽然官方 SDK 仍在快速迭代中,但其展现出的潜力和清晰的设计哲学已经足够令人振奋。我们鼓励所有对 Go 和 AI 结合感兴趣的开发者,立即上手体验。这个 SDK 无疑将成为连接你的 Go 程序与广阔智能模型世界之间最坚固、最标准的桥梁。
本文涉及源码可以在这里下载。
你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?
继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!
我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。
目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。
© 2025, bigwhite. 版权所有.
2025-07-09 08:23:57
本文永久链接 – https://tonybai.com/2025/07/09/gemini-cli-starting-guide
大家好,我是Tony Bai。
在软件开发的历史长河中,我们与机器的交互界面经历了一场有趣的轮回。
曾几何时,发光的绿色字符在黑色屏幕上跳动,命令行是我们掌控一切的神圣权杖。从编辑器(Vim/Emacs)到编译器,再到版本控制,整个世界都安然地存在于终端的心跳之中。
随后,图形用户界面(GUI)带来了集成开发环境(IDE)的黄金时代。Borland、Visual Studio、Eclipse、JetBrains… 我们将一切都“集成”到了一个窗口里,享受着点击、拖拽和可视化调试带来的便利。命令行似乎一度退居次席,成了执行零散脚本的“后台工具”。
而今天,当我们以为 VS Code 这样轻快、插件丰富的编辑器已经统一江湖时,一股强劲的“复古之风”正悄然刮起。但这一次,它并非简单的怀旧,而是一场由 AI 驱动的、向命令行的“伟大回归”。
为什么是现在?
因为 AI 的出现,再次打破了 IDE 创造的“完美闭环”。我们发现自己又一次陷入了新的“工作流摩擦”:我们的代码在一个窗口,而我们的 AI “外脑”(ChatGPT/Gemini Web)在另一个窗口。我们成了上下文的搬运工,在复制粘贴中消耗着宝贵的专注力。
IDE 插件虽有所缓解,但它们更像是被“关在笼子里”的 AI,能力受限于 IDE 提供的 API。它们无法真正理解你的整个系统环境,无法为你执行一条 docker build 命令,更无法调用你私有的测试脚本。
我们需要的,不仅仅是一个会写代码的 AI。我们需要一个能理解我们整个工作流,并能动手执行的 AI。敏锐的开发者和 AI 公司都已意识到,下一个效率的爆发点,不在 GUI,而在那片最经典、最高效的战场——命令行。
这,正是这场“命令行革命”的核心。
于是,一个全新的物种 “命令行AI智能体 (Command-Line AI Agent)” 开始涌现。OpenAI Codex、Claude Code等拥有强大能力的商业公司背书的各类智能体脚本便像雨后春笋般出现。而在这一新兴的赛道上,Google也携其 Gemini CLI,给出了一个与众不同的答案。它更侧重于工作流自动化 (Workflow Automation)。更具吸引力的是,通过个人 Google 账户认证,你就能享受到慷慨的免费使用额度,这极大地降低了每一位开发者体验这场命令行革命的门槛。
正是因为 Gemini CLI 的这种“慷慨”,我认为它值得一次系统而深入的探索。
我即将开启一个全新的微专栏系列 《Gemini CLI:重新定义命令行 AI 开发》,该专栏将用 5篇由浅入深的实战文章,向你完整地展示,当今最前沿的大语言模型(比如Gemini 2.5 pro),是如何与开发世界最经典、最高效的交互界面——命令行——相结合,从而迸发出惊人的能量。此外,专栏中的示例均采用Go代码。
在这个系列中,你将看到:
第一篇《入门篇》: 我们将为你带来初见的“Wow 时刻”。你将看到 Gemini CLI 如何仅用一个 @ 符号,就读懂并分析一个你完全陌生的 Go 项目,这是一种你从未体验过的、AI 与本地文件系统的深度融合。
第二篇《实战篇》: 我们将带你彻底驾驭 @、!、/ 这三驾马车,在真实的 Go 项目中,完成从代码分析、编译测试到 Git 操作的全流程。我们将让你相信,大部分开发任务,都可以且应该在命令行中一气呵成。
第三篇《进阶篇》: 我们将为你系上 AI 时代的“安全带”。你将掌握 Checkpointing (快照回滚) 机制,让你可以像玩游戏读档一样,随时回退 AI 的任何一次代码修改,从而安心地让它进行最大胆的重构实验。
第四篇《扩展篇》: 我们将带你扮演“造物主”的角色。你将学会如何通过自定义工具和 MCP 服务器,将你自己的脚本、公司的内部 API,甚至任何你能想到的外部系统,全部接入 Gemini CLI 的能力版图,打造真正属于你的神器。
第五篇《应用篇》: 我们将展示一个终极工作流。如何用一句自然语言指令,驱动 AI 自动完成在线研究、信息整合、内容创作,并最终将一篇完整的 Markdown 技术报告保存在你的本地。这,是自动化思想的极致体现。
这不关乎怀旧,这关乎进化。
这不是退回终端,而是带着 AI 的力量,重返我们最熟悉的战场。
如果你对提升开发效率有极致的追求,如果你相信最好的工具就应该在弹指之间,那么,请锁定我们。
点击下方卡片,即刻关注,与我们一同见证这场正在发生的革命!
如果你和我们一样,对探索 Go 与 AI 的前沿交叉领域充满热情,那么这个微专栏仅仅是一个开始。
为了感谢核心读者的支持,并打造一个更具深度和互动性的交流平台,我决定:
本付费微专栏的全部 5 篇文章,将在我的知识星球「Go & AI 精进营」中同步免费发布!
扫描下方二维码,加入「Go & AI 精进营」,与我们一起,站在未来看现在。
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。
© 2025, bigwhite. 版权所有.