四种仓库,四种摆法 左边一张目录树草图:cmd、internal、pkg、go.work 各配一句注记。右边货架上四个盒子:第一个盒子里三层纸板叠放(分层),第二个盒子上方一个首尾相接的圆环(控制循环),第三个盒子上方一排放置高矮不一的竖条(平铺的功能包),第四个是三个小方块被一条带子扎在一起(多 module)。 $ tree -L 1 cmd/ 每个组件一个 main internal/ 编译器围栏 pkg/ 争议最大的一个 go.work 14 个 module 同一套词汇,四种答案 →

从 Java 转 Go 的人,第一次打开 kubernetes/kubernetes 的源码,大多会做同一个动作:找 controller 在哪、service 在哪、DAO 在哪。毕竟在 Spring 的世界里,代码就住在这三层里。但 ls 的结果是一套陌生的词汇:cmd、pkg、staging、api、vendor……一个熟悉的名字都没有。

更添乱的是,搜索引擎会把我们带到一份 “Standard Go Project Layout”,看起来这就是官方答案;而 Go 的技术负责人 Russ Cox 专门去那个仓库开过一个 issue,标题就叫 “this is not a standard Go project layout”1。连「标准布局」本身都是一场误会,那 Go 大工程的代码到底按什么组织?

我们把问题拆开:

  1. cmd/、pkg/、internal/ 这三个随处可见的目录,各是什么来历?谁定的?
  2. 为什么 etcd 顶层连 cmd/ 和 internal/ 都没有?
  3. k8s 里明明有个 pkg/controller——它是 Java 意义上的 controller 吗?那 service、DAO 的对应物在哪?
  4. 有没有 Go 项目真的按 handler/service/db 分层?
  5. prometheus 为什么又是另一种答案?

本文顺着这五个问题往下走,样本是四个真实的大工程:kubernetes、multica(一个用 Go 写的 AI agent 协作平台)、etcd、prometheus。文中所有目录名、文件名和行号,都来自 2026-10-31 当天各仓库的主分支。


一、cmd、pkg、internal:三个目录,三种来历

先看 k8s 的顶层:

$ ls kubernetes/
api/ build/ cluster/ cmd/ docs/ hack/ pkg/ plugin/
staging/ test/ third_party/ vendor/ …

(staging 顺带交代一句,后面不再展开:它是「待拆分仓库的孵化区」,staging/src/k8s.io/ 下的代码是唯一权威副本,由 publishing-bot 单向发布成 client-go、apimachinery 等独立仓库2。)

cmd/ 是多二进制的收纳处。 cmd/ 下每个子目录编译出一个二进制,目录名就是二进制名。k8s 的 cmd/ 下有 kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kubectl、kubeadm 等组件入口,外加十几个代码生成工具。这些入口文件统一的特点是非常薄:cmd/kube-apiserver/apiserver.go 的 main 全部有效代码只有三行(NewAPIServerCommand → cli.Run → os.Exit),cmd/kubelet/kubelet.go 只有 1488 字节,真正的装配都在同目录的 app/ 子包里。Go 官方文档对服务器项目的建议正是这个分工:命令放 cmd/,逻辑放 internal/3。

internal/ 是唯一有编译器背书的目录语义。 它不是惯例,是语言特性。Go 1.4 的发布说明写得很清楚:go 命令引入了一种机制,internal 包「不可以被其所在源码子树之外的包导入」4。现行工具链文档的表述更精确:internal 目录里(及其之下)的代码,只有与 internal 共享同一上级 import path 的代码才能导入5。大项目用它把「公开 API」和「私有实现」隔开——比如 multica 的 server module 把全部 49 个业务包都放进了 internal/。

pkg/ 是争议最大的一个。 它的流行来自那份 “Standard Go Project Layout”:把「可被外部引用的库代码」放进 pkg/6。但这份「标准」的 README 自己声明它不是官方标准,Russ Cox 在 issue #117 里说得更直白——Go 生态里绝大多数包并不放在 pkg/ 子目录里,他认可的最小布局只有三条:根目录放 LICENSE、go.mod、Go 代码,「That’s it. That’s the “standard”.」1。官方的组织哲学是按功能内聚切包,包名说明「提供什么」而不是「装了什么」7,util、common、misc 这类名字被官方博客点名反对8。事实上,prometheus 和 etcd 的顶层都没有 pkg/;multica 保留了 pkg/,但里面放的是「跨进程共享的契约」(sqlc 生成的 DAO、daemon 与 server 之间的 WebSocket 协议),语义已经和 project-layout 的本意不同了。

四个仓库,三套答案(均为 2026-10-31 的主分支)
仓库顶层目录风格二进制入口「私有实现」的围栏
kubernetescmd + pkg + staging,单 modulecmd/<组件>/,main 极薄公开面走 staging 发布;pkg/ 不对外
multica(server/)cmd + internal + pkg,单 modulecmd/server、cmd/multicainternal/ 圈住 49 个业务包
prometheus21 个功能包平铺,没有 pkg/cmd/prometheus、cmd/promtoolinternal/ 很小,近乎无围栏
etcd顶层目录 = module(go.work × 14)module 根的 main.gogo.mod 依赖图 + 个别 internal/

cmd/ 和 internal/ 看似标配,其实各自回答一个具体问题:有几个二进制要放?哪些包不许外人碰?

图 1 · 目录约定不是规定动作,而是对「入口收纳」和「可见性围栏」两个问题的回答;etcd 把两个答案都换了写法

cmd/ 和 internal/ 看起来是大项目的标配。但打开 etcd,这两个目录都不在——它们解决的问题消失了吗?

二、etcd:module 边界取代了 cmd/ 和 internal/

先看现象,etcd 的顶层:

$ ls etcd/
api/ cache/ client/ contrib/ etcdctl/ etcdutl/ pkg/
server/ tests/ tools/ go.work …

没有 cmd/,顶层也没有 internal/。原因藏在根目录的 go.work 里:这个仓库不是一个 Go module,而是 14 个 module 组成的 workspace9——server/、client/v3/、api/、etcdctl/、etcdutl/、pkg/、tests/……每个顶层目录自带 go.mod,独立版本、独立发布。

cmd/ 为什么消失了。 回想 cmd/ 的本质:它是「一个仓库要产出多个二进制」时的集中收纳。etcd 把每个二进制拆成了独立 module——server module 产 etcd,etcdctl module 产 etcdctl,etcdutl module 产 etcdutl。一个 module 只产出一个二进制时,main 包放在 module 根就是唯一合理的位置,cmd/ 反而成了多余的一层嵌套。server/main.go 确实就在那里,而且同样薄:整个文件只是 etcdmain.Main(os.Args) 的壳,文件注释自称 “simple wrapper of the real etcd entrypoint”10。k8s 恰好是反面的例子:单一大 module 要产出几十个二进制,才需要 cmd/ 来聚类。

internal/ 的消失要分两层说。 在 module 之间,围栏换了角色:go.mod 的依赖图本身就是可见性边界。client/v3 的 go.mod 只 require api/v3、client/pkg/v3 和 grpc 等少数依赖,根本不 require server/v3——外部项目(比如 k8s 的 apiserver)import client/v3 时,server 的 raft、bbolt 这些重依赖连进入依赖图的机会都没有。反过来,etcdutl 是快照恢复工具,它的 go.mod 老老实实 require 了 server/v3、bbolt 和 raft。谁轻谁重,go.mod 里写得明明白白。

但在 module 内部,internal/ 并没有被消灭:client/v3/internal/ 下面就躺着 endpoint/ 和 resolver/ 两个包,是 gRPC 端点解析的私有实现11。module 边界替代的只是 internal/ 在 module 之间的角色;module 之内想做细粒度隐藏,internal/ 依然是 Go 给的唯一工具。至于 server module 完全不用 internal/,更像历史惯例而非设计——etcdutl 既然能 require 整个 server/v3,server/etcdserver、server/storage 这些包在技术上就是「事实上的公开 API」。

etcd 的多 module 结构 etcd 仓库由 go.work 串起 14 个 module。外部项目 import client/v3 时依赖图里只有轻依赖;etcdutl 才 require 带 raft 和 bbolt 的 server/v3。internal 在 module 内部仍在使用:client/v3/internal 下有 endpoint 和 resolver 两个包。 etcd-io/etcd · 根 go.work 串起 14 个 module 外部项目 k8s.io/apiserver import client/v3 → go.etcd.io/etcd/client/v3 require api/v3 · client/pkg/v3 server/v3 不 require —— 依赖图即围栏 internal/ endpoint/ · resolver/ module 内部的隐藏仍靠它 go.etcd.io/etcd/server/v3 main.go → etcdmain.Main(...) require raft/v3 v3.7.0 bbolt v1.5.0 etcdctl main.go etcdutl main.go 轻 · 只依赖 client/v3 重 · 拖入 raft/bbolt 虚线:internal/ 没有被消灭, module 内部它依然在场。 顶层目录 = 发布物 = module:每个目录自带 go.mod。
图 2 · cmd/ 的角色被「一个 module 一个二进制」彻底取代;internal/ 只在 module 之间被依赖图取代,module 内部仍在使用

目录约定搞清楚了。回到最初的问题:controller 在哪?k8s 里明明有个 pkg/controller/——但打开它,会发现这个词被重新定义了。

三、k8s 的 controller:一个被重新定义的词

抱着找「接 HTTP 请求的那个 controller」的预期,打开 pkg/controller/deployment/deployment_controller.go,看到的 DeploymentController struct 里没有路由、没有 HTTP,最显眼的字段是一个队列:queue workqueue.TypedRateLimitingInterface[string]12。

原因是在 k8s 的语境里,controller 不是 MVC 的 controller,而是控制循环(control loop)。kube-controller-manager 的启动命令对自己的描述原文就是定义:“a controller is a control loop that watches the shared state of the cluster through the apiserver and makes changes attempting to move the current state towards the desired state”13。pkg/controller/ 下约 50 个目录——deployment、replicaset、statefulset、cronjob、namespace、garbagecollector……——全是这样的循环,由 cmd/kube-controller-manager 统一注册、统一启动。注册入口是 app/controller_descriptor.go 的 NewControllerDescriptors();读旧资料时留意,它以前叫 NewControllerInitializers,后来重构成了现在的名字14。

deployment controller 是这种循环的教科书样例,结构分三段(图 3)。它给 Deployment、ReplicaSet、Pod 三个 informer 注册事件回调,回调里不写任何业务逻辑,只做一件事:把对象的 key 放进队列。另一边,若干 worker 从队列里取 key,调 syncDeployment:从 informer 的本地缓存读对象、对比期望状态与实际状态、决定滚动更新还是扩缩容;失败就把 key 限流重入队,最多重试 15 次12。informer(监听 + 缓存)、workqueue(去重 + 限流重试)、sync loop(纠偏),三段拼出一个完整的 reconcile loop。

k8s 控制循环的三段式结构 deployment controller 的三段式:informer 事件回调只做 dc.queue.Add(key);workqueue 去重限流;worker 出队调 syncDeployment 纠偏,失败 AddRateLimited 重入队(maxRetries=15)。全部控制器由 NewControllerDescriptors 统一注册。 pkg/controller/deployment · deployment_controller.go NewControllerDescriptors() controller_descriptor.go register × 约 50 个控制器 informer 事件回调 3 个对象 d/rs/podInformer ResourceEventHandlerFuncs 回调只做一件事: dc.queue.Add(key) workqueue 去重 · 限流 TypedRateLimiting Interface[string] 同一个 key 只留一份 processNextWorkItem syncDeployment 按 key 读 Deployment 对比 ReplicaSet 实际状态 rolloutRolling / rolloutRecreate / sync key 入队 worker 出队 失败 AddRateLimited 重入队 · maxRetries = 15 事件回调不写业务逻辑,只入队一个 key;纠偏全部发生在 syncDeployment。
图 3 · k8s 的 controller 是 informer + workqueue + sync 的控制循环:事件只落一个 key,真正的业务在异步纠偏里

那 HTTP 请求路径上有什么?apiserver 收到「PUT 一个 Deployment」时,走的是认证 → 鉴权 → 准入 → 存储,全程无状态、没有「业务逻辑」可言——真正的业务(该建几个 Pod、怎么滚动)发生在上面那个异步循环里。请求路径上唯一像 Java 分层里某个角色的,是 pkg/registry:按 API group 和资源组织的 REST storage 层,core 组下 pod、service、node、namespace、configmap……每种资源一个目录,把 CRUD 收口到 apiserver 的 generic registry,最终落到唯一的持久化后端 etcd15。(严谨起见标注一下:各资源目录内部的 storage/storage.go 文件名是按惯例的推断,本次只核实到目录层级。)

对齐一下两边的词汇:Java 的 controller ≈ apiserver 的 REST / registry 层(接请求,但不做业务);Java 的 service ≈ 不存在——它的职责整体转移给了 pkg/controller 里的异步控制循环;Java 的 DAO ≈ pkg/registry + etcd3 store。

难道 Go 项目都没有 service 层?那倒未必——换一个业务系统看看。

四、multica:长回三层的 Go 项目

multica 是用 Go 写的 AI agent 协作平台,它的 server module 是教科书式的 cmd + internal + pkg。打开 internal/,两个包的名字毫不掩饰:handler、service。

以「评论里 @ 了一个 agent,把它叫起来干活」为例,整条调用链可以逐行对上(图 4):

  1. handler 层:internal/handler/comment.go 的 CreateComment(:1629)接请求;computeCommentAgentTriggers(:2636)算出这条评论该叫醒谁,@提及由 util.ParseMentions 解析16。
  2. service 层:handler 调 TaskService.EnqueueTaskForMention,最终落到 internal/service/task.go 的 enqueueMentionTaskWithCommentPlan(:1464)——事务、去重(哨兵错误 ErrDuplicatePendingTask)、入队都在这里17。
  3. DAO 层:领取任务时 claimTask(:3612)调用 qtx.ClaimAgentTask。而 ClaimAgentTask 不是手写的 Go 函数——它是 sqlc 从 pkg/db/queries/agent.sql:734 那段 SQL(-- name: ClaimAgentTask :one)生成的类型安全方法18。queries/ 下 67 个 .sql 文件的角色几乎就是 MyBatis 的 mapper XML,generated/ 目录是生成的 DAO。

对照 Spring 官方文档的定义——@Controller、@Service、@Repository 分别是表现层、业务层、持久层的 @Component 特化19——multica 的 handler / service / db 几乎一一对应。

multica 的三层与 Java 对照 左边是 Java Spring 的 @Controller/@Service/@Repository 三层;右边是 multica 的真实调用链:handler/comment.go 的 CreateComment 到 service/task.go 的 EnqueueTaskForMention,再到 pkg/db 里 sqlc 从 agent.sql:734 生成的 ClaimAgentTask。虚线表示读路径 handler 直查 db、不经过 service。 Java Spring(对照) multica/server · main @ 2026-10-31 @Controller 表现层 @Service 业务层 @Repository 持久层(DAO) 注解把 bean 按技术职能标层 internal/handler/comment.go chi CreateComment(w, r) :1629 computeCommentAgentTriggers :2636 internal/service/task.go TaskService EnqueueTaskForMention enqueueMentionTaskWithCommentPlan :1464 pkg/db(sqlc) SQL 即 DAO queries/agent.sql:734 ClaimAgentTask generated/agent.sql.go(生成) 写 / 编排走 service 类型安全调用 虚线:读路径 handler 持有 h.Queries 直查 db,不经过 service。
图 4 · multica 的 handler/service/db 与 Java 三层一一对应,但读路径允许越层——分层存在,边界更松

和 Java 严格三层相比,multica 有两处明显的「Go 式松动」。一是边界:写路径和业务编排走 service,简单的读路径 handler 直接持有 h.Queries 查 db,不经过 service——在 Java 的严格分层里,这是会被 code review 拦下来的写法。二是装配:没有框架扫描,也没有依赖注入容器,cmd/server/main.go 手工 new 出一切(db.New(pool),再逐个传给 service);路由集中在 130KB 的 router.go 里注册,那张路由表的角色约等于一整份 @RequestMapping 清单。

为什么这个项目「敢」分层、也需要分层?因为它是 CRUD 业务系统:同步请求路径上真的有编排要做——解析提及、计算该触发谁、去重、入队、保证同一 (issue, agent) 的任务串行。业务形态决定了分层的收益。

那是不是所有 Go 项目都该抄这三层?prometheus 给出了否定的答案。

五、prometheus:没有 service 层生态位的系统

prometheus 的顶层是另一种风格:21 个目录全部平铺——scrape/、rules/、promql/、storage/、tsdb/、web/、discovery/、notifier/……没有 pkg/,cmd/ 下也只有 prometheus 和 promtool 两个入口。

它的 HTTP 层看起来像 controller:web/api/v1/api.go 一个 97KB 的文件装下全部 v1 端点,type API struct + NewAPI(...) + Register(router) 逐条注册路由20。但往下看一层就断了:没有 service 层。NewAPI 的 40 多个参数直接是各子系统的本尊——promql.QueryEngine、storage.Queryable、scrape 和 rules 的 retriever——handler 里直接 api.QueryEngine.NewInstantQuery(...) 然后执行,web 层只剩下参数解析、错误码映射、响应序列化。

业务逻辑住在各子系统自己的目录里,而且是纵切自治的。以 scrape/ 为例:manager.go 管配置重载和 target 集合,scrape.go(81KB)是抓取循环,target.go 是领域对象,metrics.go 是这个子系统自己的监控指标——一个目录就是一个自包含的小系统,对外只暴露一个 Manager,等着 cmd/prometheus/main.go 那个 93KB 的装配文件把它接进全局21。rules/、discovery/(下挂约 30 个服务发现来源)、notifier/ 都是同样的形态。storage/ 与 tsdb/ 的分离则是另一个标本:storage/interface.go(31KB)定义 Queryable / Appendable 接口,tsdb/ 是内置实现,web、promql、rules 只依赖接口,实现由 main 在装配时注入。

prometheus 的 web 层直调功能包 web/api/v1/api.go 的 API struct 通过 NewAPI 的 40 多个参数直接持有 promql、storage、scrape、rules 各子系统的本尊,中间没有 service 层;storage 是接口,tsdb 是实现。 prometheus/prometheus · 顶层 21 个功能包平铺,没有 pkg/ web/api/v1/api.go 97KB type API struct NewAPI(40+ 个参数) 直接持有 QueryEngine / Queryable / retrievers promql engine.go 199KB scrape scrape.go 81KB rules manager.go storage 接口 · 31KB tsdb 实现 handler 直调 实现 …(discovery、notifier 同构) 没有 service 层:API struct 直接持有各子系统的本尊,web 只剩协议适配。
图 5 · prometheus 的 web 层像 controller,但 handler 星形直连各功能包——service 层没有生态位

prometheus 为什么没有 service 层的生态位?因为它的「业务」本来就不是「一次请求编排多个领域对象」,而是各子系统永不停歇的主循环:抓取、评估规则、发告警。HTTP API 只是探视这些循环的窗口,窗口后面没有等待编排的东西,service 层自然无从谈起。

这种切法在 Go 社区有理论表述。Ben Johnson 的 Standard Package Layout 主张领域类型放根包、子包按依赖分组(postgres、http 各做一个适配器),并且明确要求根包不依赖应用内任何其他包22;Clean Architecture 在 Go 里的实践(ports / adapters / application / domain)同样把分层规则定义成依赖方向:外层可以引用内层,反过来不行23。也就是说,Go 社区不是不分层,而是把分层的轴从「技术职能」(controller/service/dao 横切)换成了「依赖方向与领域」(纵切)。

三个项目,三种答案。回头看开头的五个问题,可以收拢了。

六、写在最后

回到开头那个 ls 的结果。现在再看 k8s 的顶层目录,cmd、pkg、staging 不再是陌生的词汇;再遇到一个新的 Go 仓库,找代码的姿势可以总结成三步:

  1. 先看这个仓库产出什么:cmd/ 下有几个二进制?或者像 etcd 那样,go.work 串了几个 module、哪个 module 的根上有 main.go?
  2. 再看边界在哪:internal/ 圈出了私有实现;go.mod 的 require 圈出了 module 之间的真实依赖。
  3. 最后按业务形态猜逻辑的位置:CRUD 业务系统,去请求路径上找(handler/service);声明式基础设施,去控制循环里找(reconcile loop);单一职责系统,去各子系统的主循环里找,HTTP 层只是窗口。

五个问题的答案收拢如下:

  1. cmd/ 是「多二进制」的收纳惯例,internal/ 是 Go 1.4 起编译器强制的可见性围栏,pkg/ 是流行但有争议的社区惯例——所谓「标准 Go 布局」并不标准。
  2. etcd 没有 cmd/,因为「一个 module 一个二进制」让集中收纳失去了意义;internal/ 在 module 之间被 go.mod 依赖图取代,在 module 内部依然在场(client/v3/internal 为证)。
  3. k8s 的 controller 是控制循环,不是 MVC controller;service 层不存在,职责转移给了异步的 reconcile;DAO 的对应物是 pkg/registry + etcd3 store。
  4. multica 真实存在 handler / service / db 三层(sqlc 让 SQL 即 DAO),因为它是 CRUD 业务系统;但边界比 Java 松,读路径 handler 直查 db。
  5. prometheus 没有 service 层的生态位:子系统纵切自治,web 层直调功能包,装配集中在 main.go。

一句话总结:Go 项目的目录结构不是语言规定的,而是业务形态(同步 CRUD 还是异步控制循环)和发布形态(单 module 还是多 module)决定的;service 和 controller 的概念都在,只是住的楼层不一样了。

参考资料

Footnotes

  1. this is not a standard Go project layout · Issue #117Russ Cox · GitHub · 2021-04-09github.com ↗rsc:"the vast majority of packages in the Go ecosystem do not put the importable packages in a pkg subdirectory"https://github.com/golang-standards/project-layout/issues/117 ↩ ↩2

  2. kubernetes/staging/README.mdGitHubgithub.com ↗"The code in the staging/ directory is authoritative",由 publishing-bot 单向发布到 k8s.io 各仓库https://github.com/kubernetes/kubernetes/blob/master/staging/README.md ↩

  3. Organizing a Go modulego.devgo.dev ↗官方对服务器项目的建议:命令放 cmd/,逻辑放 internal/;示例里没有 pkg/https://go.dev/doc/modules/layout ↩

  4. Go 1.4 Release Notes — Internal packagesgo.dev · 2014-12go.dev ↗internal 包机制的引入:"may not be imported by packages outside the source subtree in which they reside"https://go.dev/doc/go1.4 ↩

  5. cmd/go 文档 — Internal packagespkg.go.devpkg.go.dev ↗"Code in or below a directory named 'internal' is importable only by code that shares the same import path above the internal directory."https://pkg.go.dev/cmd/go#hdr-Internal_packages ↩

  6. Standard Go Project LayoutGitHubgithub.com ↗README 自述:"This is NOT an official standard defined by the core Go dev team"https://github.com/golang-standards/project-layout ↩

  7. Organizing Go codeAndrew Gerrand · Go Blog · 2012-08-16go.dev ↗包名为内容提供上下文;最小化导出接口("If in doubt, leave it out!")https://go.dev/blog/organizing-go-code ↩

  8. Package namesSameer Ajmani · Go Blog · 2015-02-04go.dev ↗"Avoid meaningless package names. Packages named util, common, or misc …"https://go.dev/blog/package-names ↩

  9. etcd/go.workGitHubgithub.com ↗2026-10-31 的 main 分支:14 个 use 项,server、client/v3、api、etcdctl、etcdutl 等各为独立 modulehttps://github.com/etcd-io/etcd/blob/main/go.work ↩

  10. etcd/server/main.goGitHubgithub.com ↗文件注释自称 "simple wrapper of the real etcd entrypoint",只调 etcdmain.Main(os.Args)https://github.com/etcd-io/etcd/blob/main/server/main.go ↩

  11. etcd/client/v3/internalGitHubgithub.com ↗endpoint/ 与 resolver/ 两个包:module 内部仍在使用 internal/ 的实证https://github.com/etcd-io/etcd/tree/main/client/v3/internal ↩

  12. pkg/controller/deployment/deployment_controller.goGitHubgithub.com ↗informer 回调只做 dc.queue.Add(key);worker 出队调 syncDeployment;失败 AddRateLimited,maxRetries=15https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/deployment/deployment_controller.go ↩ ↩2

  13. cmd/kube-controller-manager/app/controllermanager.goGitHubgithub.com ↗cobra Long 描述:"a controller is a control loop that watches the shared state of the cluster …"https://github.com/kubernetes/kubernetes/blob/master/cmd/kube-controller-manager/app/controllermanager.go ↩

  14. cmd/kube-controller-manager/app/controller_descriptor.goGitHubgithub.com ↗NewControllerDescriptors() 注册全部内置控制器;旧名 NewControllerInitializers 已重构https://github.com/kubernetes/kubernetes/blob/master/cmd/kube-controller-manager/app/controller_descriptor.go ↩

  15. pkg/registryGitHubgithub.com ↗按 API group / 资源组织的 REST storage 层,core 下 pod、service、node、namespace 等各一个目录https://github.com/kubernetes/kubernetes/tree/master/pkg/registry ↩

  16. multica/server/internal/handler/comment.goGitHubgithub.com ↗CreateComment :1629;computeCommentAgentTriggers :2636(行号为 2026-10-31 的 main)https://github.com/multica-ai/multica/blob/main/server/internal/handler/comment.go ↩

  17. multica/server/internal/service/task.goGitHubgithub.com ↗enqueueMentionTaskWithCommentPlan :1464;claimTask :3612;ErrDuplicatePendingTask :755https://github.com/multica-ai/multica/blob/main/server/internal/service/task.go ↩

  18. multica/server/pkg/db/queries/agent.sqlGitHubgithub.com ↗:734 -- name: ClaimAgentTask :one,sqlc 据此生成类型安全的 Go 方法https://github.com/multica-ai/multica/blob/main/server/pkg/db/queries/agent.sql ↩

  19. Classpath Scanning and Managed Components — Stereotype AnnotationsSpring Framework 文档docs.spring.io ↗"@Repository, @Service, and @Controller are specializations of @Component … (persistence, service, and presentation layers)"https://docs.spring.io/spring-framework/reference/core/beans/classpath-scanning.html ↩

  20. prometheus/web/api/v1/api.goGitHubgithub.com ↗97KB 装下全部 v1 端点;NewAPI 的 40+ 个参数直接是 promql.QueryEngine、storage.Queryable 等本尊https://github.com/prometheus/prometheus/blob/main/web/api/v1/api.go ↩

  21. prometheus/cmd/prometheus/main.goGitHubgithub.com ↗93KB 的手工装配文件:各子系统 Manager 在这里接进全局https://github.com/prometheus/prometheus/blob/main/cmd/prometheus/main.go ↩

  22. Standard Package LayoutBen Johnson · Go Beyond · 2016-08-10gobeyond.dev ↗领域类型放根包:"The root package should not depend on any other package in your application!"https://www.gobeyond.dev/standard-package-layout/ ↩

  23. How to implement Clean Architecture in Go (Golang)Miłosz Smółka · Three Dots Labs · 2020-09-01threedots.tech ↗ports / adapters / application / domain 四层,分层规则是依赖方向https://threedots.tech/post/introducing-clean-architecture/ ↩